Skip to content

Commit 567ab39

Browse files
committed
chore(misc): apply review feedback to clean up findMatchingProjects
1 parent 70f9e2b commit 567ab39

File tree

7 files changed

+130
-66
lines changed

7 files changed

+130
-66
lines changed

docs/generated/packages/angular/generators/application.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@
165165
},
166166
"additionalProperties": false,
167167
"required": ["name"],
168-
"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",
168+
"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",
169169
"presets": []
170170
},
171171
"aliases": ["app"],

docs/generated/packages/react/generators/application.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@
192192
}
193193
},
194194
"required": [],
195-
"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",
195+
"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",
196196
"presets": []
197197
},
198198
"aliases": ["app"],

docs/generated/packages/web/generators/application.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
}
105105
},
106106
"required": ["name"],
107-
"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",
107+
"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",
108108
"presets": []
109109
},
110110
"aliases": ["app"],

docs/shared/recipes/generators/generator-options.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ The alias of this property. Example:
533533
{
534534
"tags": {
535535
"type": "string",
536-
"description": "Add tags to the project",
536+
"description": "Add tags to the project (used for linting)",
537537
"alias": "t"
538538
},
539539
"directory": {

packages/nx/src/project-graph/build-nodes/workspace-projects.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ export async function buildWorkspaceProjectNodes(
2828
nxJson: NxJsonConfiguration
2929
) {
3030
const toAdd = [];
31-
const projects = Object.keys(ctx.workspace.projects);
32-
const projectsGraph = projects.reduce((graph, project) => {
33-
const projectConfiguration = ctx.workspace.projects[project];
31+
const projects = Object.keys(ctx.projectsConfigurations.projects);
32+
33+
// Used for expanding implicit dependencies (e.g. `@proj/*` or `tag:foo`)
34+
const partialProjectGraphNodes = projects.reduce((graph, project) => {
35+
const projectConfiguration = ctx.projectsConfigurations.projects[project];
3436
graph[project] = {
3537
name: project,
3638
type: projectConfiguration.projectType === 'library' ? 'lib' : 'app', // missing fallback to `e2e`
@@ -43,7 +45,7 @@ export async function buildWorkspaceProjectNodes(
4345
}, {} as Record<string, ProjectGraphProjectNode>);
4446

4547
for (const key of projects) {
46-
const p = ctx.workspace.projects[key];
48+
const p = ctx.projectsConfigurations.projects[key];
4749
const projectRoot = join(workspaceRoot, p.root);
4850

4951
if (existsSync(join(projectRoot, 'package.json'))) {
@@ -73,13 +75,13 @@ export async function buildWorkspaceProjectNodes(
7375
p.implicitDependencies = normalizeImplicitDependencies(
7476
key,
7577
p.implicitDependencies,
76-
projectsGraph
78+
partialProjectGraphNodes
7779
);
7880

7981
p.targets = mergePluginTargetsWithNxTargets(
8082
p.root,
8183
p.targets,
82-
await loadNxPlugins(ctx.workspace.plugins)
84+
await loadNxPlugins(ctx.nxJsonConfiguration.plugins)
8385
);
8486

8587
p.targets = normalizeProjectTargets(p, nxJson.targetDefaults, key);

packages/nx/src/utils/assert-workspace-validity.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function assertWorkspaceValidity(
1212
nxJson: NxJsonConfiguration
1313
) {
1414
const projectNames = Object.keys(projectsConfigurations.projects);
15-
const projectsGraph = projectNames.reduce((graph, project) => {
15+
const projectGraphNodes = projectNames.reduce((graph, project) => {
1616
const projectConfiguration = projectsConfigurations.projects[project];
1717
graph[project] = {
1818
name: project,
@@ -64,7 +64,7 @@ export function assertWorkspaceValidity(
6464
filename,
6565
projectNames,
6666
projects,
67-
projectsGraph
67+
projectGraphNodes
6868
);
6969
return map;
7070
}, invalidImplicitDependencies);
@@ -81,7 +81,7 @@ export function assertWorkspaceValidity(
8181
projectName,
8282
project.implicitDependencies,
8383
projects,
84-
projectsGraph
84+
projectGraphNodes
8585
);
8686
return map;
8787
}, invalidImplicitDependencies);
Lines changed: 115 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
11
import minimatch = require('minimatch');
2-
import { type ProjectGraphProjectNode } from '../config/project-graph';
2+
import type { ProjectGraphProjectNode } from '../config/project-graph';
33

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

6+
const validPatternTypes = [
7+
'name', // Pattern is based on the project's name
8+
'tag', // Pattern is based on the project's tags
9+
] as const;
10+
type ProjectPatternType = typeof validPatternTypes[number];
11+
12+
interface ProjectPattern {
13+
// If true, the pattern is an exclude pattern
14+
exclude: boolean;
15+
// The type of pattern to match against
16+
type: ProjectPatternType;
17+
// The pattern to match against
18+
value: string;
19+
}
20+
621
/**
722
* Find matching project names given a list of potential project names or globs.
823
*
@@ -16,81 +31,72 @@ export function findMatchingProjects(
1631
| Record<string, ProjectGraphProjectNode>
1732
| Map<string, ProjectGraphProjectNode>
1833
): string[] {
19-
const projectObject =
20-
projects instanceof Map ? Object.fromEntries(projects) : projects;
21-
const projectNames = Object.keys(projectObject);
22-
const patternObjects = patterns.map((pattern) => {
23-
let isExclude = false;
24-
if (pattern.startsWith('!')) {
25-
isExclude = true;
26-
pattern = pattern.substring(1);
27-
}
28-
let [value, type] = pattern.split(':').reverse();
29-
if (value.startsWith('!')) {
30-
isExclude ||= true;
31-
value = value.substring(1);
32-
}
33-
return {
34-
not: isExclude,
35-
type: type,
36-
value,
37-
};
38-
});
34+
const projectNames = keys(projects);
35+
36+
const patternObjects: ProjectPattern[] = patterns.map((p) =>
37+
parseStringPattern(p, projects)
38+
);
3939

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

43-
for (const patternObject of patternObjects) {
44-
if (patternObject.value === '*') {
45-
projectNames.every((projectName) =>
46-
(patternObject.not ? excludedProjects : selectedProjects).add(
47-
projectName
48-
)
49-
);
50-
if (patternObjects.length === 1) continue;
43+
for (const pattern of patternObjects) {
44+
// Handle wildcard with short-circuit, as its a common case with potentially
45+
// large project sets and we can avoid the more expensive glob matching.
46+
if (pattern.value === '*') {
47+
for (const projectName of projectNames) {
48+
if (pattern.exclude) {
49+
excludedProjects.add(projectName);
50+
} else {
51+
selectedProjects.add(projectName);
52+
}
53+
}
54+
continue;
5155
}
5256

53-
if (patternObject.type === 'tag') {
57+
if (pattern.type === 'tag') {
5458
for (const projectName of projectNames) {
55-
const tags = projectObject[projectName].data.tags || [];
59+
const tags =
60+
getItemInMapOrRecord(projects, projectName).data.tags || [];
5661

57-
if (tags.includes(patternObject.value)) {
58-
(patternObject.not ? excludedProjects : selectedProjects).add(
62+
if (tags.includes(pattern.value)) {
63+
(pattern.exclude ? excludedProjects : selectedProjects).add(
5964
projectName
6065
);
6166
continue;
6267
}
6368

64-
if (!globCharacters.some((c) => patternObject.value.includes(c))) {
69+
if (!globCharacters.some((c) => pattern.value.includes(c))) {
6570
continue;
6671
}
6772

68-
if (minimatch.match(tags, patternObject.value).length)
69-
(patternObject.not ? excludedProjects : selectedProjects).add(
73+
if (minimatch.match(tags, pattern.value).length)
74+
(pattern.exclude ? excludedProjects : selectedProjects).add(
7075
projectName
7176
);
7277
}
7378
continue;
74-
}
79+
} else if (pattern.type === 'name') {
80+
if (hasKey(projects, pattern.value)) {
81+
(pattern.exclude ? excludedProjects : selectedProjects).add(
82+
pattern.value
83+
);
84+
continue;
85+
}
7586

76-
if (projectNames.includes(patternObject.value)) {
77-
(patternObject.not ? excludedProjects : selectedProjects).add(
78-
patternObject.value
79-
);
80-
continue;
81-
}
87+
if (!globCharacters.some((c) => pattern.value.includes(c))) {
88+
continue;
89+
}
8290

83-
if (!globCharacters.some((c) => patternObject.value.includes(c))) {
84-
continue;
91+
const matchedProjectNames = minimatch.match(projectNames, pattern.value);
92+
for (const projectName of matchedProjectNames) {
93+
if (pattern.exclude) {
94+
excludedProjects.add(projectName);
95+
} else {
96+
selectedProjects.add(projectName);
97+
}
98+
}
8599
}
86-
87-
const matchedProjectNames = minimatch.match(
88-
projectNames,
89-
patternObject.value
90-
);
91-
matchedProjectNames.every((projectName) =>
92-
(patternObject.not ? excludedProjects : selectedProjects).add(projectName)
93-
);
94100
}
95101

96102
for (const project of excludedProjects) {
@@ -99,3 +105,59 @@ export function findMatchingProjects(
99105

100106
return Array.from(selectedProjects);
101107
}
108+
109+
function keys(
110+
object: Record<string, unknown> | Map<string, unknown>
111+
): string[] {
112+
return object instanceof Map ? [...object.keys()] : Object.keys(object);
113+
}
114+
115+
function hasKey(
116+
object: Record<string, unknown> | Map<string, unknown>,
117+
key: string
118+
) {
119+
return object instanceof Map ? object.has(key) : key in object;
120+
}
121+
122+
function getItemInMapOrRecord<T>(
123+
object: Record<string, T> | Map<string, T>,
124+
key: string
125+
): T {
126+
return object instanceof Map ? object.get(key) : object[key];
127+
}
128+
129+
function parseStringPattern(
130+
pattern: string,
131+
projects:
132+
| Map<string, ProjectGraphProjectNode>
133+
| Record<string, ProjectGraphProjectNode>
134+
): ProjectPattern {
135+
let type: ProjectPatternType;
136+
let value: string;
137+
let isExclude = false;
138+
if (pattern.startsWith('!')) {
139+
isExclude = true;
140+
pattern = pattern.substring(1);
141+
}
142+
143+
const indexOfFirstPotentialSeparator = pattern.indexOf(':');
144+
if (indexOfFirstPotentialSeparator === -1 || hasKey(projects, pattern)) {
145+
type = 'name';
146+
value = pattern;
147+
} else {
148+
const potentialType = pattern.substring(0, indexOfFirstPotentialSeparator);
149+
if (isValidPatternType(potentialType)) {
150+
type = potentialType as ProjectPatternType;
151+
value = pattern.substring(indexOfFirstPotentialSeparator + 1);
152+
} else {
153+
type = 'name';
154+
value = pattern;
155+
}
156+
}
157+
158+
return { type, value, exclude: isExclude };
159+
}
160+
161+
function isValidPatternType(type: string): type is ProjectPatternType {
162+
return validPatternTypes.includes(type as ProjectPatternType);
163+
}

0 commit comments

Comments
 (0)