Skip to content
Open
Show file tree
Hide file tree
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
26 changes: 26 additions & 0 deletions .yarn/versions/7129-catalog-workspace.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
releases:
"@yarnpkg/cli": patch
"@yarnpkg/plugin-workspace-tools": patch

declined:
- "@yarnpkg/plugin-compat"
- "@yarnpkg/plugin-constraints"
- "@yarnpkg/plugin-dlx"
- "@yarnpkg/plugin-essentials"
- "@yarnpkg/plugin-init"
- "@yarnpkg/plugin-interactive-tools"
- "@yarnpkg/plugin-nm"
- "@yarnpkg/plugin-npm-cli"
- "@yarnpkg/plugin-pack"
- "@yarnpkg/plugin-patch"
- "@yarnpkg/plugin-pnp"
- "@yarnpkg/plugin-pnpm"
- "@yarnpkg/plugin-stage"
- "@yarnpkg/plugin-typescript"
- "@yarnpkg/plugin-version"
- "@yarnpkg/builder"
- "@yarnpkg/core"
- "@yarnpkg/doctor"
- "@yarnpkg/nm"
- "@yarnpkg/pnpify"
- "@yarnpkg/sdks"
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
exec: {execFile},
fs: {writeJson, writeFile},
tests: {testIf, FEATURE_CHECKS},
yarn,
} = require(`pkg-tests-core`);

const forEachVerboseDone = FEATURE_CHECKS.forEachVerboseDone
Expand Down Expand Up @@ -773,6 +774,54 @@ describe(`Commands`, () => {
),
);

test(
`should include workspace dependencies resolved through catalogs if using --from and --recursive`,
makeTemporaryEnv(
{
private: true,
workspaces: [`packages/*`],
},
async ({path, run}) => {
await writeJson(`${path}/packages/my-dep/package.json`, {
name: `my-dep`,
version: `1.0.0`,
scripts: {
print: `echo Test My Dep`,
},
});

await writeJson(`${path}/packages/my-package/package.json`, {
name: `my-package`,
version: `1.0.0`,
scripts: {
print: `echo Test My Package`,
},
dependencies: {
[`my-dep`]: `catalog:`,
},
});

await yarn.writeConfiguration(path, {
enableTransparentWorkspaces: false,
catalog: {
[`my-dep`]: `workspace:*`,
},
});

await run(`install`);

await expect(run(`workspaces`, `foreach`, `--recursive`, `--topological-dev`, `--from`, `my-package`, `--exclude`, `my-package`, `run`, `print`, {cwd: path})).resolves.toEqual({
code: 0,
stderr: ``,
stdout: [
`Test My Dep\n`,
...forEachVerboseDone,
].join(``),
});
},
),
);

test(
`--since runs on no workspaces if there have been no changes`,
makeWorkspacesForeachSinceEnv(async ({run}) => {
Expand Down
118 changes: 103 additions & 15 deletions packages/plugin-workspace-tools/sources/commands/foreach.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli';
import {Configuration, LocatorHash, Project, scriptUtils, Workspace} from '@yarnpkg/core';
import {DescriptorHash, MessageName, Report, StreamReport} from '@yarnpkg/core';
import {formatUtils, miscUtils, structUtils, nodeUtils} from '@yarnpkg/core';
import {gitUtils} from '@yarnpkg/plugin-git';
import {Command, Option, Usage, UsageError} from 'clipanion';
import micromatch from 'micromatch';
import pLimit from 'p-limit';
import {Writable} from 'stream';
import {WriteStream} from 'tty';
import * as t from 'typanion';
import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli';
import {Configuration, LocatorHash, Manifest, Project, scriptUtils, ThrowReport, Workspace} from '@yarnpkg/core';
import {Descriptor, DescriptorHash, HardDependencies, MessageName, Report, StreamReport} from '@yarnpkg/core';
import {formatUtils, miscUtils, structUtils, nodeUtils} from '@yarnpkg/core';
import {gitUtils} from '@yarnpkg/plugin-git';
import {Command, Option, Usage, UsageError} from 'clipanion';
import micromatch from 'micromatch';
import pLimit from 'p-limit';
import {Writable} from 'stream';
import {WriteStream} from 'tty';
import * as t from 'typanion';

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

const resolver = configuration.makeResolver();
const resolveOptions = {
project,
resolver,
report: new ThrowReport(),
};

const resolvedWorkspaceDependencies = new Map<string, Promise<Workspace | null>>();
const tryWorkspaceByDescriptor = async (workspace: Workspace, descriptor: Descriptor) => {
const cacheKey = `${workspace.anchoredLocator.locatorHash}:${descriptor.descriptorHash}`;
let promise = resolvedWorkspaceDependencies.get(cacheKey);
if (typeof promise === `undefined`) {
promise = (async () => {
const dependency = await configuration.reduceHook(hooks => {
return hooks.reduceDependency;
}, descriptor, project, workspace.anchoredLocator, descriptor, {
resolver,
resolveOptions,
});

if (!structUtils.areIdentsEqual(descriptor, dependency))
throw new Error(`Assertion failed: The descriptor ident cannot be changed through aliases`);

return project.tryWorkspaceByDescriptor(dependency);
})();

resolvedWorkspaceDependencies.set(cacheKey, promise);
}

return await promise;
};

const getRecursiveWorkspaceDependencies = async (workspace: Workspace, {dependencies = Manifest.hardDependencies}: {dependencies?: Array<HardDependencies>} = {}) => {
const workspaceList = new Set<Workspace>();

const visitWorkspace = async (workspace: Workspace) => {
for (const dependencyType of dependencies) {
for (const descriptor of workspace.manifest[dependencyType].values()) {
const foundWorkspace = await tryWorkspaceByDescriptor(workspace, descriptor);
if (foundWorkspace === null || workspaceList.has(foundWorkspace))
continue;

workspaceList.add(foundWorkspace);
await visitWorkspace(foundWorkspace);
}
}
};

await visitWorkspace(workspace);
return workspaceList;
};

const getRecursiveWorkspaceDependents = async (workspace: Workspace, {dependencies = Manifest.hardDependencies}: {dependencies?: Array<HardDependencies>} = {}) => {
const workspaceList = new Set<Workspace>();

const visitWorkspace = async (workspace: Workspace) => {
for (const projectWorkspace of project.workspaces) {
let isDependent = false;

for (const dependencyType of dependencies) {
for (const descriptor of projectWorkspace.manifest[dependencyType].values()) {
const foundWorkspace = await tryWorkspaceByDescriptor(projectWorkspace, descriptor);
if (foundWorkspace !== null && structUtils.areLocatorsEqual(foundWorkspace.anchoredLocator, workspace.anchoredLocator)) {
isDependent = true;
break;
}
}

if (isDependent) {
break;
}
}

if (isDependent && !workspaceList.has(projectWorkspace)) {
workspaceList.add(projectWorkspace);
await visitWorkspace(projectWorkspace);
}
}
};

await visitWorkspace(workspace);
return workspaceList;
};

const log = (msg: string) => {
if (!this.dryRun)
return;
Expand Down Expand Up @@ -200,10 +284,14 @@ export default class WorkspacesForeachCommand extends BaseCommand {
if (this.recursive) {
if (this.since) {
log(`Option --recursive --since is set; recursively selecting all dependent workspaces`);
extra = new Set(selection.map(workspace => [...workspace.getRecursiveWorkspaceDependents()]).flat());
extra = new Set((await Promise.all(selection.map(async workspace => {
return [...await getRecursiveWorkspaceDependents(workspace)];
}))).flat());
} else {
log(`Option --recursive is set; recursively selecting all transitive dependencies`);
extra = new Set(selection.map(workspace => [...workspace.getRecursiveWorkspaceDependencies()]).flat());
extra = new Set((await Promise.all(selection.map(async workspace => {
return [...await getRecursiveWorkspaceDependencies(workspace)];
}))).flat());
}
} else if (this.worktree) {
log(`Option --worktree is set; recursively selecting all nested workspaces`);
Expand Down Expand Up @@ -387,8 +475,8 @@ export default class WorkspacesForeachCommand extends BaseCommand {
: workspace.manifest.dependencies;

for (const descriptor of resolvedSet.values()) {
const workspace = project.tryWorkspaceByDescriptor(descriptor);
isRunnable = workspace === null || !needsProcessing.has(workspace.anchoredLocator.locatorHash);
const dependencyWorkspace = await tryWorkspaceByDescriptor(workspace, descriptor);
isRunnable = dependencyWorkspace === null || !needsProcessing.has(dependencyWorkspace.anchoredLocator.locatorHash);

if (!isRunnable) {
break;
Expand Down
Loading