diff --git a/.yarn/versions/21659536.yml b/.yarn/versions/21659536.yml new file mode 100644 index 000000000000..54642a287609 --- /dev/null +++ b/.yarn/versions/21659536.yml @@ -0,0 +1,23 @@ +releases: + "@yarnpkg/cli": minor + "@yarnpkg/plugin-essentials": minor + +declined: + - "@yarnpkg/plugin-compat" + - "@yarnpkg/plugin-constraints" + - "@yarnpkg/plugin-dlx" + - "@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/plugin-workspace-tools" + - "@yarnpkg/builder" + - "@yarnpkg/core" + - "@yarnpkg/doctor" diff --git a/packages/acceptance-tests/pkg-tests-specs/sources/commands/link.test.js b/packages/acceptance-tests/pkg-tests-specs/sources/commands/link.test.js index fd7755e477e2..b62edc194d4d 100644 --- a/packages/acceptance-tests/pkg-tests-specs/sources/commands/link.test.js +++ b/packages/acceptance-tests/pkg-tests-specs/sources/commands/link.test.js @@ -205,5 +205,41 @@ describe(`Commands`, () => { }, ), ); + + test( + `it should allow linking multiple workspaces`, + makeTemporaryEnv({}, async ({path, run, source}) => { + const tmp = await createTemporaryFolder(); + + await writeJson(`${tmp}/my-workspace/package.json`, { + private: true, + workspaces: [`packages/*`], + }); + + await writeJson(`${tmp}/my-workspace/packages/workspace-a/package.json`, { + name: `workspace-a`, + }); + + await writeJson(`${tmp}/my-workspace/packages/workspace-b/package.json`, { + name: `workspace-b`, + }); + + await writeJson(`${tmp}/my-workspace/packages/workspace-c/package.json`, { + name: `workspace-c`, + }); + + await run(`link`, `${tmp}/my-workspace/packages/workspace-b`, `${tmp}/my-workspace/packages/workspace-c`); + + const manifest = await readJson(`${path}/package.json`); + + expect(manifest.resolutions).not.toHaveProperty(`workspace-a`); + await expect(manifest).toMatchObject({ + resolutions: { + [`workspace-b`]: `portal:${npath.toPortablePath(`${tmp}/my-workspace/packages/workspace-b`)}`, + [`workspace-c`]: `portal:${npath.toPortablePath(`${tmp}/my-workspace/packages/workspace-c`)}`, + }, + }); + }), + ); }); }); diff --git a/packages/plugin-essentials/sources/commands/link.ts b/packages/plugin-essentials/sources/commands/link.ts index d77df621a5cf..fe250d58e0cf 100644 --- a/packages/plugin-essentials/sources/commands/link.ts +++ b/packages/plugin-essentials/sources/commands/link.ts @@ -15,8 +15,8 @@ export default class LinkCommand extends BaseCommand { This command will set a new \`resolutions\` field in the project-level manifest and point it to the workspace at the specified location (even if part of another project). `, examples: [[ - `Register a remote workspace for use in the current project`, - `$0 link ~/ts-loader`, + `Register one or more remote workspaces for use in the current project`, + `$0 link ~/ts-loader ~/jest`, ], [ `Register all workspaces from a remote project for use in the current project`, `$0 link ~/jest --all`, @@ -24,18 +24,18 @@ export default class LinkCommand extends BaseCommand { }); all = Option.Boolean(`-A,--all`, false, { - description: `Link all workspaces belonging to the target project to the current one`, + description: `Link all workspaces belonging to the target projects to the current one`, }); private = Option.Boolean(`-p,--private`, false, { - description: `Also link private workspaces belonging to the target project to the current one`, + description: `Also link private workspaces belonging to the target projects to the current one`, }); relative = Option.Boolean(`-r,--relative`, false, { description: `Link workspaces using relative paths instead of absolute paths`, }); - destination = Option.String(); + destinations = Option.Rest(); async execute() { const configuration = await Configuration.find(this.context.cwd, this.context.plugins); @@ -49,36 +49,42 @@ export default class LinkCommand extends BaseCommand { restoreResolutions: false, }); - const absoluteDestination = ppath.resolve(this.context.cwd, npath.toPortablePath(this.destination)); + const topLevelWorkspace = project.topLevelWorkspace; + const linkedWorkspaces = []; - const configuration2 = await Configuration.find(absoluteDestination, this.context.plugins, {useRc: false, strict: false}); - const {project: project2, workspace: workspace2} = await Project.find(configuration2, absoluteDestination); + for (const destination of this.destinations) { + const absoluteDestination = ppath.resolve(this.context.cwd, npath.toPortablePath(destination)); - if (project.cwd === project2.cwd) - throw new UsageError(`Invalid destination; Can't link the project to itself`); + const configuration2 = await Configuration.find(absoluteDestination, this.context.plugins, {useRc: false, strict: false}); + const {project: project2, workspace: workspace2} = await Project.find(configuration2, absoluteDestination); - if (!workspace2) - throw new WorkspaceRequiredError(project2.cwd, absoluteDestination); + if (project.cwd === project2.cwd) + throw new UsageError(`Invalid destination '${destination}'; Can't link the project to itself`); - const topLevelWorkspace = project.topLevelWorkspace; - const linkedWorkspaces = []; + if (!workspace2) + throw new WorkspaceRequiredError(project2.cwd, absoluteDestination); - if (this.all) { - for (const workspace of project2.workspaces) - if (workspace.manifest.name && (!workspace.manifest.private || this.private)) - linkedWorkspaces.push(workspace); + if (this.all) { + let found = false; + for (const workspace of project2.workspaces) { + if (workspace.manifest.name && (!workspace.manifest.private || this.private)) { + linkedWorkspaces.push(workspace); + found = true; + } + } - if (linkedWorkspaces.length === 0) { - throw new UsageError(`No workspace found to be linked in the target project`); - } - } else { - if (!workspace2.manifest.name) - throw new UsageError(`The target workspace doesn't have a name and thus cannot be linked`); + if (!found) { + throw new UsageError(`No workspace found to be linked in the target project: ${destination}`); + } + } else { + if (!workspace2.manifest.name) + throw new UsageError(`The target workspace at '${destination}' doesn't have a name and thus cannot be linked`); - if (workspace2.manifest.private && !this.private) - throw new UsageError(`The target workspace is marked private - use the --private flag to link it anyway`); + if (workspace2.manifest.private && !this.private) + throw new UsageError(`The target workspace at '${destination}' is marked private - use the --private flag to link it anyway`); - linkedWorkspaces.push(workspace2); + linkedWorkspaces.push(workspace2); + } } for (const workspace of linkedWorkspaces) {