Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 64 additions & 6 deletions packages/angular/cli/src/commands/mcp/tools/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ const listProjectsOutputSchema = {
'The prefix to use for component selectors.' +
` For example, a prefix of 'app' would result in selectors like '<app-my-component>'.`,
),
unitTestFramework: z
.enum(['jasmine', 'jest', 'vitest', 'unknown'])
.optional()
.describe(
'The unit test framework used by the project, such as Jasmine, Jest, or Vitest. ' +
'This field is critical for generating correct and idiomatic unit tests. ' +
'When writing or modifying tests, you MUST use the APIs corresponding to this framework.',
),
}),
),
}),
Expand Down Expand Up @@ -84,21 +92,25 @@ export const LIST_PROJECTS_TOOL = declareTool({
title: 'List Angular Projects',
description: `
<Purpose>
Provides a comprehensive overview of all Angular workspaces and projects within a monorepo.
Provides a comprehensive overview of all Angular workspaces and projects within the repository.
It is essential to use this tool as a first step before performing any project-specific actions to understand the available projects,
their types, and their locations.
</Purpose>
<Use Cases>
* Finding the correct project name to use in other commands (e.g., \`ng generate component my-comp --project=my-app\`).
* Identifying the \`root\` and \`sourceRoot\` of a project to read, analyze, or modify its files.
* Determining if a project is an \`application\` or a \`library\`.
* Determining a project's unit test framework (\`unitTestFramework\`) before writing or modifying tests.
* Getting the \`selectorPrefix\` for a project before generating a new component to ensure it follows conventions.
* Identifying the major version of the Angular framework for each workspace, which is crucial for monorepos.
* Determining a project's primary function by inspecting its builder (e.g., '@angular-devkit/build-angular:browser' for an application).
</Use Cases>
<Operational Notes>
* **Working Directory:** Shell commands for a project (like \`ng generate\`) **MUST**
be executed from the parent directory of the \`path\` field for the relevant workspace.
* **Unit Testing:** The \`unitTestFramework\` field tells you which testing API to use (e.g., Jasmine, Jest).
If the value is 'unknown', you **MUST** inspect the project's configuration files
(e.g., \`karma.conf.js\`, \`jest.config.js\`, or the 'test' target in \`angular.json\`) to determine the
framework before generating tests.
* **Disambiguation:** A monorepo may contain multiple workspaces (e.g., for different applications or even in output directories).
Use the \`path\` of each workspace to understand its context and choose the correct project.
</Operational Notes>`,
Expand Down Expand Up @@ -230,9 +242,55 @@ async function findAngularCoreVersion(
return undefined;
}

// Types for the structured output of the helper function.
// Helper types inferred from the Zod schema for structured data processing.
type WorkspaceData = z.infer<typeof listProjectsOutputSchema.workspaces>[number];
type ParsingError = z.infer<typeof listProjectsOutputSchema.parsingErrors>[number];
type VersioningError = z.infer<typeof listProjectsOutputSchema.versioningErrors>[number];

/**
* Determines the unit test framework for a project based on its 'test' target configuration.
* It handles both the modern `@angular/build:unit-test` builder with its `runner` option
* and older builders where the framework is inferred from the builder name.
* @param testTarget The 'test' target definition from the workspace configuration.
* @returns The name of the unit test framework ('jasmine', 'jest', 'vitest'), 'unknown' if
* the framework can't be determined from a known builder, or `undefined` if there is no test target.
*/
function getUnitTestFramework(
testTarget: import('@angular-devkit/core').workspaces.TargetDefinition | undefined,
): 'jasmine' | 'jest' | 'vitest' | 'unknown' | undefined {
if (!testTarget) {
return undefined;
}

// For the new unit-test builder, the runner option directly informs the framework.
if (testTarget.builder === '@angular/build:unit-test') {
const runner = testTarget.options?.['runner'] as 'karma' | 'vitest' | undefined;
if (runner === 'karma') {
return 'jasmine'; // Karma is a runner, but the framework is Jasmine.
} else {
return runner; // For 'vitest', the runner and framework are the same.
}
}

// Fallback for older builders where the framework is inferred from the builder name.
if (testTarget.builder) {
const testBuilder = testTarget.builder;
if (
testBuilder.includes('karma') ||
testBuilder === '@angular-devkit/build-angular:web-test-runner'
) {
return 'jasmine';
} else if (testBuilder.includes('jest')) {
return 'jest';
} else if (testBuilder.includes('vitest')) {
return 'vitest';
} else {
return 'unknown';
}
}

return undefined;
}

/**
* Loads, parses, and transforms a single angular.json file into the tool's output format.
Expand All @@ -255,13 +313,16 @@ async function loadAndParseWorkspace(
const ws = await AngularWorkspace.load(configFile);
const projects = [];
for (const [name, project] of ws.projects.entries()) {
const unitTestFramework = getUnitTestFramework(project.targets.get('test'));

projects.push({
name,
type: project.extensions['projectType'] as 'application' | 'library' | undefined,
builder: project.targets.get('build')?.builder,
root: project.root,
sourceRoot: project.sourceRoot ?? path.posix.join(project.root, 'src'),
selectorPrefix: project.extensions['prefix'] as string,
unitTestFramework,
});
}

Expand All @@ -278,9 +339,6 @@ async function loadAndParseWorkspace(
}
}

// Types for the structured output of the helper function.
type VersioningError = z.infer<typeof listProjectsOutputSchema.versioningErrors>[number];

/**
* Processes a single `angular.json` file to extract workspace and framework version information.
* @param configFile The path to the `angular.json` file.
Expand Down