chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { logger, workspaceRoot, ProjectConfiguration } from '@nx/devkit';
|
||||
import { hashWithWorkspaceContext } from 'nx/src/utils/workspace-context';
|
||||
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
|
||||
import { hashObject } from 'nx/src/hasher/file-hasher';
|
||||
import { PluginCache } from 'nx/src/utils/plugin-cache-utils';
|
||||
|
||||
export interface AnalysisSuccessResult {
|
||||
// Maps project file path -> node configuration
|
||||
nodesByFile: Record<string, ProjectConfiguration>;
|
||||
// Maps project root -> referenced project roots
|
||||
referencesByRoot: Record<
|
||||
string,
|
||||
{ refs: string[]; sourceConfigFile: string }
|
||||
>;
|
||||
}
|
||||
export interface AnalysisErrorResult {
|
||||
error: Error;
|
||||
}
|
||||
export type AnalysisResult = AnalysisSuccessResult | AnalysisErrorResult;
|
||||
|
||||
/**
|
||||
* Options passed to the MSBuild analyzer.
|
||||
* These are the target names that the analyzer will use to generate targets.
|
||||
*/
|
||||
export interface DotNetAnalyzerOptions {
|
||||
buildTargetName?: string;
|
||||
testTargetName?: string;
|
||||
cleanTargetName?: string;
|
||||
restoreTargetName?: string;
|
||||
publishTargetName?: string;
|
||||
packTargetName?: string;
|
||||
}
|
||||
|
||||
interface AnalyzerCache {
|
||||
hash: string;
|
||||
result: AnalysisResult;
|
||||
}
|
||||
|
||||
let cache: AnalyzerCache | null = null;
|
||||
|
||||
/**
|
||||
* Get the path to the msbuild-analyzer executable
|
||||
*/
|
||||
function getAnalyzerPath(): string {
|
||||
const executableName = 'MsbuildAnalyzer.dll';
|
||||
|
||||
const possiblePaths = [
|
||||
// When running from dist/packages/dotnet
|
||||
join(__dirname, '..', 'lib', executableName),
|
||||
// When running from packages/dotnet/src (development)
|
||||
join(__dirname, 'lib', executableName),
|
||||
];
|
||||
|
||||
for (const path of possiblePaths) {
|
||||
if (existsSync(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`msbuild-analyzer not found at any expected location. Please build it first with: nx run dotnet:copy-assets`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash every file that affects MSBuild evaluation — project files plus the Directory.*
|
||||
* matches surfaced by the createNodesV2 glob. The analyzer partitions the same list on
|
||||
* its side, so we don't classify it here.
|
||||
*/
|
||||
async function calculateProjectFilesHash(files: string[]): Promise<string> {
|
||||
return await hashWithWorkspaceContext(workspaceRoot, files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the msbuild-analyzer and return the results.
|
||||
* Uses stdin for large file lists to avoid ARG_MAX issues.
|
||||
*/
|
||||
function runAnalyzer(
|
||||
files: string[],
|
||||
options?: DotNetAnalyzerOptions
|
||||
): AnalysisSuccessResult {
|
||||
if (files.length === 0) {
|
||||
return { nodesByFile: {}, referencesByRoot: {} };
|
||||
}
|
||||
|
||||
const analyzerPath = getAnalyzerPath();
|
||||
|
||||
// Set environment variables for the analyzer process
|
||||
const env = { ...process.env };
|
||||
|
||||
// TODO(@AgentEnder): Remove this if anyone reports issues with being unable
|
||||
// to locate the .NET runtime, currently I'm not hitting the issue but when I was
|
||||
// this solved it, and it took a deal of effort to track down so I'm leaving it here commented for now.
|
||||
// In Nx 23, if no one has reported the issue, its probably safe to remove.
|
||||
//
|
||||
// On macOS/Linux, set library path to help find libhostfxr.dylib
|
||||
// if (process.platform === 'darwin' || process.platform === 'linux') {
|
||||
// const dotnetRoot = process.env.DOTNET_ROOT || '/usr/local/share/dotnet';
|
||||
// const hostFxrPath = join(dotnetRoot, 'host', 'fxr');
|
||||
|
||||
// if (existsSync(hostFxrPath)) {
|
||||
// const versions = readdirSync(hostFxrPath);
|
||||
// if (versions.length > 0) {
|
||||
// // Use the latest version
|
||||
// const latestVersion = versions.sort().reverse()[0];
|
||||
// const fxrDir = join(hostFxrPath, latestVersion);
|
||||
|
||||
// const envVar =
|
||||
// process.platform === 'darwin'
|
||||
// ? 'DYLD_FALLBACK_LIBRARY_PATH'
|
||||
// : 'LD_LIBRARY_PATH';
|
||||
// const currentValue = env[envVar];
|
||||
// env[envVar] = currentValue ? `${fxrDir}:${currentValue}` : fxrDir;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
try {
|
||||
let output: string;
|
||||
|
||||
// Prepare CLI arguments
|
||||
const args = [analyzerPath, workspaceRoot];
|
||||
|
||||
// Add plugin options as JSON string if provided
|
||||
if (options) {
|
||||
args.push(JSON.stringify(options));
|
||||
}
|
||||
|
||||
// Use stdin mode for large file lists to avoid ARG_MAX issues. The analyzer
|
||||
// partitions paths by filename, so we just stream everything in one block.
|
||||
const input = files.join('\n');
|
||||
const result = spawnSync('dotnet', args, {
|
||||
input,
|
||||
encoding: 'utf-8',
|
||||
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
||||
stdio: ['pipe', 'pipe', 'pipe'], // stdin, stdout, stderr
|
||||
windowsHide: true,
|
||||
env,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`Analyzer exited with code ${result.status}: ${result.stderr}`
|
||||
);
|
||||
}
|
||||
|
||||
// Output stderr (includes performance logs when NX_PERF_LOGGING=true)
|
||||
if (result.stderr) {
|
||||
console.error(result.stderr);
|
||||
}
|
||||
|
||||
output = result.stdout;
|
||||
|
||||
return JSON.parse(output) as AnalysisSuccessResult;
|
||||
} catch (error) {
|
||||
const err = error as { stderr?: string; message: string };
|
||||
if (err.stderr) {
|
||||
logger.error(`msbuild-analyzer error: ${err.stderr}`);
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to run msbuild-analyzer: ${err.message}${
|
||||
err.stderr ? `\n${err.stderr}` : ''
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project analysis results for the given project files.
|
||||
* Results are cached based on the content hash of all project files.
|
||||
* This should be called by createNodes to populate the cache.
|
||||
*/
|
||||
export async function analyzeProjects(
|
||||
files: string[],
|
||||
options?: DotNetAnalyzerOptions
|
||||
): Promise<AnalysisResult> {
|
||||
const filesHash = await calculateProjectFilesHash(files);
|
||||
|
||||
// Return cached results if the hash matches
|
||||
if (
|
||||
cache &&
|
||||
cache.hash === filesHash &&
|
||||
// NOTE: We don't read from the cache here if it's an error result,
|
||||
// to allow retrying analysis in case of transient errors or errors fixed
|
||||
// that may not be reflected in the hash (like setting an env var).
|
||||
isAnalysisSuccessResult(cache.result)
|
||||
) {
|
||||
return cache.result;
|
||||
}
|
||||
|
||||
const optionsHash = hashObject(options);
|
||||
const analyzerCache = new PluginCache<AnalysisSuccessResult>(
|
||||
join(workspaceDataDirectory, `dotnet-${optionsHash}.hash`)
|
||||
);
|
||||
const cachedResult = analyzerCache.get(filesHash);
|
||||
if (cachedResult) {
|
||||
// Update cache
|
||||
cache = {
|
||||
hash: filesHash,
|
||||
result: cachedResult,
|
||||
};
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
// Run the analyzer
|
||||
try {
|
||||
const result = runAnalyzer(files, options);
|
||||
|
||||
// Update local cache
|
||||
cache = {
|
||||
hash: filesHash,
|
||||
result,
|
||||
};
|
||||
// Update persistent cache
|
||||
analyzerCache.set(filesHash, result);
|
||||
analyzerCache.writeToDisk();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
// We save the error result in the local cache to avoid getting
|
||||
// a different error when reading the cached result to createDependencies.
|
||||
// Instead, we'll find a cached error and know that it was printed earlier.
|
||||
// We DO NOT save error results to the on-disk cache to allow retries without
|
||||
// running `nx reset`
|
||||
const errorResult: AnalysisErrorResult = {
|
||||
error: err,
|
||||
};
|
||||
cache = {
|
||||
hash: filesHash,
|
||||
result: errorResult,
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the cached analysis results without running the analyzer.
|
||||
* This should be called by createDependencies, which always runs after createNodes.
|
||||
* If the cache is empty, returns an empty result (this shouldn't happen in normal operation).
|
||||
*/
|
||||
export function readCachedAnalysisResult(): AnalysisResult {
|
||||
if (cache) {
|
||||
return cache.result;
|
||||
}
|
||||
|
||||
// This shouldn't happen since createNodes always runs first
|
||||
throw new Error(
|
||||
'Analysis result cache is empty. Ensure that analyzeProjects() is called before readCachedAnalysisResult().'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache (useful for testing)
|
||||
*/
|
||||
export function clearCache(): void {
|
||||
cache = null;
|
||||
}
|
||||
|
||||
export function isAnalysisErrorResult(
|
||||
result: AnalysisResult
|
||||
): result is AnalysisErrorResult {
|
||||
return 'error' in result;
|
||||
}
|
||||
|
||||
export function isAnalysisSuccessResult(
|
||||
result: AnalysisResult
|
||||
): result is AnalysisSuccessResult {
|
||||
return !('error' in result);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`ci-workflow generator connected to nxCloud circleci pipeline should match snapshot 1`] = `
|
||||
"version: 2.1
|
||||
|
||||
orbs:
|
||||
nx: nrwl/nx@1.6.2
|
||||
|
||||
env:
|
||||
NX_BATCH_MODE: true
|
||||
|
||||
jobs:
|
||||
main:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/dotnet/sdk:10.0
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
# This enables task distribution via Nx Cloud
|
||||
# Run this command as early as possible, before dependencies are installed
|
||||
# Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
|
||||
# Uncomment this line to enable task distribution
|
||||
# - run: ./nx start-ci-run --distribute-on="3 linux-medium-dotnet" --stop-agents-after="build"
|
||||
|
||||
- nx/set-shas:
|
||||
main-branch-name: 'main'
|
||||
|
||||
# # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected.
|
||||
- run:
|
||||
command: ./nx affected --base=$NX_BASE --head=$NX_HEAD -t build
|
||||
# Nx Cloud recommends fixes for failures to help you get CI green faster. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
- run:
|
||||
command: ./nx fix-ci
|
||||
when: always
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
ci:
|
||||
jobs:
|
||||
- main
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ci-workflow generator connected to nxCloud github pipeline should match snapshot 1`] = `
|
||||
"name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NX_BATCH_MODE: true
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# This enables task distribution via Nx Cloud
|
||||
# Run this command as early as possible, before dependencies are installed
|
||||
# Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
|
||||
# Uncomment this line to enable task distribution
|
||||
# - run: ./nx start-ci-run --distribute-on="3 linux-medium-dotnet" --stop-agents-after="build"
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.x'
|
||||
|
||||
- uses: nrwl/nx-set-shas@v4
|
||||
|
||||
# # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected.
|
||||
- run: ./nx affected -t build
|
||||
# Nx Cloud recommends fixes for failures to help you get CI green faster. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
- run: ./nx fix-ci
|
||||
if: always()
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ci-workflow generator not connected to nxCloud circleci pipeline should match snapshot 1`] = `
|
||||
"version: 2.1
|
||||
|
||||
orbs:
|
||||
nx: nrwl/nx@1.6.2
|
||||
|
||||
env:
|
||||
NX_BATCH_MODE: true
|
||||
|
||||
jobs:
|
||||
main:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/dotnet/sdk:10.0
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
# This enables task distribution via Nx Cloud
|
||||
# Run this command as early as possible, before dependencies are installed
|
||||
# Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
|
||||
# Connect your workspace by running "nx connect" and uncomment this line to enable task distribution
|
||||
# - run: ./nx start-ci-run --distribute-on="3 linux-medium-dotnet" --stop-agents-after="build"
|
||||
|
||||
- nx/set-shas:
|
||||
main-branch-name: 'main'
|
||||
|
||||
# # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected.
|
||||
- run:
|
||||
command: ./nx affected --base=$NX_BASE --head=$NX_HEAD -t build
|
||||
# Nx Cloud recommends fixes for failures to help you get CI green faster. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
- run:
|
||||
command: ./nx fix-ci
|
||||
when: always
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
ci:
|
||||
jobs:
|
||||
- main
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ci-workflow generator not connected to nxCloud github pipeline should match snapshot 1`] = `
|
||||
"name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NX_BATCH_MODE: true
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# This enables task distribution via Nx Cloud
|
||||
# Run this command as early as possible, before dependencies are installed
|
||||
# Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
|
||||
# Connect your workspace by running "nx connect" and uncomment this line to enable task distribution
|
||||
# - run: ./nx start-ci-run --distribute-on="3 linux-medium-dotnet" --stop-agents-after="build"
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.x'
|
||||
|
||||
- uses: nrwl/nx-set-shas@v4
|
||||
|
||||
# # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected.
|
||||
- run: ./nx affected -t build
|
||||
# Nx Cloud recommends fixes for failures to help you get CI green faster. Learn more: https://nx.dev/ci/features/self-healing-ci
|
||||
- run: ./nx fix-ci
|
||||
if: always()
|
||||
"
|
||||
`;
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
nx: nrwl/nx@1.6.2
|
||||
|
||||
env:
|
||||
NX_BATCH_MODE: true
|
||||
|
||||
jobs:
|
||||
main:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/dotnet/sdk:10.0
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
# This enables task distribution via Nx Cloud
|
||||
# Run this command as early as possible, before dependencies are installed
|
||||
# Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
|
||||
<% if (connectedToCloud) { %># Uncomment this line to enable task distribution<% } else { %># Connect your workspace by running "nx connect" and uncomment this line to enable task distribution<% } %>
|
||||
# - run: ./nx start-ci-run --distribute-on="3 linux-medium-dotnet" --stop-agents-after="build"
|
||||
|
||||
- nx/set-shas:
|
||||
main-branch-name: '<%= mainBranch %>'
|
||||
<% for (const command of commands) { %><% if (command.comments) { %><% for (const comment of command.comments) { %>
|
||||
# <%- comment %><% } %><% } %><% if (command.command) { %>
|
||||
- run:
|
||||
command: <%= command.command %><% if (command.alwaysRun) { %>
|
||||
when: always<% } %><% } %><% } %>
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
<%= workflowFileName %>:
|
||||
jobs:
|
||||
- main
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
name: <%= workflowName %>
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- <%= mainBranch %>
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NX_BATCH_MODE: true
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# This enables task distribution via Nx Cloud
|
||||
# Run this command as early as possible, before dependencies are installed
|
||||
# Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
|
||||
<% if (connectedToCloud) { %># Uncomment this line to enable task distribution<% } else { %># Connect your workspace by running "nx connect" and uncomment this line to enable task distribution<% } %>
|
||||
# - run: ./nx start-ci-run --distribute-on="3 linux-medium-dotnet" --stop-agents-after="build"
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.x'
|
||||
|
||||
- uses: nrwl/nx-set-shas@v4
|
||||
<% for (const command of commands) { %><% if (command.comments) { %><% for (const comment of command.comments) { %>
|
||||
# <%- comment %><% } %><% } %><% if (command.command) { %>
|
||||
- run: <%= command.command %><% if (command.alwaysRun) { %>
|
||||
if: always()<% } %><% } %><% } %>
|
||||
@@ -0,0 +1,52 @@
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { readNxJson, Tree, updateNxJson } from '@nx/devkit';
|
||||
|
||||
import { ciWorkflowGenerator } from './generator';
|
||||
|
||||
describe('ci-workflow generator', () => {
|
||||
let tree: Tree;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
['connected to nxCloud', true],
|
||||
['not connected to nxCloud', false],
|
||||
] as const)(`%s`, (_, connectedToCloud) => {
|
||||
let nxCloudAccessToken: string;
|
||||
|
||||
beforeEach(() => {
|
||||
if (connectedToCloud) {
|
||||
const nxJson = readNxJson(tree);
|
||||
nxJson.nxCloudAccessToken = 'test';
|
||||
updateNxJson(tree, nxJson);
|
||||
} else {
|
||||
nxCloudAccessToken = process.env.NX_CLOUD_ACCESS_TOKEN;
|
||||
delete process.env.NX_CLOUD_ACCESS_TOKEN;
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (connectedToCloud) {
|
||||
const nxJson = readNxJson(tree);
|
||||
delete nxJson.nxCloudAccessToken;
|
||||
updateNxJson(tree, nxJson);
|
||||
} else {
|
||||
process.env.NX_CLOUD_ACCESS_TOKEN = nxCloudAccessToken;
|
||||
}
|
||||
});
|
||||
describe.each([
|
||||
['github', '.github/workflows/ci.yml'],
|
||||
['circleci', '.circleci/config.yml'],
|
||||
] as const)(`%s pipeline`, (ciProvider, output) => {
|
||||
it('should match snapshot', async () => {
|
||||
await ciWorkflowGenerator(tree, {
|
||||
name: 'CI',
|
||||
ci: ciProvider,
|
||||
});
|
||||
expect(tree.read(output, 'utf-8')).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
formatFiles,
|
||||
generateFiles,
|
||||
names,
|
||||
readNxJson,
|
||||
Tree,
|
||||
} from '@nx/devkit';
|
||||
import { join } from 'path';
|
||||
import { getNxCloudUrl, isNxCloudUsed } from 'nx/src/utils/nx-cloud-utils';
|
||||
import { deduceDefaultBase } from 'nx/src/utils/default-base';
|
||||
|
||||
const NX_AFFECTED_COMMENT = `# Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected.`;
|
||||
|
||||
function getCiCommands(ci: Schema['ci']): Command[] {
|
||||
switch (ci) {
|
||||
case 'circleci': {
|
||||
return [
|
||||
{
|
||||
comments: [NX_AFFECTED_COMMENT],
|
||||
},
|
||||
{
|
||||
command: `./nx affected --base=$NX_BASE --head=$NX_HEAD -t build`,
|
||||
},
|
||||
getNxCloudFixCiCommand(),
|
||||
];
|
||||
}
|
||||
default: {
|
||||
return [
|
||||
{
|
||||
comments: [NX_AFFECTED_COMMENT],
|
||||
command: `./nx affected -t build`,
|
||||
},
|
||||
getNxCloudFixCiCommand(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNxCloudFixCiCommand(): Command {
|
||||
return {
|
||||
comments: [
|
||||
`Nx Cloud recommends fixes for failures to help you get CI green faster. Learn more: https://nx.dev/ci/features/self-healing-ci`,
|
||||
],
|
||||
command: `./nx fix-ci`,
|
||||
alwaysRun: true,
|
||||
};
|
||||
}
|
||||
|
||||
export type Command = {
|
||||
command?: string;
|
||||
comments?: string[];
|
||||
alwaysRun?: boolean;
|
||||
};
|
||||
|
||||
export interface Schema {
|
||||
name: string;
|
||||
ci: 'github' | 'circleci';
|
||||
/**
|
||||
* Custom commands to run in the CI workflow.
|
||||
* If not provided, default commands will be generated based on the CI provider.
|
||||
*/
|
||||
commands?: Command[];
|
||||
}
|
||||
|
||||
export async function ciWorkflowGenerator(tree: Tree, schema: Schema) {
|
||||
const ci = schema.ci;
|
||||
|
||||
const options = getTemplateData(tree, schema);
|
||||
generateFiles(tree, join(__dirname, 'files', ci), '', options);
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
interface Substitutes {
|
||||
mainBranch: string;
|
||||
workflowName: string;
|
||||
workflowFileName: string;
|
||||
commands: Command[];
|
||||
nxCloudHost: string;
|
||||
connectedToCloud: boolean;
|
||||
}
|
||||
|
||||
function getTemplateData(tree: Tree, options: Schema): Substitutes {
|
||||
const { name: workflowName, fileName: workflowFileName } = names(
|
||||
options.name
|
||||
);
|
||||
|
||||
let nxCloudHost: string = 'nx.app';
|
||||
try {
|
||||
const nxCloudUrl = getNxCloudUrl(readNxJson(tree));
|
||||
nxCloudHost = new URL(nxCloudUrl).host;
|
||||
} catch {}
|
||||
|
||||
const mainBranch = deduceDefaultBase();
|
||||
|
||||
const commands = options.commands ?? getCiCommands(options.ci);
|
||||
|
||||
const connectedToCloud = isNxCloudUsed(readNxJson(tree));
|
||||
|
||||
return {
|
||||
workflowName,
|
||||
workflowFileName,
|
||||
commands,
|
||||
mainBranch,
|
||||
nxCloudHost,
|
||||
connectedToCloud,
|
||||
};
|
||||
}
|
||||
|
||||
export default ciWorkflowGenerator;
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"$id": "NxDotnetCiWorkflowSchema",
|
||||
"title": ".NET CI Workflow Generator",
|
||||
"description": "Setup a CI Workflow to run Nx in CI.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ci": {
|
||||
"type": "string",
|
||||
"description": "CI provider.",
|
||||
"enum": ["github", "circleci"],
|
||||
"x-prompt": {
|
||||
"message": "What is your target CI provider?",
|
||||
"type": "list",
|
||||
"items": [
|
||||
{
|
||||
"value": "github",
|
||||
"label": "GitHub Actions"
|
||||
},
|
||||
{
|
||||
"value": "circleci",
|
||||
"label": "Circle CI"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Workflow name.",
|
||||
"$default": {
|
||||
"$source": "argv",
|
||||
"index": 0
|
||||
},
|
||||
"default": "CI",
|
||||
"x-prompt": "How should we name your workflow?",
|
||||
"pattern": "^[a-zA-Z].*$"
|
||||
}
|
||||
},
|
||||
"required": ["ci", "name"]
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { readNxJson, Tree, updateNxJson } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
|
||||
import { initGenerator } from './init';
|
||||
|
||||
describe('@nx/dotnet:init', () => {
|
||||
let tree: Tree;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
tree.write('MyApp/MyApp.csproj', '<Project Sdk="Microsoft.NET.Sdk" />');
|
||||
});
|
||||
|
||||
describe('plugin', () => {
|
||||
it('should add the plugin', async () => {
|
||||
await initGenerator(tree, {
|
||||
skipFormat: true,
|
||||
skipPackageJson: false,
|
||||
});
|
||||
const nxJson = readNxJson(tree);
|
||||
expect(nxJson?.plugins).toMatchInlineSnapshot(`
|
||||
[
|
||||
"@nx/dotnet",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not overwrite existing plugins', async () => {
|
||||
updateNxJson(tree, {
|
||||
plugins: ['foo'],
|
||||
});
|
||||
await initGenerator(tree, {
|
||||
skipFormat: true,
|
||||
skipPackageJson: false,
|
||||
});
|
||||
const nxJson = readNxJson(tree);
|
||||
expect(nxJson?.plugins).toMatchInlineSnapshot(`
|
||||
[
|
||||
"foo",
|
||||
"@nx/dotnet",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not add plugin if already in array', async () => {
|
||||
updateNxJson(tree, {
|
||||
plugins: ['@nx/dotnet'],
|
||||
});
|
||||
await initGenerator(tree, {
|
||||
skipFormat: true,
|
||||
skipPackageJson: false,
|
||||
});
|
||||
const nxJson = readNxJson(tree);
|
||||
expect(nxJson?.plugins).toMatchInlineSnapshot(`
|
||||
[
|
||||
"@nx/dotnet",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('namedInputs', () => {
|
||||
it('should add the namedInputs', async () => {
|
||||
await initGenerator(tree, {
|
||||
skipFormat: true,
|
||||
skipPackageJson: false,
|
||||
});
|
||||
const nxJson = readNxJson(tree);
|
||||
expect(nxJson?.namedInputs).toMatchInlineSnapshot(`
|
||||
{
|
||||
"default": [
|
||||
"{projectRoot}/**/*",
|
||||
],
|
||||
"production": [
|
||||
"default",
|
||||
"!{projectRoot}/**/*.Tests/**/*",
|
||||
"!{projectRoot}/**/bin/**/*",
|
||||
"!{projectRoot}/**/obj/**/*",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not overwrite existing namedInputs', async () => {
|
||||
updateNxJson(tree, {
|
||||
namedInputs: {
|
||||
default: ['foo'],
|
||||
production: ['bar', '!{projectRoot}/cypress/**/*'],
|
||||
},
|
||||
});
|
||||
await initGenerator(tree, {
|
||||
skipFormat: true,
|
||||
skipPackageJson: false,
|
||||
});
|
||||
const nxJson = readNxJson(tree);
|
||||
expect(nxJson?.namedInputs).toMatchInlineSnapshot(`
|
||||
{
|
||||
"default": [
|
||||
"foo",
|
||||
"{projectRoot}/**/*",
|
||||
],
|
||||
"production": [
|
||||
"bar",
|
||||
"!{projectRoot}/cypress/**/*",
|
||||
"default",
|
||||
"!{projectRoot}/**/*.Tests/**/*",
|
||||
"!{projectRoot}/**/bin/**/*",
|
||||
"!{projectRoot}/**/obj/**/*",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
formatFiles,
|
||||
GeneratorCallback,
|
||||
logger,
|
||||
readNxJson,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateNxJson,
|
||||
visitNotIgnoredFiles,
|
||||
} from '@nx/devkit';
|
||||
import ignore = require('ignore');
|
||||
import { nxVersion } from '../../utils/versions';
|
||||
import { InitGeneratorSchema } from './schema';
|
||||
import { hasDotNetPlugin } from '../../utils/has-dotnet-plugin';
|
||||
|
||||
export async function initGenerator(tree: Tree, options: InitGeneratorSchema) {
|
||||
const tasks: GeneratorCallback[] = [];
|
||||
|
||||
if (!options.skipPackageJson && tree.exists('package.json')) {
|
||||
tasks.push(
|
||||
addDependenciesToPackageJson(
|
||||
tree,
|
||||
{},
|
||||
{
|
||||
'@nx/dotnet': nxVersion,
|
||||
},
|
||||
undefined,
|
||||
options.keepExistingVersions
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
addPlugin(tree);
|
||||
updateNxJsonConfiguration(tree);
|
||||
updateGitIgnore(tree);
|
||||
|
||||
if (!options.skipFormat) {
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
return runTasksInSerial(...tasks);
|
||||
}
|
||||
|
||||
export function updateGitIgnore(tree: Tree) {
|
||||
const gitignorePath = '.gitignore';
|
||||
if (tree.exists(gitignorePath)) {
|
||||
let gitignore = tree.read(gitignorePath, 'utf-8');
|
||||
const sectionHeader = '# .NET';
|
||||
const potentialLinesToAdd = new Set(['**/bin/', '**/obj/', '/artifacts/']);
|
||||
|
||||
if (gitignore.includes(sectionHeader)) {
|
||||
// Section already exists, do not modify
|
||||
return;
|
||||
}
|
||||
|
||||
// Line with issues -> files that would have been ignored.
|
||||
const issues = new Map<string, string[]>();
|
||||
|
||||
visitNotIgnoredFiles(tree, '.', (filePath) => {
|
||||
for (const line of potentialLinesToAdd) {
|
||||
const ig = ignore();
|
||||
ig.add(line);
|
||||
if (ig.ignores(filePath)) {
|
||||
if (!issues.has(line)) {
|
||||
issues.set(line, []);
|
||||
}
|
||||
issues.get(line)!.push(filePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
let hasIssues = issues.size > 0;
|
||||
if (hasIssues) {
|
||||
logger.warn(
|
||||
`The following .gitignore entries cannot be added because they would ignore existing files:`
|
||||
);
|
||||
}
|
||||
for (const [line, files] of issues) {
|
||||
potentialLinesToAdd.delete(line);
|
||||
logger.warn(
|
||||
`- "${line}" would ignore the following existing files:\n` +
|
||||
files.map((f) => ` - ${f}`).join('\n')
|
||||
);
|
||||
}
|
||||
if (hasIssues) {
|
||||
logger.warn(
|
||||
`Review the above patterns and manually update your .gitignore as necessary.`
|
||||
);
|
||||
}
|
||||
if (potentialLinesToAdd.size > 0) {
|
||||
gitignore += `\n${sectionHeader}\n`;
|
||||
for (const line of potentialLinesToAdd) {
|
||||
gitignore += `${line}\n`;
|
||||
}
|
||||
tree.write(gitignorePath, gitignore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addPlugin(tree: Tree) {
|
||||
const nxJson = readNxJson(tree);
|
||||
|
||||
if (!hasDotNetPlugin(tree)) {
|
||||
nxJson.plugins ??= [];
|
||||
nxJson.plugins.push('@nx/dotnet');
|
||||
updateNxJson(tree, nxJson);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateNxJsonConfiguration(tree: Tree) {
|
||||
const nxJson = readNxJson(tree);
|
||||
|
||||
if (!nxJson.namedInputs) {
|
||||
nxJson.namedInputs = {};
|
||||
}
|
||||
|
||||
// Default inputs include all project files
|
||||
const defaultFilesSet = nxJson.namedInputs.default ?? [];
|
||||
nxJson.namedInputs.default = Array.from(
|
||||
new Set([...defaultFilesSet, '{projectRoot}/**/*'])
|
||||
);
|
||||
|
||||
// Production inputs exclude test files and build outputs
|
||||
const productionFileSet = nxJson.namedInputs.production ?? [];
|
||||
nxJson.namedInputs.production = Array.from(
|
||||
new Set([
|
||||
...productionFileSet,
|
||||
'default',
|
||||
'!{projectRoot}/**/*.Tests/**/*',
|
||||
'!{projectRoot}/**/bin/**/*',
|
||||
'!{projectRoot}/**/obj/**/*',
|
||||
])
|
||||
);
|
||||
|
||||
updateNxJson(tree, nxJson);
|
||||
}
|
||||
|
||||
export default initGenerator;
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface InitGeneratorSchema {
|
||||
skipFormat?: boolean;
|
||||
skipPackageJson?: boolean;
|
||||
keepExistingVersions?: boolean;
|
||||
updatePackageScripts?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"$id": "NxDotNetInitSchema",
|
||||
"title": ".NET Init Generator",
|
||||
"description": "Initializes a .NET project in the current workspace.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"skipFormat": {
|
||||
"description": "Skip formatting files.",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"skipPackageJson": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Do not add dependencies to `package.json`.",
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"keepExistingVersions": {
|
||||
"type": "boolean",
|
||||
"x-priority": "internal",
|
||||
"description": "Keep existing dependencies versions",
|
||||
"default": false
|
||||
},
|
||||
"updatePackageScripts": {
|
||||
"type": "boolean",
|
||||
"x-priority": "internal",
|
||||
"description": "Update `package.json` scripts with inferred targets",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
createNodes,
|
||||
createNodesV2,
|
||||
createDependencies,
|
||||
} from './plugins/plugin';
|
||||
|
||||
export { DotNetPluginOptions } from './plugins/create-nodes';
|
||||
@@ -0,0 +1,66 @@
|
||||
import { readNxJson, Tree } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import update from './update-plugin-path';
|
||||
|
||||
describe('update-plugin-path migration', () => {
|
||||
let tree: Tree;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
it('should do nothing when there are no plugins', () => {
|
||||
tree.write('nx.json', JSON.stringify({ namedInputs: {} }));
|
||||
update(tree);
|
||||
expect(readNxJson(tree)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"namedInputs": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should leave the bare @nx/dotnet plugin untouched', () => {
|
||||
tree.write('nx.json', JSON.stringify({ plugins: ['@nx/dotnet'] }));
|
||||
update(tree);
|
||||
expect(readNxJson(tree)?.plugins).toEqual(['@nx/dotnet']);
|
||||
});
|
||||
|
||||
it('should rewrite the string @nx/dotnet/plugin entry to @nx/dotnet', () => {
|
||||
tree.write('nx.json', JSON.stringify({ plugins: ['@nx/dotnet/plugin'] }));
|
||||
update(tree);
|
||||
expect(readNxJson(tree)?.plugins).toEqual(['@nx/dotnet']);
|
||||
});
|
||||
|
||||
it('should rewrite the object @nx/dotnet/plugin entry while preserving options', () => {
|
||||
tree.write(
|
||||
'nx.json',
|
||||
JSON.stringify({
|
||||
plugins: [
|
||||
{
|
||||
plugin: '@nx/dotnet/plugin',
|
||||
options: { build: { targetName: 'compile' } },
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
update(tree);
|
||||
expect(readNxJson(tree)?.plugins).toEqual([
|
||||
{ plugin: '@nx/dotnet', options: { build: { targetName: 'compile' } } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should update the entry in place, preserving the position of other plugins', () => {
|
||||
tree.write(
|
||||
'nx.json',
|
||||
JSON.stringify({
|
||||
plugins: ['@nx/js', '@nx/dotnet/plugin', { plugin: '@nx/eslint' }],
|
||||
})
|
||||
);
|
||||
update(tree);
|
||||
expect(readNxJson(tree)?.plugins).toEqual([
|
||||
'@nx/js',
|
||||
'@nx/dotnet',
|
||||
{ plugin: '@nx/eslint' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { readNxJson, Tree, updateNxJson } from '@nx/devkit';
|
||||
|
||||
const OLD_PATH = '@nx/dotnet/plugin';
|
||||
const NEW_PATH = '@nx/dotnet';
|
||||
|
||||
/**
|
||||
* The `@nx/dotnet/plugin` subpath export has been removed in favor of the bare
|
||||
* `@nx/dotnet` specifier. This migration rewrites any `nx.json` plugin entries
|
||||
* that still register the plugin via the old `@nx/dotnet/plugin` path, updating
|
||||
* each entry in place so the order of the `plugins` array is preserved.
|
||||
*/
|
||||
export default function update(tree: Tree) {
|
||||
const nxJson = readNxJson(tree);
|
||||
if (!nxJson?.plugins?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let updated = false;
|
||||
for (let i = 0; i < nxJson.plugins.length; i++) {
|
||||
const plugin = nxJson.plugins[i];
|
||||
if (typeof plugin === 'string') {
|
||||
if (plugin === OLD_PATH) {
|
||||
nxJson.plugins[i] = NEW_PATH;
|
||||
updated = true;
|
||||
}
|
||||
} else if (plugin.plugin === OLD_PATH) {
|
||||
plugin.plugin = NEW_PATH;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
updateNxJson(tree, nxJson);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
CreateDependencies,
|
||||
DependencyType,
|
||||
RawProjectGraphDependency,
|
||||
} from '@nx/devkit';
|
||||
|
||||
import { DotNetPluginOptions } from './create-nodes';
|
||||
import {
|
||||
readCachedAnalysisResult,
|
||||
isAnalysisErrorResult,
|
||||
} from '../analyzer/analyzer-client';
|
||||
import { createProjectRootMappingsFromProjectConfigurations } from '@nx/devkit/internal';
|
||||
|
||||
export const createDependencies: CreateDependencies<
|
||||
DotNetPluginOptions
|
||||
> = async (_, ctx) => {
|
||||
const dependencies: RawProjectGraphDependency[] = [];
|
||||
const rootMap = createProjectRootMappingsFromProjectConfigurations(
|
||||
ctx.projects
|
||||
);
|
||||
|
||||
// Read the cached analysis result populated by createNodes
|
||||
// createNodes always runs before createDependencies, so the cache should be populated
|
||||
const cachedResult = readCachedAnalysisResult();
|
||||
|
||||
if (isAnalysisErrorResult(cachedResult)) {
|
||||
throw new Error(
|
||||
'There was an error analyzing .NET projects. See earlier logs.'
|
||||
);
|
||||
}
|
||||
|
||||
const { referencesByRoot } = cachedResult;
|
||||
|
||||
// Map references to dependencies
|
||||
// The analyzer returns: { [projectRoot]: [referencedProjectRoot1, referencedProjectRoot2, ...] }
|
||||
// We need to convert this to Nx dependencies
|
||||
for (const [sourceRoot, referencedRoots] of Object.entries(
|
||||
referencesByRoot
|
||||
)) {
|
||||
const sourceName = rootMap.get(sourceRoot);
|
||||
if (!sourceName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const targetRoot of referencedRoots.refs) {
|
||||
const targetName = rootMap.get(targetRoot);
|
||||
if (targetName) {
|
||||
dependencies.push({
|
||||
source: sourceName,
|
||||
target: targetName,
|
||||
type: DependencyType.static,
|
||||
sourceFile: referencedRoots.sourceConfigFile,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
};
|
||||
@@ -0,0 +1,621 @@
|
||||
import { ProjectConfiguration, TargetConfiguration } from '@nx/devkit';
|
||||
import {
|
||||
DotNetPluginOptions,
|
||||
TargetConfigurationWithName,
|
||||
} from './create-nodes';
|
||||
|
||||
// Import the internal function for testing
|
||||
// We'll need to export it or test it indirectly through createNodesV2
|
||||
// For now, let's create a mock version to test the logic
|
||||
|
||||
/**
|
||||
* Merge user-specified target configurations with the generated targets from the analyzer
|
||||
* This is a copy of the function from create-nodes.ts for testing purposes
|
||||
*/
|
||||
function mergeUserTargetConfigurations(
|
||||
node: ProjectConfiguration,
|
||||
options: DotNetPluginOptions
|
||||
): ProjectConfiguration {
|
||||
if (!node.targets || !options) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// Import mergeTargetConfigurations from nx
|
||||
const {
|
||||
mergeTargetConfigurations,
|
||||
} = require('nx/src/project-graph/utils/project-configuration-utils');
|
||||
|
||||
const targetMappings: Array<{
|
||||
targetOption: TargetConfigurationWithName | false | undefined;
|
||||
defaultTargetName: string;
|
||||
}> = [
|
||||
{ targetOption: options.build, defaultTargetName: 'build' },
|
||||
{ targetOption: options.test, defaultTargetName: 'test' },
|
||||
{ targetOption: options.clean, defaultTargetName: 'clean' },
|
||||
{ targetOption: options.restore, defaultTargetName: 'restore' },
|
||||
{ targetOption: options.publish, defaultTargetName: 'publish' },
|
||||
{ targetOption: options.pack, defaultTargetName: 'pack' },
|
||||
{ targetOption: options.watch, defaultTargetName: 'watch' },
|
||||
{ targetOption: options.run, defaultTargetName: 'run' },
|
||||
];
|
||||
|
||||
const mergedTargets = { ...node.targets };
|
||||
|
||||
for (const { targetOption, defaultTargetName } of targetMappings) {
|
||||
// Disabled target from user configuration
|
||||
if (targetOption === false) {
|
||||
delete mergedTargets[defaultTargetName];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use empty object as default when option is not provided
|
||||
const { targetName, ...userSpecifiedConfig } = targetOption ?? {};
|
||||
const actualTargetName = targetName ?? defaultTargetName;
|
||||
|
||||
// Find the generated target - it might be under the default name or the user-specified name
|
||||
const generatedTarget =
|
||||
mergedTargets[actualTargetName] ?? mergedTargets[defaultTargetName];
|
||||
|
||||
if (!generatedTarget) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasUserConfig = Object.keys(userSpecifiedConfig).length > 0;
|
||||
const isRenamed = actualTargetName !== defaultTargetName;
|
||||
|
||||
// Merge user config with generated target if user config is provided
|
||||
if (hasUserConfig) {
|
||||
mergedTargets[actualTargetName] = mergeTargetConfigurations(
|
||||
userSpecifiedConfig as TargetConfiguration,
|
||||
generatedTarget
|
||||
);
|
||||
} else if (isRenamed) {
|
||||
// If only renaming (no config to merge), just copy the target to the new name
|
||||
mergedTargets[actualTargetName] = { ...generatedTarget };
|
||||
}
|
||||
|
||||
// If target was renamed, remove the old target name
|
||||
if (isRenamed && mergedTargets[defaultTargetName]) {
|
||||
delete mergedTargets[defaultTargetName];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
targets: mergedTargets,
|
||||
};
|
||||
}
|
||||
|
||||
describe('@nx/dotnet - createNodes', () => {
|
||||
describe('mergeUserTargetConfigurations', () => {
|
||||
it('should return node unchanged when no options provided', () => {
|
||||
const node: ProjectConfiguration = {
|
||||
root: 'apps/my-app',
|
||||
name: 'my-app',
|
||||
targets: {
|
||||
build: {
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet build',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = mergeUserTargetConfigurations(node, {});
|
||||
expect(result).toEqual(node);
|
||||
});
|
||||
|
||||
it('should return node unchanged when no targets exist', () => {
|
||||
const node: ProjectConfiguration = {
|
||||
root: 'apps/my-app',
|
||||
name: 'my-app',
|
||||
};
|
||||
|
||||
const result = mergeUserTargetConfigurations(node, {
|
||||
build: { targetName: 'compile' },
|
||||
});
|
||||
expect(result).toEqual(node);
|
||||
});
|
||||
|
||||
it('should merge user options into generated target', () => {
|
||||
const node: ProjectConfiguration = {
|
||||
root: 'apps/my-app',
|
||||
name: 'my-app',
|
||||
targets: {
|
||||
build: {
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet build',
|
||||
cwd: 'apps/my-app',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const options: DotNetPluginOptions = {
|
||||
build: {
|
||||
options: {
|
||||
additionalOption: 'value',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = mergeUserTargetConfigurations(node, options);
|
||||
|
||||
expect(result.targets?.build).toEqual({
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet build',
|
||||
cwd: 'apps/my-app',
|
||||
additionalOption: 'value',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge user configurations into generated target', () => {
|
||||
const node: ProjectConfiguration = {
|
||||
root: 'apps/my-app',
|
||||
name: 'my-app',
|
||||
targets: {
|
||||
build: {
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet build',
|
||||
},
|
||||
configurations: {
|
||||
debug: {
|
||||
configuration: 'Debug',
|
||||
},
|
||||
release: {
|
||||
configuration: 'Release',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const options: DotNetPluginOptions = {
|
||||
build: {
|
||||
configurations: {
|
||||
production: {
|
||||
optimization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = mergeUserTargetConfigurations(node, options);
|
||||
|
||||
expect(result.targets?.build?.configurations).toEqual({
|
||||
debug: {
|
||||
configuration: 'Debug',
|
||||
},
|
||||
release: {
|
||||
configuration: 'Release',
|
||||
},
|
||||
production: {
|
||||
optimization: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should rename target when targetName is specified', () => {
|
||||
const node: ProjectConfiguration = {
|
||||
root: 'apps/my-app',
|
||||
name: 'my-app',
|
||||
targets: {
|
||||
build: {
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet build',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const options: DotNetPluginOptions = {
|
||||
build: {
|
||||
targetName: 'compile',
|
||||
options: {
|
||||
additionalOption: 'value',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = mergeUserTargetConfigurations(node, options);
|
||||
|
||||
expect(result.targets?.compile).toBeDefined();
|
||||
expect(result.targets?.build).toBeUndefined();
|
||||
expect(result.targets?.compile?.options).toEqual({
|
||||
command: 'dotnet build',
|
||||
additionalOption: 'value',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple target configurations', () => {
|
||||
const node: ProjectConfiguration = {
|
||||
root: 'apps/my-app',
|
||||
name: 'my-app',
|
||||
targets: {
|
||||
build: {
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet build',
|
||||
},
|
||||
},
|
||||
test: {
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet test',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const options: DotNetPluginOptions = {
|
||||
build: {
|
||||
targetName: 'compile',
|
||||
},
|
||||
test: {
|
||||
targetName: 'unit-test',
|
||||
options: {
|
||||
logger: 'console',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = mergeUserTargetConfigurations(node, options);
|
||||
|
||||
expect(result.targets?.compile).toBeDefined();
|
||||
expect(result.targets?.build).toBeUndefined();
|
||||
expect(result.targets?.['unit-test']).toBeDefined();
|
||||
expect(result.targets?.test).toBeUndefined();
|
||||
expect(result.targets?.['unit-test']?.options).toEqual({
|
||||
command: 'dotnet test',
|
||||
logger: 'console',
|
||||
});
|
||||
});
|
||||
|
||||
it('should add dependsOn to target', () => {
|
||||
const node: ProjectConfiguration = {
|
||||
root: 'apps/my-app',
|
||||
name: 'my-app',
|
||||
targets: {
|
||||
test: {
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet test',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const options: DotNetPluginOptions = {
|
||||
test: {
|
||||
dependsOn: ['build'],
|
||||
},
|
||||
};
|
||||
|
||||
const result = mergeUserTargetConfigurations(node, options);
|
||||
|
||||
expect(result.targets?.test?.dependsOn).toEqual(['build']);
|
||||
});
|
||||
|
||||
it('should handle cache configuration', () => {
|
||||
const node: ProjectConfiguration = {
|
||||
root: 'apps/my-app',
|
||||
name: 'my-app',
|
||||
targets: {
|
||||
build: {
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet build',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const options: DotNetPluginOptions = {
|
||||
build: {
|
||||
cache: true,
|
||||
inputs: ['{projectRoot}/**/*.cs', '^production'],
|
||||
outputs: ['{projectRoot}/bin'],
|
||||
},
|
||||
};
|
||||
|
||||
const result = mergeUserTargetConfigurations(node, options);
|
||||
|
||||
expect(result.targets?.build?.cache).toBe(true);
|
||||
expect(result.targets?.build?.inputs).toEqual([
|
||||
'{projectRoot}/**/*.cs',
|
||||
'^production',
|
||||
]);
|
||||
expect(result.targets?.build?.outputs).toEqual(['{projectRoot}/bin']);
|
||||
});
|
||||
|
||||
it('should rename target even when no other config is provided', () => {
|
||||
const node: ProjectConfiguration = {
|
||||
root: 'apps/my-app',
|
||||
name: 'my-app',
|
||||
targets: {
|
||||
build: {
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet build',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const options: DotNetPluginOptions = {
|
||||
build: {
|
||||
targetName: 'compile',
|
||||
// No other config, but should still rename
|
||||
},
|
||||
};
|
||||
|
||||
const result = mergeUserTargetConfigurations(node, options);
|
||||
|
||||
// Target should be renamed even with no other config
|
||||
expect(result.targets?.compile).toBeDefined();
|
||||
expect(result.targets?.build).toBeUndefined();
|
||||
expect(result.targets?.compile).toEqual({
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet build',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle all target types', () => {
|
||||
const node: ProjectConfiguration = {
|
||||
root: 'apps/my-app',
|
||||
name: 'my-app',
|
||||
targets: {
|
||||
build: {
|
||||
executor: 'nx:run-commands',
|
||||
options: { command: 'dotnet build' },
|
||||
},
|
||||
test: {
|
||||
executor: 'nx:run-commands',
|
||||
options: { command: 'dotnet test' },
|
||||
},
|
||||
clean: {
|
||||
executor: 'nx:run-commands',
|
||||
options: { command: 'dotnet clean' },
|
||||
},
|
||||
restore: {
|
||||
executor: 'nx:run-commands',
|
||||
options: { command: 'dotnet restore' },
|
||||
},
|
||||
publish: {
|
||||
executor: 'nx:run-commands',
|
||||
options: { command: 'dotnet publish' },
|
||||
},
|
||||
pack: {
|
||||
executor: 'nx:run-commands',
|
||||
options: { command: 'dotnet pack' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const options: DotNetPluginOptions = {
|
||||
build: { options: { b: 1 } },
|
||||
test: { options: { t: 2 } },
|
||||
clean: { options: { c: 3 } },
|
||||
restore: { options: { r: 4 } },
|
||||
publish: { options: { p: 5 } },
|
||||
pack: { options: { pk: 6 } },
|
||||
};
|
||||
|
||||
const result = mergeUserTargetConfigurations(node, options);
|
||||
|
||||
expect(result.targets?.build?.options).toEqual({
|
||||
command: 'dotnet build',
|
||||
b: 1,
|
||||
});
|
||||
expect(result.targets?.test?.options).toEqual({
|
||||
command: 'dotnet test',
|
||||
t: 2,
|
||||
});
|
||||
expect(result.targets?.clean?.options).toEqual({
|
||||
command: 'dotnet clean',
|
||||
c: 3,
|
||||
});
|
||||
expect(result.targets?.restore?.options).toEqual({
|
||||
command: 'dotnet restore',
|
||||
r: 4,
|
||||
});
|
||||
expect(result.targets?.publish?.options).toEqual({
|
||||
command: 'dotnet publish',
|
||||
p: 5,
|
||||
});
|
||||
expect(result.targets?.pack?.options).toEqual({
|
||||
command: 'dotnet pack',
|
||||
pk: 6,
|
||||
});
|
||||
});
|
||||
|
||||
it('should override existing options when user provides same option', () => {
|
||||
const node: ProjectConfiguration = {
|
||||
root: 'apps/my-app',
|
||||
name: 'my-app',
|
||||
targets: {
|
||||
build: {
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet build',
|
||||
cwd: 'apps/my-app',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const options: DotNetPluginOptions = {
|
||||
build: {
|
||||
options: {
|
||||
cwd: 'different/path',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = mergeUserTargetConfigurations(node, options);
|
||||
|
||||
expect(result.targets?.build?.options?.cwd).toBe('different/path');
|
||||
});
|
||||
|
||||
it('should handle complex nested configurations', () => {
|
||||
const node: ProjectConfiguration = {
|
||||
root: 'libs/my-lib',
|
||||
name: 'my-lib',
|
||||
targets: {
|
||||
build: {
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet build',
|
||||
},
|
||||
configurations: {
|
||||
debug: {
|
||||
configuration: 'Debug',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const options: DotNetPluginOptions = {
|
||||
build: {
|
||||
targetName: 'compile',
|
||||
options: {
|
||||
verbose: true,
|
||||
},
|
||||
configurations: {
|
||||
release: {
|
||||
configuration: 'Release',
|
||||
optimization: true,
|
||||
},
|
||||
},
|
||||
cache: true,
|
||||
inputs: ['default', '^production'],
|
||||
outputs: ['{projectRoot}/bin/{configuration}'],
|
||||
dependsOn: ['^build'],
|
||||
},
|
||||
};
|
||||
|
||||
const result = mergeUserTargetConfigurations(node, options);
|
||||
|
||||
expect(result.targets?.compile).toBeDefined();
|
||||
expect(result.targets?.build).toBeUndefined();
|
||||
expect(result.targets?.compile).toMatchObject({
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet build',
|
||||
verbose: true,
|
||||
},
|
||||
configurations: {
|
||||
debug: {
|
||||
configuration: 'Debug',
|
||||
},
|
||||
release: {
|
||||
configuration: 'Release',
|
||||
optimization: true,
|
||||
},
|
||||
},
|
||||
cache: true,
|
||||
inputs: ['default', '^production'],
|
||||
outputs: ['{projectRoot}/bin/{configuration}'],
|
||||
dependsOn: ['^build'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove disabled targets from configuration', () => {
|
||||
const node: ProjectConfiguration = {
|
||||
root: 'apps/my-app',
|
||||
name: 'my-app',
|
||||
targets: {
|
||||
build: {
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet build',
|
||||
},
|
||||
},
|
||||
test: {
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet test',
|
||||
},
|
||||
},
|
||||
clean: {
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
command: 'dotnet clean',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const options: DotNetPluginOptions = {
|
||||
test: false,
|
||||
clean: false,
|
||||
};
|
||||
|
||||
const result = mergeUserTargetConfigurations(node, options);
|
||||
|
||||
expect(result.targets?.build).toBeDefined();
|
||||
expect(result.targets?.test).toBeUndefined();
|
||||
expect(result.targets?.clean).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('target name extraction', () => {
|
||||
it('should extract default target names when not specified', () => {
|
||||
const options: DotNetPluginOptions = {
|
||||
build: {},
|
||||
test: {},
|
||||
};
|
||||
|
||||
const buildTargetName =
|
||||
(options.build && options.build.targetName) || 'build';
|
||||
const testTargetName =
|
||||
(options.test && options.test.targetName) || 'test';
|
||||
|
||||
expect(buildTargetName).toBe('build');
|
||||
expect(testTargetName).toBe('test');
|
||||
});
|
||||
|
||||
it('should extract custom target names when specified', () => {
|
||||
const options: DotNetPluginOptions = {
|
||||
build: { targetName: 'compile' },
|
||||
test: { targetName: 'unit-test' },
|
||||
};
|
||||
|
||||
const buildTargetName =
|
||||
(options.build && options.build.targetName) || 'build';
|
||||
const testTargetName =
|
||||
(options.test && options.test.targetName) || 'test';
|
||||
|
||||
expect(buildTargetName).toBe('compile');
|
||||
expect(testTargetName).toBe('unit-test');
|
||||
});
|
||||
|
||||
it('should handle mixed default and custom names', () => {
|
||||
const options: DotNetPluginOptions = {
|
||||
build: { targetName: 'compile' },
|
||||
test: {},
|
||||
clean: { targetName: 'cleanup' },
|
||||
};
|
||||
|
||||
const buildTargetName =
|
||||
(options.build && options.build.targetName) || 'build';
|
||||
const testTargetName =
|
||||
(options.test && options.test.targetName) || 'test';
|
||||
const cleanTargetName =
|
||||
(options.clean && options.clean.targetName) || 'clean';
|
||||
|
||||
expect(buildTargetName).toBe('compile');
|
||||
expect(testTargetName).toBe('test');
|
||||
expect(cleanTargetName).toBe('cleanup');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,260 @@
|
||||
import {
|
||||
CreateNodes,
|
||||
logger,
|
||||
ProjectConfiguration,
|
||||
TargetConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import {
|
||||
analyzeProjects,
|
||||
isAnalysisErrorResult,
|
||||
} from '../analyzer/analyzer-client';
|
||||
import { mergeTargetConfigurations } from 'nx/src/project-graph/utils/project-configuration-utils';
|
||||
|
||||
export type TargetConfigurationWithName = Partial<TargetConfiguration> & {
|
||||
/**
|
||||
* The name of the target. Defaults to the target type (e.g., 'build', 'test', etc.)
|
||||
*/
|
||||
targetName?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration options for the @nx/dotnet plugin.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In nx.json:
|
||||
* {
|
||||
* "plugins": [
|
||||
* {
|
||||
* "plugin": "@nx/dotnet",
|
||||
* "options": {
|
||||
* "build": {
|
||||
* "targetName": "compile",
|
||||
* "options": {
|
||||
* "additionalOption": "value"
|
||||
* },
|
||||
* "configurations": {
|
||||
* "production": {
|
||||
* "optimization": true
|
||||
* }
|
||||
* }
|
||||
* },
|
||||
* "test": {
|
||||
* "targetName": "unit-test",
|
||||
* "dependsOn": ["build"]
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface DotNetPluginOptions {
|
||||
/**
|
||||
* Configuration for the build target.
|
||||
* Use `targetName` to rename the target, and provide additional options/configurations to merge with the generated target.
|
||||
*/
|
||||
build?: TargetConfigurationWithName | false;
|
||||
/**
|
||||
* Configuration for the test target.
|
||||
* Use `targetName` to rename the target, and provide additional options/configurations to merge with the generated target.
|
||||
*/
|
||||
test?: TargetConfigurationWithName | false;
|
||||
/**
|
||||
* Configuration for the clean target.
|
||||
* Use `targetName` to rename the target, and provide additional options/configurations to merge with the generated target.
|
||||
*/
|
||||
clean?: TargetConfigurationWithName | false;
|
||||
/**
|
||||
* Configuration for the restore target.
|
||||
* Use `targetName` to rename the target, and provide additional options/configurations to merge with the generated target.
|
||||
*/
|
||||
restore?: TargetConfigurationWithName | false;
|
||||
/**
|
||||
* Configuration for the publish target.
|
||||
* Use `targetName` to rename the target, and provide additional options/configurations to merge with the generated target.
|
||||
*/
|
||||
publish?: TargetConfigurationWithName | false;
|
||||
/**
|
||||
* Configuration for the pack target.
|
||||
* Use `targetName` to rename the target, and provide additional options/configurations to merge with the generated target.
|
||||
*/
|
||||
pack?: TargetConfigurationWithName | false;
|
||||
/**
|
||||
* Configuration for the watch target.
|
||||
* Use `targetName` to rename the target, and provide additional options/configurations to merge with the generated target.
|
||||
*/
|
||||
watch?: TargetConfigurationWithName | false;
|
||||
/**
|
||||
* Configuration for the run target.
|
||||
* Use `targetName` to rename the target, and provide additional options/configurations to merge with the generated target.
|
||||
*/
|
||||
run?: TargetConfigurationWithName | false;
|
||||
}
|
||||
|
||||
// MSBuild auto-imports Directory.Build.props/.targets from each ancestor of a project file,
|
||||
// reads Directory.Build.rsp from ancestors during CLI builds, applies Directory.Solution.*
|
||||
// when building a .sln, and reads Directory.Packages.props from the nearest ancestor when
|
||||
// Central Package Management is in use. Matching them here causes createNodesV2 to re-run
|
||||
// (and the analyzer's cache to invalidate) when any of them change, and gives us the file
|
||||
// list to hand to the analyzer so it can declare per-project ancestor inputs.
|
||||
// The analyzer partitions matched paths into project vs directory files by filename, so we
|
||||
// don't have to repeat that classification on this side.
|
||||
const dotnetProjectGlob =
|
||||
'**/{*.{csproj,fsproj,vbproj},Directory.Build.{props,targets,rsp},Directory.Solution.{props,targets},Directory.Packages.props}';
|
||||
|
||||
/**
|
||||
* Merge user-specified target configurations with the generated targets from the analyzer
|
||||
*/
|
||||
function mergeUserTargetConfigurations(
|
||||
node: ProjectConfiguration,
|
||||
options: DotNetPluginOptions
|
||||
): ProjectConfiguration {
|
||||
if (!node.targets || !options) {
|
||||
return node;
|
||||
}
|
||||
|
||||
const targetMappings: Array<{
|
||||
targetOption: TargetConfigurationWithName | false | undefined;
|
||||
defaultTargetName: string;
|
||||
}> = [
|
||||
{ targetOption: options.build, defaultTargetName: 'build' },
|
||||
{ targetOption: options.test, defaultTargetName: 'test' },
|
||||
{ targetOption: options.clean, defaultTargetName: 'clean' },
|
||||
{ targetOption: options.restore, defaultTargetName: 'restore' },
|
||||
{ targetOption: options.publish, defaultTargetName: 'publish' },
|
||||
{ targetOption: options.pack, defaultTargetName: 'pack' },
|
||||
{ targetOption: options.watch, defaultTargetName: 'watch' },
|
||||
{ targetOption: options.run, defaultTargetName: 'run' },
|
||||
];
|
||||
|
||||
const mergedTargets = { ...node.targets };
|
||||
|
||||
for (const { targetOption, defaultTargetName } of targetMappings) {
|
||||
// Disabled target from user configuration
|
||||
if (targetOption === false) {
|
||||
delete mergedTargets[defaultTargetName];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use empty object as default when option is not provided
|
||||
const { targetName, ...userSpecifiedConfig } = targetOption ?? {};
|
||||
const actualTargetName = targetName ?? defaultTargetName;
|
||||
|
||||
// Find the generated target - it might be under the default name or the user-specified name
|
||||
const generatedTarget =
|
||||
mergedTargets[actualTargetName] ?? mergedTargets[defaultTargetName];
|
||||
|
||||
if (!generatedTarget) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasUserConfig = Object.keys(userSpecifiedConfig).length > 0;
|
||||
const isRenamed = actualTargetName !== defaultTargetName;
|
||||
|
||||
// Merge user config with generated target if user config is provided
|
||||
if (hasUserConfig) {
|
||||
mergedTargets[actualTargetName] = mergeTargetConfigurations(
|
||||
userSpecifiedConfig as TargetConfiguration,
|
||||
generatedTarget
|
||||
);
|
||||
} else if (isRenamed) {
|
||||
// If only renaming (no config to merge), just copy the target to the new name
|
||||
mergedTargets[actualTargetName] = { ...generatedTarget };
|
||||
}
|
||||
|
||||
// If target was renamed, remove the old target name
|
||||
if (isRenamed && mergedTargets[defaultTargetName]) {
|
||||
delete mergedTargets[defaultTargetName];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
targets: mergedTargets,
|
||||
};
|
||||
}
|
||||
|
||||
export const createNodes: CreateNodes<DotNetPluginOptions> = [
|
||||
dotnetProjectGlob,
|
||||
async (configFilePaths, options, context) => {
|
||||
// Analyze all projects - the C# analyzer builds the complete Nx structure
|
||||
try {
|
||||
// Normalize options to handle undefined (when plugin is registered as string)
|
||||
const normalizedOptions = options ?? {};
|
||||
|
||||
// Extract target names from new format and create options for analyzer
|
||||
const analyzerOptions = {
|
||||
buildTargetName:
|
||||
(normalizedOptions.build && normalizedOptions.build.targetName) ||
|
||||
'build',
|
||||
testTargetName:
|
||||
(normalizedOptions.test && normalizedOptions.test.targetName) ||
|
||||
'test',
|
||||
cleanTargetName:
|
||||
(normalizedOptions.clean && normalizedOptions.clean.targetName) ||
|
||||
'clean',
|
||||
restoreTargetName:
|
||||
(normalizedOptions.restore && normalizedOptions.restore.targetName) ||
|
||||
'restore',
|
||||
publishTargetName:
|
||||
(normalizedOptions.publish && normalizedOptions.publish.targetName) ||
|
||||
'publish',
|
||||
packTargetName:
|
||||
(normalizedOptions.pack && normalizedOptions.pack.targetName) ||
|
||||
'pack',
|
||||
watchTargetName:
|
||||
(normalizedOptions.watch && normalizedOptions.watch.targetName) ||
|
||||
'watch',
|
||||
runTargetName:
|
||||
(normalizedOptions.run && normalizedOptions.run.targetName) || 'run',
|
||||
};
|
||||
|
||||
const result = await analyzeProjects(
|
||||
[...configFilePaths],
|
||||
analyzerOptions
|
||||
);
|
||||
|
||||
if (isAnalysisErrorResult(result)) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
const { nodesByFile } = result;
|
||||
|
||||
// Return array of [configFile, result] tuples
|
||||
return configFilePaths.map((configFile) => {
|
||||
const node = nodesByFile[configFile];
|
||||
if (!node) {
|
||||
// Directory.Build.* / Directory.Solution.* files contribute no projects of
|
||||
// their own; returning an empty config is the conventional "skip" response.
|
||||
return [configFile, {}];
|
||||
}
|
||||
|
||||
// Merge user-specified target configurations with generated targets. The analyzer
|
||||
// has already written the Directory.* inputs onto each cacheable target's Inputs.
|
||||
const mergedNode = mergeUserTargetConfigurations(
|
||||
node,
|
||||
normalizedOptions
|
||||
);
|
||||
|
||||
return [
|
||||
configFile,
|
||||
{
|
||||
projects: {
|
||||
[mergedNode.root]: mergedNode,
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error(`Failed to run MSBuild analyzer: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link createNodes} instead. This will be removed in Nx 24.
|
||||
*/
|
||||
export const createNodesV2 = createNodes;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createNodes as createDotNetNodes } from './create-nodes';
|
||||
import { createDependencies as createDotNetDependencies } from './create-dependencies';
|
||||
|
||||
// The plugin can be fully disabled (e.g. to debug graph issues) via the
|
||||
// NX_DOTNET_DISABLE environment variable. When disabled, no nodes or
|
||||
// dependencies are created so Nx effectively ignores .NET projects.
|
||||
const disabled = process.env.NX_DOTNET_DISABLE === 'true';
|
||||
|
||||
export const name = disabled ? '@nx/dotnet [disabled]' : '@nx/dotnet';
|
||||
|
||||
export const createNodes = disabled ? undefined : createDotNetNodes;
|
||||
|
||||
export const createNodesV2 = disabled ? undefined : createDotNetNodes;
|
||||
|
||||
export const createDependencies = disabled
|
||||
? undefined
|
||||
: createDotNetDependencies;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { readNxJson, Tree } from '@nx/devkit';
|
||||
|
||||
export function hasDotNetPlugin(tree: Tree): boolean {
|
||||
const nxJson = readNxJson(tree);
|
||||
return !!nxJson.plugins?.some((p) =>
|
||||
typeof p === 'string' ? p === '@nx/dotnet' : p.plugin === '@nx/dotnet'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const nxVersion = require('../../package.json').version;
|
||||
Reference in New Issue
Block a user