Skip to content

Commit 8a2480c

Browse files
fix(workspace-tools): resolve catalog workspace deps in foreach
1 parent 6861e75 commit 8a2480c

3 files changed

Lines changed: 178 additions & 15 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
releases:
2+
"@yarnpkg/cli": patch
3+
"@yarnpkg/plugin-workspace-tools": patch
4+
5+
declined:
6+
- "@yarnpkg/plugin-compat"
7+
- "@yarnpkg/plugin-constraints"
8+
- "@yarnpkg/plugin-dlx"
9+
- "@yarnpkg/plugin-essentials"
10+
- "@yarnpkg/plugin-init"
11+
- "@yarnpkg/plugin-interactive-tools"
12+
- "@yarnpkg/plugin-nm"
13+
- "@yarnpkg/plugin-npm-cli"
14+
- "@yarnpkg/plugin-pack"
15+
- "@yarnpkg/plugin-patch"
16+
- "@yarnpkg/plugin-pnp"
17+
- "@yarnpkg/plugin-pnpm"
18+
- "@yarnpkg/plugin-stage"
19+
- "@yarnpkg/plugin-typescript"
20+
- "@yarnpkg/plugin-version"
21+
- "@yarnpkg/builder"
22+
- "@yarnpkg/core"
23+
- "@yarnpkg/doctor"
24+
- "@yarnpkg/nm"
25+
- "@yarnpkg/pnpify"
26+
- "@yarnpkg/sdks"

packages/acceptance-tests/pkg-tests-specs/sources/commands/workspaces/foreach.test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const {
44
exec: {execFile},
55
fs: {writeJson, writeFile},
66
tests: {testIf, FEATURE_CHECKS},
7+
yarn,
78
} = require(`pkg-tests-core`);
89

910
const forEachVerboseDone = FEATURE_CHECKS.forEachVerboseDone
@@ -773,6 +774,54 @@ describe(`Commands`, () => {
773774
),
774775
);
775776

777+
test(
778+
`should include workspace dependencies resolved through catalogs if using --from and --recursive`,
779+
makeTemporaryEnv(
780+
{
781+
private: true,
782+
workspaces: [`packages/*`],
783+
},
784+
async ({path, run}) => {
785+
await writeJson(`${path}/packages/my-dep/package.json`, {
786+
name: `my-dep`,
787+
version: `1.0.0`,
788+
scripts: {
789+
print: `echo Test My Dep`,
790+
},
791+
});
792+
793+
await writeJson(`${path}/packages/my-package/package.json`, {
794+
name: `my-package`,
795+
version: `1.0.0`,
796+
scripts: {
797+
print: `echo Test My Package`,
798+
},
799+
dependencies: {
800+
[`my-dep`]: `catalog:`,
801+
},
802+
});
803+
804+
await yarn.writeConfiguration(path, {
805+
enableTransparentWorkspaces: false,
806+
catalog: {
807+
[`my-dep`]: `workspace:*`,
808+
},
809+
});
810+
811+
await run(`install`);
812+
813+
await expect(run(`workspaces`, `foreach`, `--recursive`, `--topological-dev`, `--from`, `my-package`, `--exclude`, `my-package`, `run`, `print`, {cwd: path})).resolves.toEqual({
814+
code: 0,
815+
stderr: ``,
816+
stdout: [
817+
`Test My Dep\n`,
818+
...forEachVerboseDone,
819+
].join(``),
820+
});
821+
},
822+
),
823+
);
824+
776825
test(
777826
`--since runs on no workspaces if there have been no changes`,
778827
makeWorkspacesForeachSinceEnv(async ({run}) => {

packages/plugin-workspace-tools/sources/commands/foreach.ts

Lines changed: 103 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli';
2-
import {Configuration, LocatorHash, Project, scriptUtils, Workspace} from '@yarnpkg/core';
3-
import {DescriptorHash, MessageName, Report, StreamReport} from '@yarnpkg/core';
4-
import {formatUtils, miscUtils, structUtils, nodeUtils} from '@yarnpkg/core';
5-
import {gitUtils} from '@yarnpkg/plugin-git';
6-
import {Command, Option, Usage, UsageError} from 'clipanion';
7-
import micromatch from 'micromatch';
8-
import pLimit from 'p-limit';
9-
import {Writable} from 'stream';
10-
import {WriteStream} from 'tty';
11-
import * as t from 'typanion';
1+
import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli';
2+
import {Configuration, LocatorHash, Manifest, Project, scriptUtils, ThrowReport, Workspace} from '@yarnpkg/core';
3+
import {Descriptor, DescriptorHash, HardDependencies, MessageName, Report, StreamReport} from '@yarnpkg/core';
4+
import {formatUtils, miscUtils, structUtils, nodeUtils} from '@yarnpkg/core';
5+
import {gitUtils} from '@yarnpkg/plugin-git';
6+
import {Command, Option, Usage, UsageError} from 'clipanion';
7+
import micromatch from 'micromatch';
8+
import pLimit from 'p-limit';
9+
import {Writable} from 'stream';
10+
import {WriteStream} from 'tty';
11+
import * as t from 'typanion';
1212

1313
// eslint-disable-next-line arca/no-default-export
1414
export default class WorkspacesForeachCommand extends BaseCommand {
@@ -149,6 +149,90 @@ export default class WorkspacesForeachCommand extends BaseCommand {
149149
if (command.path.length === 0)
150150
throw new UsageError(`Invalid subcommand name for iteration - use the 'run' keyword if you wish to execute a script`);
151151

152+
const resolver = configuration.makeResolver();
153+
const resolveOptions = {
154+
project,
155+
resolver,
156+
report: new ThrowReport(),
157+
};
158+
159+
const resolvedWorkspaceDependencies = new Map<string, Promise<Workspace | null>>();
160+
const tryWorkspaceByDescriptor = async (workspace: Workspace, descriptor: Descriptor) => {
161+
const cacheKey = `${workspace.anchoredLocator.locatorHash}:${descriptor.descriptorHash}`;
162+
let promise = resolvedWorkspaceDependencies.get(cacheKey);
163+
if (typeof promise === `undefined`) {
164+
promise = (async () => {
165+
const dependency = await configuration.reduceHook(hooks => {
166+
return hooks.reduceDependency;
167+
}, descriptor, project, workspace.anchoredLocator, descriptor, {
168+
resolver,
169+
resolveOptions,
170+
});
171+
172+
if (!structUtils.areIdentsEqual(descriptor, dependency))
173+
throw new Error(`Assertion failed: The descriptor ident cannot be changed through aliases`);
174+
175+
return project.tryWorkspaceByDescriptor(dependency);
176+
})();
177+
178+
resolvedWorkspaceDependencies.set(cacheKey, promise);
179+
}
180+
181+
return await promise;
182+
};
183+
184+
const getRecursiveWorkspaceDependencies = async (workspace: Workspace, {dependencies = Manifest.hardDependencies}: {dependencies?: Array<HardDependencies>} = {}) => {
185+
const workspaceList = new Set<Workspace>();
186+
187+
const visitWorkspace = async (workspace: Workspace) => {
188+
for (const dependencyType of dependencies) {
189+
for (const descriptor of workspace.manifest[dependencyType].values()) {
190+
const foundWorkspace = await tryWorkspaceByDescriptor(workspace, descriptor);
191+
if (foundWorkspace === null || workspaceList.has(foundWorkspace))
192+
continue;
193+
194+
workspaceList.add(foundWorkspace);
195+
await visitWorkspace(foundWorkspace);
196+
}
197+
}
198+
};
199+
200+
await visitWorkspace(workspace);
201+
return workspaceList;
202+
};
203+
204+
const getRecursiveWorkspaceDependents = async (workspace: Workspace, {dependencies = Manifest.hardDependencies}: {dependencies?: Array<HardDependencies>} = {}) => {
205+
const workspaceList = new Set<Workspace>();
206+
207+
const visitWorkspace = async (workspace: Workspace) => {
208+
for (const projectWorkspace of project.workspaces) {
209+
let isDependent = false;
210+
211+
for (const dependencyType of dependencies) {
212+
for (const descriptor of projectWorkspace.manifest[dependencyType].values()) {
213+
const foundWorkspace = await tryWorkspaceByDescriptor(projectWorkspace, descriptor);
214+
if (foundWorkspace !== null && structUtils.areLocatorsEqual(foundWorkspace.anchoredLocator, workspace.anchoredLocator)) {
215+
isDependent = true;
216+
break;
217+
}
218+
}
219+
220+
if (isDependent) {
221+
break;
222+
}
223+
}
224+
225+
if (isDependent && !workspaceList.has(projectWorkspace)) {
226+
workspaceList.add(projectWorkspace);
227+
await visitWorkspace(projectWorkspace);
228+
}
229+
}
230+
};
231+
232+
await visitWorkspace(workspace);
233+
return workspaceList;
234+
};
235+
152236
const log = (msg: string) => {
153237
if (!this.dryRun)
154238
return;
@@ -200,10 +284,14 @@ export default class WorkspacesForeachCommand extends BaseCommand {
200284
if (this.recursive) {
201285
if (this.since) {
202286
log(`Option --recursive --since is set; recursively selecting all dependent workspaces`);
203-
extra = new Set(selection.map(workspace => [...workspace.getRecursiveWorkspaceDependents()]).flat());
287+
extra = new Set((await Promise.all(selection.map(async workspace => {
288+
return [...await getRecursiveWorkspaceDependents(workspace)];
289+
}))).flat());
204290
} else {
205291
log(`Option --recursive is set; recursively selecting all transitive dependencies`);
206-
extra = new Set(selection.map(workspace => [...workspace.getRecursiveWorkspaceDependencies()]).flat());
292+
extra = new Set((await Promise.all(selection.map(async workspace => {
293+
return [...await getRecursiveWorkspaceDependencies(workspace)];
294+
}))).flat());
207295
}
208296
} else if (this.worktree) {
209297
log(`Option --worktree is set; recursively selecting all nested workspaces`);
@@ -387,8 +475,8 @@ export default class WorkspacesForeachCommand extends BaseCommand {
387475
: workspace.manifest.dependencies;
388476

389477
for (const descriptor of resolvedSet.values()) {
390-
const workspace = project.tryWorkspaceByDescriptor(descriptor);
391-
isRunnable = workspace === null || !needsProcessing.has(workspace.anchoredLocator.locatorHash);
478+
const dependencyWorkspace = await tryWorkspaceByDescriptor(workspace, descriptor);
479+
isRunnable = dependencyWorkspace === null || !needsProcessing.has(dependencyWorkspace.anchoredLocator.locatorHash);
392480

393481
if (!isRunnable) {
394482
break;

0 commit comments

Comments
 (0)