Skip to content

Commit

Permalink
feat(plugin-essentials): support multiple workspaces in yarn link com…
Browse files Browse the repository at this point in the history
…mand (#4573)
  • Loading branch information
Noah authored Jun 24, 2022
1 parent 82a5ec4 commit 2ba6e59
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 27 deletions.
23 changes: 23 additions & 0 deletions .yarn/versions/21659536.yml
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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`)}`,
},
});
}),
);
});
});
60 changes: 33 additions & 27 deletions packages/plugin-essentials/sources/commands/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,27 @@ 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`,
]],
});

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);
Expand All @@ -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) {
Expand Down

0 comments on commit 2ba6e59

Please sign in to comment.