Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions libraries/rush-lib/src/cli/parsing/SelectionParameterSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { NamedProjectSelectorParser } from '../../logic/selectors/NamedProjectSe
import { TagProjectSelectorParser } from '../../logic/selectors/TagProjectSelectorParser';
import { VersionPolicyProjectSelectorParser } from '../../logic/selectors/VersionPolicyProjectSelectorParser';
import { SubspaceSelectorParser } from '../../logic/selectors/SubspaceSelectorParser';
import { PathProjectSelectorParser } from '../../logic/selectors/PathProjectSelectorParser';
import { RushConstants } from '../../logic/RushConstants';
import type { Subspace } from '../../api/Subspace';

Expand Down Expand Up @@ -72,6 +73,7 @@ export class SelectionParameterSet {
selectorParsers.set('tag', new TagProjectSelectorParser(rushConfiguration));
selectorParsers.set('version-policy', new VersionPolicyProjectSelectorParser(rushConfiguration));
selectorParsers.set('subspace', new SubspaceSelectorParser(rushConfiguration));
selectorParsers.set('path', new PathProjectSelectorParser(rushConfiguration));

this._selectorParserByScope = selectorParsers;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import * as nodePath from 'node:path';

import { AlreadyReportedError } from '@rushstack/node-core-library';
import type { LookupByPath } from '@rushstack/lookup-by-path';

import type { RushConfiguration } from '../../api/RushConfiguration';
import type { RushConfigurationProject } from '../../api/RushConfigurationProject';
import type { IEvaluateSelectorOptions, ISelectorParser } from './ISelectorParser';
import { RushConstants } from '../RushConstants';

export class PathProjectSelectorParser implements ISelectorParser<RushConfigurationProject> {
private readonly _rushConfiguration: RushConfiguration;

public constructor(rushConfiguration: RushConfiguration) {
this._rushConfiguration = rushConfiguration;
}

public async evaluateSelectorAsync({
unscopedSelector,
terminal,
parameterName
}: IEvaluateSelectorOptions): Promise<Iterable<RushConfigurationProject>> {
// Resolve the input path against the working directory
const absolutePath: string = nodePath.resolve(process.cwd(), unscopedSelector);

// Relativize it to the rushJsonFolder
const relativePath: string = nodePath.relative(this._rushConfiguration.rushJsonFolder, absolutePath);

// If the path is outside the Rush workspace, it's an error
if (relativePath.startsWith('..') || nodePath.isAbsolute(relativePath)) {
terminal.writeErrorLine(
`The path "${unscopedSelector}" passed to "${parameterName}" resolves to "${absolutePath}" ` +
`which is outside the Rush workspace root "${this._rushConfiguration.rushJsonFolder}".`
);
throw new AlreadyReportedError();
}

// Get the LookupByPath instance for the Rush root
const lookupByPath: LookupByPath<RushConfigurationProject> =
this._rushConfiguration.getProjectLookupForRoot(this._rushConfiguration.rushJsonFolder);

// Try to find a project that contains this path, or all projects within this path
const exactProject: RushConfigurationProject | undefined = lookupByPath.get(relativePath);

if (exactProject) {
// The path exactly matches a project folder
return [exactProject];
}

// Check if this is a path within a project
const containingProject: RushConfigurationProject | undefined = lookupByPath.findChildPath(relativePath);

if (containingProject) {
// The path is within a project
return [containingProject];
}

// Check if there are any projects under this path (i.e., it's a directory containing projects)
const projectsUnderPath: RushConfigurationProject[] = [];
for (const [, project] of lookupByPath.entries(relativePath)) {
projectsUnderPath.push(project);
}

if (projectsUnderPath.length > 0) {
return projectsUnderPath;
}

// No projects found
terminal.writeErrorLine(
`The path "${unscopedSelector}" passed to "${parameterName}" does not match any project in ` +
`${RushConstants.rushJsonFilename}. The resolved path relative to the Rush root is "${relativePath}".`
);
throw new AlreadyReportedError();
}

public getCompletions(): Iterable<string> {
// Return empty completions as path completions are typically handled by the shell
return [];
}
}
Loading