Skip to content

Commit

Permalink
chore(misc): apply review feedback to clean up findMatchingProjects
Browse files Browse the repository at this point in the history
  • Loading branch information
AgentEnder committed Apr 6, 2023
1 parent 70f9e2b commit b9fa86a
Show file tree
Hide file tree
Showing 12 changed files with 139 additions and 77 deletions.
2 changes: 1 addition & 1 deletion docs/generated/cli/run-many.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Test all projects ending with `*-app` except `excluded-app`. Note: your shell ma
nx run-many --target=test --projects=*-app --exclude=excluded-app
```

Test all projects with tags starting with `api-*`. Note: your shell may require you to escape the `*` like this: `\*`:
Test all projects with tags starting with `api-`. Note: your shell may require you to escape the `*` like this: `\*`:

```shell
nx run-many --target=test --projects=tag:api-*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@
},
"additionalProperties": false,
"required": ["name"],
"examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Simple Application\" %}\n\nCreate an application named `my-app`:\n\n```bash\nnx g @nrwl/angular:application my-app\n```\n\n{% /tab %}\n\n{% tab label=\"Specify directory and style extension\" %}\n\nCreate an application named `my-app` in the `my-dir` directory and use `scss` for styles:\n\n```bash\nnx g @nrwl/angular:app my-app --directory=my-dir --style=scss\n```\n\n{% /tab %}\n\n{% tab label=\"Single File Components application\" %}\n\nCreate an application with Single File Components (inline styles and inline templates):\n\n```bash\nnx g @nrwl/angular:app my-app --inlineStyle --inlineTemplate\n```\n\n{% /tab %}\n\n{% tab label=\"Standalone Components application\" %}\n\nCreate an application that is setup to use standalone components:\n\n```bash\nnx g @nrwl/angular:app my-app --standalone\n```\n\n{% /tab %}\n\n{% tab label=\"Set custom prefix and tags\" %}\n\nSet the prefix to apply to generated selectors and add tags to the application.\n\n```bash\nnx g @nrwl/angular:app my-app --prefix=admin --tags=scope:admin,type:ui\n```\n\n{% /tab %}\n{% /tabs %}\n",
"examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Simple Application\" %}\n\nCreate an application named `my-app`:\n\n```bash\nnx g @nrwl/angular:application my-app\n```\n\n{% /tab %}\n\n{% tab label=\"Specify directory and style extension\" %}\n\nCreate an application named `my-app` in the `my-dir` directory and use `scss` for styles:\n\n```bash\nnx g @nrwl/angular:app my-app --directory=my-dir --style=scss\n```\n\n{% /tab %}\n\n{% tab label=\"Single File Components application\" %}\n\nCreate an application with Single File Components (inline styles and inline templates):\n\n```bash\nnx g @nrwl/angular:app my-app --inlineStyle --inlineTemplate\n```\n\n{% /tab %}\n\n{% tab label=\"Standalone Components application\" %}\n\nCreate an application that is setup to use standalone components:\n\n```bash\nnx g @nrwl/angular:app my-app --standalone\n```\n\n{% /tab %}\n\n{% tab label=\"Set custom prefix and tags\" %}\n\nSet the prefix to apply to generated selectors and add tags to the application (used for linting).\n\n```bash\nnx g @nrwl/angular:app my-app --prefix=admin --tags=scope:admin,type:ui\n```\n\n{% /tab %}\n{% /tabs %}\n",
"presets": []
},
"aliases": ["app"],
Expand Down
2 changes: 1 addition & 1 deletion docs/generated/packages/nx/documents/run-many.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Test all projects ending with `*-app` except `excluded-app`. Note: your shell ma
nx run-many --target=test --projects=*-app --exclude=excluded-app
```

Test all projects with tags starting with `api-*`. Note: your shell may require you to escape the `*` like this: `\*`:
Test all projects with tags starting with `api-`. Note: your shell may require you to escape the `*` like this: `\*`:

```shell
nx run-many --target=test --projects=tag:api-*
Expand Down
2 changes: 1 addition & 1 deletion docs/generated/packages/react/generators/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@
}
},
"required": [],
"examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Simple Application\" %}\n\nCreate an application named `my-app`:\n\n```bash\nnx g @nrwl/react:application my-app\n```\n\n{% /tab %}\n\n{% tab label=\"Application using Vite as bundler\" %}\n\nCreate an application named `my-app`:\n\n```bash\nnx g @nrwl/react:app my-app --bundler=vite\n```\n\n{% /tab %}\n\n{% tab label=\"Specify directory and style extension\" %}\n\nCreate an application named `my-app` in the `my-dir` directory and use `scss` for styles:\n\n```bash\nnx g @nrwl/react:app my-app --directory=my-dir --style=scss\n```\n\n{% /tab %}\n\n{% tab label=\"Add tags\" %}\n\nAdd tags to the application.\n\n```bash\nnx g @nrwl/react:app my-app --tags=scope:admin,type:ui\n```\n\n{% /tab %}\n{% /tabs %}\n",
"examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Simple Application\" %}\n\nCreate an application named `my-app`:\n\n```bash\nnx g @nrwl/react:application my-app\n```\n\n{% /tab %}\n\n{% tab label=\"Application using Vite as bundler\" %}\n\nCreate an application named `my-app`:\n\n```bash\nnx g @nrwl/react:app my-app --bundler=vite\n```\n\n{% /tab %}\n\n{% tab label=\"Specify directory and style extension\" %}\n\nCreate an application named `my-app` in the `my-dir` directory and use `scss` for styles:\n\n```bash\nnx g @nrwl/react:app my-app --directory=my-dir --style=scss\n```\n\n{% /tab %}\n\n{% tab label=\"Add tags\" %}\n\nAdd tags to the application (used for linting).\n\n```bash\nnx g @nrwl/react:app my-app --tags=scope:admin,type:ui\n```\n\n{% /tab %}\n{% /tabs %}\n",
"presets": []
},
"aliases": ["app"],
Expand Down
2 changes: 1 addition & 1 deletion docs/generated/packages/web/generators/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
}
},
"required": ["name"],
"examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Simple Application\" %}\n\nCreate an application named `my-app`:\n\n```bash\nnx g @nrwl/web:application my-app\n```\n\n{% /tab %}\n\n{% tab label=\"Application using Vite as bundler\" %}\n\nCreate an application named `my-app`:\n\n```bash\nnx g @nrwl/web:app my-app --bundler=vite\n```\n\n{% /tab %}\n\n{% tab label=\"Specify directory\" %}\n\nCreate an application named `my-app` in the `my-dir` directory:\n\n```bash\nnx g @nrwl/web:app my-app --directory=my-dir\n```\n\n{% /tab %}\n\n{% tab label=\"Add tags\" %}\n\nAdd tags to the application.\n\n```bash\nnx g @nrwl/web:app my-app --tags=scope:admin,type:ui\n```\n\n{% /tab %}\n{% /tabs %}\n",
"examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Simple Application\" %}\n\nCreate an application named `my-app`:\n\n```bash\nnx g @nrwl/web:application my-app\n```\n\n{% /tab %}\n\n{% tab label=\"Application using Vite as bundler\" %}\n\nCreate an application named `my-app`:\n\n```bash\nnx g @nrwl/web:app my-app --bundler=vite\n```\n\n{% /tab %}\n\n{% tab label=\"Specify directory\" %}\n\nCreate an application named `my-app` in the `my-dir` directory:\n\n```bash\nnx g @nrwl/web:app my-app --directory=my-dir\n```\n\n{% /tab %}\n\n{% tab label=\"Add tags\" %}\n\nAdd tags to the application (used for linting).\n\n```bash\nnx g @nrwl/web:app my-app --tags=scope:admin,type:ui\n```\n\n{% /tab %}\n{% /tabs %}\n",
"presets": []
},
"aliases": ["app"],
Expand Down
2 changes: 1 addition & 1 deletion docs/shared/recipes/generators/generator-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ The alias of this property. Example:
{
"tags": {
"type": "string",
"description": "Add tags to the project",
"description": "Add tags to the project (used for linting)",
"alias": "t"
},
"directory": {
Expand Down
2 changes: 1 addition & 1 deletion packages/nx/src/command-line/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ export const examples: Record<string, Example[]> = {
{
command: 'run-many --target=test --projects=tag:api-*',
description:
'Test all projects with tags starting with `api-*`. Note: your shell may require you to escape the `*` like this: `\\*`',
'Test all projects with tags starting with `api-`. Note: your shell may require you to escape the `*` like this: `\\*`',
},
{
command: 'run-many --targets=lint,test,build --all',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export function buildImplicitProjectDependencies(
ctx: ProjectGraphProcessorContext,
builder: ProjectGraphBuilder
) {
Object.keys(ctx.workspace.projects).forEach((source) => {
const p = ctx.workspace.projects[source];
Object.keys(ctx.projectsConfigurations.projects).forEach((source) => {
const p = ctx.projectsConfigurations.projects[source];
if (p.implicitDependencies && p.implicitDependencies.length > 0) {
p.implicitDependencies.forEach((target) => {
if (target.startsWith('!')) {
Expand Down
16 changes: 9 additions & 7 deletions packages/nx/src/project-graph/build-nodes/workspace-projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ export async function buildWorkspaceProjectNodes(
nxJson: NxJsonConfiguration
) {
const toAdd = [];
const projects = Object.keys(ctx.workspace.projects);
const projectsGraph = projects.reduce((graph, project) => {
const projectConfiguration = ctx.workspace.projects[project];
const projects = Object.keys(ctx.projectsConfigurations.projects);

// Used for expanding implicit dependencies (e.g. `@proj/*` or `tag:foo`)
const partialProjectGraphNodes = projects.reduce((graph, project) => {
const projectConfiguration = ctx.projectsConfigurations.projects[project];
graph[project] = {
name: project,
type: projectConfiguration.projectType === 'library' ? 'lib' : 'app', // missing fallback to `e2e`
Expand All @@ -43,7 +45,7 @@ export async function buildWorkspaceProjectNodes(
}, {} as Record<string, ProjectGraphProjectNode>);

for (const key of projects) {
const p = ctx.workspace.projects[key];
const p = ctx.projectsConfigurations.projects[key];
const projectRoot = join(workspaceRoot, p.root);

if (existsSync(join(projectRoot, 'package.json'))) {
Expand Down Expand Up @@ -73,13 +75,13 @@ export async function buildWorkspaceProjectNodes(
p.implicitDependencies = normalizeImplicitDependencies(
key,
p.implicitDependencies,
projectsGraph
partialProjectGraphNodes
);

p.targets = mergePluginTargetsWithNxTargets(
p.root,
p.targets,
await loadNxPlugins(ctx.workspace.plugins)
await loadNxPlugins(ctx.nxJsonConfiguration.plugins)
);

p.targets = normalizeProjectTargets(p, nxJson.targetDefaults, key);
Expand All @@ -91,7 +93,7 @@ export async function buildWorkspaceProjectNodes(
? 'e2e'
: 'app'
: 'lib';
const tags = ctx.workspace.projects?.[key]?.tags || [];
const tags = ctx.projectsConfigurations.projects?.[key]?.tags || [];

toAdd.push({
name: key,
Expand Down
6 changes: 3 additions & 3 deletions packages/nx/src/utils/assert-workspace-validity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function assertWorkspaceValidity(
nxJson: NxJsonConfiguration
) {
const projectNames = Object.keys(projectsConfigurations.projects);
const projectsGraph = projectNames.reduce((graph, project) => {
const projectGraphNodes = projectNames.reduce((graph, project) => {
const projectConfiguration = projectsConfigurations.projects[project];
graph[project] = {
name: project,
Expand Down Expand Up @@ -64,7 +64,7 @@ export function assertWorkspaceValidity(
filename,
projectNames,
projects,
projectsGraph
projectGraphNodes
);
return map;
}, invalidImplicitDependencies);
Expand All @@ -81,7 +81,7 @@ export function assertWorkspaceValidity(
projectName,
project.implicitDependencies,
projects,
projectsGraph
projectGraphNodes
);
return map;
}, invalidImplicitDependencies);
Expand Down
7 changes: 2 additions & 5 deletions packages/nx/src/utils/find-matching-projects.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { findMatchingProjects } from './find-matching-projects';
import { type ProjectGraphProjectNode } from '../config/project-graph';
import type { ProjectGraphProjectNode } from '../config/project-graph';

describe('findMatchingProjects', () => {
let projectGraph: Record<string, ProjectGraphProjectNode> = {
Expand Down Expand Up @@ -94,7 +94,7 @@ describe('findMatchingProjects', () => {
});

it('should expand "*" for tags', () => {
expect(findMatchingProjects(['tags:*'], projectGraph)).toEqual([
expect(findMatchingProjects(['tag:*'], projectGraph)).toEqual([
'test-project',
'a',
'b',
Expand All @@ -103,9 +103,6 @@ describe('findMatchingProjects', () => {
});

it('should support negation "!" for tags', () => {
expect(findMatchingProjects(['*', 'tag:!api'], projectGraph)).toEqual([
'b',
]);
expect(findMatchingProjects(['*', '!tag:api'], projectGraph)).toEqual([
'b',
]);
Expand Down
169 changes: 116 additions & 53 deletions packages/nx/src/utils/find-matching-projects.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import minimatch = require('minimatch');
import { type ProjectGraphProjectNode } from '../config/project-graph';
import type { ProjectGraphProjectNode } from '../config/project-graph';

const globCharacters = ['*', '|', '{', '}', '(', ')'];

const validPatternTypes = [
'name', // Pattern is based on the project's name
'tag', // Pattern is based on the project's tags
] as const;
type ProjectPatternType = typeof validPatternTypes[number];

interface ProjectPattern {
// If true, the pattern is an exclude pattern
exclude: boolean;
// The type of pattern to match against
type: ProjectPatternType;
// The pattern to match against
value: string;
}

/**
* Find matching project names given a list of potential project names or globs.
*
Expand All @@ -16,81 +31,72 @@ export function findMatchingProjects(
| Record<string, ProjectGraphProjectNode>
| Map<string, ProjectGraphProjectNode>
): string[] {
const projectObject =
projects instanceof Map ? Object.fromEntries(projects) : projects;
const projectNames = Object.keys(projectObject);
const patternObjects = patterns.map((pattern) => {
let isExclude = false;
if (pattern.startsWith('!')) {
isExclude = true;
pattern = pattern.substring(1);
}
let [value, type] = pattern.split(':').reverse();
if (value.startsWith('!')) {
isExclude ||= true;
value = value.substring(1);
}
return {
not: isExclude,
type: type,
value,
};
});
const projectNames = keys(projects);

const patternObjects: ProjectPattern[] = patterns.map((p) =>
parseStringPattern(p, projects)
);

const selectedProjects: Set<string> = new Set();
const excludedProjects: Set<string> = new Set();

for (const patternObject of patternObjects) {
if (patternObject.value === '*') {
projectNames.every((projectName) =>
(patternObject.not ? excludedProjects : selectedProjects).add(
projectName
)
);
if (patternObjects.length === 1) continue;
for (const pattern of patternObjects) {
// Handle wildcard with short-circuit, as its a common case with potentially
// large project sets and we can avoid the more expensive glob matching.
if (pattern.value === '*') {
for (const projectName of projectNames) {
if (pattern.exclude) {
excludedProjects.add(projectName);
} else {
selectedProjects.add(projectName);
}
}
continue;
}

if (patternObject.type === 'tag') {
if (pattern.type === 'tag') {
for (const projectName of projectNames) {
const tags = projectObject[projectName].data.tags || [];
const tags =
getItemInMapOrRecord(projects, projectName).data.tags || [];

if (tags.includes(patternObject.value)) {
(patternObject.not ? excludedProjects : selectedProjects).add(
if (tags.includes(pattern.value)) {
(pattern.exclude ? excludedProjects : selectedProjects).add(
projectName
);
continue;
}

if (!globCharacters.some((c) => patternObject.value.includes(c))) {
if (!globCharacters.some((c) => pattern.value.includes(c))) {
continue;
}

if (minimatch.match(tags, patternObject.value).length)
(patternObject.not ? excludedProjects : selectedProjects).add(
if (minimatch.match(tags, pattern.value).length)
(pattern.exclude ? excludedProjects : selectedProjects).add(
projectName
);
}
continue;
}
} else if (pattern.type === 'name') {
if (hasKey(projects, pattern.value)) {
(pattern.exclude ? excludedProjects : selectedProjects).add(
pattern.value
);
continue;
}

if (projectNames.includes(patternObject.value)) {
(patternObject.not ? excludedProjects : selectedProjects).add(
patternObject.value
);
continue;
}
if (!globCharacters.some((c) => pattern.value.includes(c))) {
continue;
}

if (!globCharacters.some((c) => patternObject.value.includes(c))) {
continue;
const matchedProjectNames = minimatch.match(projectNames, pattern.value);
for (const projectName of matchedProjectNames) {
if (pattern.exclude) {
excludedProjects.add(projectName);
} else {
selectedProjects.add(projectName);
}
}
}

const matchedProjectNames = minimatch.match(
projectNames,
patternObject.value
);
matchedProjectNames.every((projectName) =>
(patternObject.not ? excludedProjects : selectedProjects).add(projectName)
);
}

for (const project of excludedProjects) {
Expand All @@ -99,3 +105,60 @@ export function findMatchingProjects(

return Array.from(selectedProjects);
}

function keys(
object: Record<string, unknown> | Map<string, unknown>
): string[] {
return object instanceof Map ? [...object.keys()] : Object.keys(object);
}

function hasKey(
object: Record<string, unknown> | Map<string, unknown>,
key: string
) {
return object instanceof Map ? object.has(key) : key in object;
}

function getItemInMapOrRecord<T>(
object: Record<string, T> | Map<string, T>,
key: string
): T {
return object instanceof Map ? object.get(key) : object[key];
}

function parseStringPattern(
pattern: string,
projects:
| Map<string, ProjectGraphProjectNode>
| Record<string, ProjectGraphProjectNode>
): ProjectPattern {
let type: ProjectPatternType;
let value: string;
const isExclude = pattern.startsWith('!');

// Support for things like: `!{type}:value`
if (isExclude) {
pattern = pattern.substring(1);
}

const indexOfFirstPotentialSeparator = pattern.indexOf(':');
if (indexOfFirstPotentialSeparator === -1 || hasKey(projects, pattern)) {
type = 'name';
value = pattern;
} else {
const potentialType = pattern.substring(0, indexOfFirstPotentialSeparator);
if (isValidPatternType(potentialType)) {
type = potentialType;
value = pattern.substring(indexOfFirstPotentialSeparator + 1);
} else {
type = 'name';
value = pattern;
}
}

return { type, value, exclude: isExclude };
}

function isValidPatternType(type: string): type is ProjectPatternType {
return validPatternTypes.includes(type as ProjectPatternType);
}

0 comments on commit b9fa86a

Please sign in to comment.