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
7 changes: 7 additions & 0 deletions .yarn/versions/5888d405.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
releases:
"@yarnpkg/plugin-essentials": minor

declined:
- "@yarnpkg/plugin-interactive-tools"
- "@yarnpkg/plugin-typescript"
- "@yarnpkg/cli"
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import {PortablePath, npath, xfs} from '@yarnpkg/fslib';
import {stringifySyml} from '@yarnpkg/parsers';

describe(`Features`, () => {
describe(`Before Hooks`, () => {
describe(`beforeWorkspaceDependencyAddition`, () => {
test(
`it should allow plugins to modify descriptor before addition`,
makeTemporaryEnv({}, async ({path, run}) => {
const pluginPath = npath.toPortablePath(require.resolve(`@yarnpkg/monorepo/scripts/plugin-before-hooks-test.js`));
const pluginContent = await xfs.readFilePromise(pluginPath);
await xfs.writeFilePromise(`${path}/plugin-before-hooks.js` as PortablePath, pluginContent);

await xfs.writeFilePromise(`${path}/.yarnrc.yml` as PortablePath, stringifySyml({
plugins: [`./plugin-before-hooks.js`],
}));

await run(`add`, `no-deps`);

await expect(xfs.readJsonPromise(`${path}/package.json` as PortablePath)).resolves.toMatchObject({
dependencies: {
[`no-deps`]: `^1.0.0`,
},
});
}),
);

test(
`it should work with dev dependencies`,
makeTemporaryEnv({}, async ({path, run}) => {
const pluginPath = npath.toPortablePath(require.resolve(`@yarnpkg/monorepo/scripts/plugin-before-hooks-test.js`));
const pluginContent = await xfs.readFilePromise(pluginPath);
await xfs.writeFilePromise(`${path}/plugin-before-hooks.js` as PortablePath, pluginContent);

await xfs.writeFilePromise(`${path}/.yarnrc.yml` as PortablePath, stringifySyml({
plugins: [`./plugin-before-hooks.js`],
}));

await run(`add`, `no-deps`, `-D`);

await expect(xfs.readJsonPromise(`${path}/package.json` as PortablePath)).resolves.toMatchObject({
devDependencies: {
[`no-deps`]: `^1.0.0`,
},
});
}),
);

test(
`it should not affect other packages`,
makeTemporaryEnv({}, async ({path, run}) => {
const pluginPath = npath.toPortablePath(require.resolve(`@yarnpkg/monorepo/scripts/plugin-before-hooks-test.js`));
const pluginContent = await xfs.readFilePromise(pluginPath);
await xfs.writeFilePromise(`${path}/plugin-before-hooks.js` as PortablePath, pluginContent);

await xfs.writeFilePromise(`${path}/.yarnrc.yml` as PortablePath, stringifySyml({
plugins: [`./plugin-before-hooks.js`],
}));

await run(`add`, `one-fixed-dep`);

await expect(xfs.readJsonPromise(`${path}/package.json` as PortablePath)).resolves.toMatchObject({
dependencies: {
[`one-fixed-dep`]: `^2.0.0`,
},
});
}),
);
});

describe(`beforeWorkspaceDependencyReplacement`, () => {
test(
`it should allow plugins to modify descriptor before replacement via yarn add`,
makeTemporaryEnv(
{
dependencies: {
'no-deps': `^1.0.0`,
},
},
async ({path, run}) => {
const pluginPath = npath.toPortablePath(require.resolve(`@yarnpkg/monorepo/scripts/plugin-before-hooks-test.js`));
const pluginContent = await xfs.readFilePromise(pluginPath);
await xfs.writeFilePromise(`${path}/plugin-before-hooks.js` as PortablePath, pluginContent);

await xfs.writeFilePromise(`${path}/.yarnrc.yml` as PortablePath, stringifySyml({
plugins: [`./plugin-before-hooks.js`],
}));

await run(`add`, `no-deps@^1.5.0`);

await expect(xfs.readJsonPromise(`${path}/package.json` as PortablePath)).resolves.toMatchObject({
dependencies: {
[`no-deps`]: `^2.0.0`,
},
});
},
),
);

test(
`it should allow plugins to modify descriptor before replacement via yarn up`,
makeTemporaryEnv(
{
dependencies: {
'no-deps': `^1.0.0`,
},
},
async ({path, run}) => {
const pluginPath = npath.toPortablePath(require.resolve(`@yarnpkg/monorepo/scripts/plugin-before-hooks-test.js`));
const pluginContent = await xfs.readFilePromise(pluginPath);
await xfs.writeFilePromise(`${path}/plugin-before-hooks.js` as PortablePath, pluginContent);

await xfs.writeFilePromise(`${path}/.yarnrc.yml` as PortablePath, stringifySyml({
plugins: [`./plugin-before-hooks.js`],
}));

await run(`up`, `no-deps`);

await expect(xfs.readJsonPromise(`${path}/package.json` as PortablePath)).resolves.toMatchObject({
dependencies: {
[`no-deps`]: `^2.0.0`,
},
});
},
),
);
});

describe(`beforeWorkspaceDependencyRemoval`, () => {
test(
`it should allow plugins to block removal by throwing`,
makeTemporaryEnv(
{
dependencies: {
'no-deps': `^2.0.0`,
},
},
async ({path, run}) => {
const pluginPath = npath.toPortablePath(require.resolve(`@yarnpkg/monorepo/scripts/plugin-before-hooks-test.js`));
const pluginContent = await xfs.readFilePromise(pluginPath);
await xfs.writeFilePromise(`${path}/plugin-before-hooks.js` as PortablePath, pluginContent);

await xfs.writeFilePromise(`${path}/.yarnrc.yml` as PortablePath, stringifySyml({
plugins: [`./plugin-before-hooks.js`],
}));

await expect(run(`remove`, `no-deps`)).rejects.toThrow();

await expect(xfs.readJsonPromise(`${path}/package.json` as PortablePath)).resolves.toMatchObject({
dependencies: {
'no-deps': `^2.0.0`,
},
});
},
),
);

test(
`it should allow removal of other packages`,
makeTemporaryEnv(
{
dependencies: {
'one-fixed-dep': `^1.0.0`,
},
},
async ({path, run}) => {
const pluginPath = npath.toPortablePath(require.resolve(`@yarnpkg/monorepo/scripts/plugin-before-hooks-test.js`));
const pluginContent = await xfs.readFilePromise(pluginPath);
await xfs.writeFilePromise(`${path}/plugin-before-hooks.js` as PortablePath, pluginContent);

await xfs.writeFilePromise(`${path}/.yarnrc.yml` as PortablePath, stringifySyml({
plugins: [`./plugin-before-hooks.js`],
}));

await run(`remove`, `one-fixed-dep`);

const manifest = await xfs.readJsonPromise(`${path}/package.json` as PortablePath);
expect(manifest.dependencies).toBeUndefined();
},
),
);
});

describe(`Multiple hooks interaction`, () => {
test(
`it should handle both addition and replacement hooks in yarn add`,
makeTemporaryEnv(
{
dependencies: {
'no-deps': `^1.0.0`,
},
},
async ({path, run}) => {
const pluginPath = npath.toPortablePath(require.resolve(`@yarnpkg/monorepo/scripts/plugin-before-hooks-test.js`));
const pluginContent = await xfs.readFilePromise(pluginPath);
await xfs.writeFilePromise(`${path}/plugin-before-hooks.js` as PortablePath, pluginContent);

await xfs.writeFilePromise(`${path}/.yarnrc.yml` as PortablePath, stringifySyml({
plugins: [`./plugin-before-hooks.js`],
}));

await run(`add`, `no-deps@^1.5.0`);

await expect(xfs.readJsonPromise(`${path}/package.json` as PortablePath)).resolves.toMatchObject({
dependencies: {
[`no-deps`]: `^2.0.0`,
},
});

await run(`add`, `one-fixed-dep`);

await expect(xfs.readJsonPromise(`${path}/package.json` as PortablePath)).resolves.toMatchObject({
dependencies: {
[`no-deps`]: `^2.0.0`,
[`one-fixed-dep`]: expect.any(String),
},
});
},
),
);
});
});
});
28 changes: 28 additions & 0 deletions packages/plugin-essentials/sources/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,34 @@ export default class AddCommand extends BaseCommand {
});

const results = await Promise.all(targetList.map(async target => {
const current = workspace.manifest[target].get(request.identHash);
const isReplacement = typeof current !== `undefined`;

for (const plugin of configuration.plugins.values()) {
const hooks = plugin.hooks as Hooks;

if (isReplacement) {
if (!hooks?.beforeWorkspaceDependencyReplacement)
continue;

await hooks.beforeWorkspaceDependencyReplacement(
workspace,
target,
current,
request,
);
} else {
if (!hooks?.beforeWorkspaceDependencyAddition)
continue;

await hooks.beforeWorkspaceDependencyAddition(
workspace,
target,
request,
);
}
}

const suggestedDescriptors = await suggestUtils.getSuggestedDescriptors(request, {project, workspace, cache, fixed, target, modifier, strategies, maxResults});
return {request, suggestedDescriptors, target};
}));
Expand Down
14 changes: 13 additions & 1 deletion packages/plugin-essentials/sources/commands/remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,19 @@ export default class RemoveCommand extends BaseCommand {
if (typeof removedDescriptor === `undefined`)
throw new Error(`Assertion failed: Expected the descriptor to be registered`);

workspace.manifest[target].delete(identHash);
for (const plugin of configuration.plugins.values()) {
const hooks = plugin.hooks as Hooks;
if (!hooks?.beforeWorkspaceDependencyRemoval)
continue;

await hooks.beforeWorkspaceDependencyRemoval(
workspace,
target,
removedDescriptor,
);
}

workspace.manifest[target].delete(removedDescriptor.identHash);

afterWorkspaceDependencyRemovalList.push([
workspace,
Expand Down
13 changes: 13 additions & 0 deletions packages/plugin-essentials/sources/commands/up.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,19 @@ export default class UpCommand extends BaseCommand {
const request = structUtils.makeDescriptor(ident, pseudoDescriptor.range);

allSuggestionsPromises.push(Promise.resolve().then(async () => {
for (const plugin of configuration.plugins.values()) {
const hooks = plugin.hooks as Hooks;
if (!hooks?.beforeWorkspaceDependencyReplacement)
continue;

await hooks.beforeWorkspaceDependencyReplacement(
workspace,
target,
existingDescriptor,
request,
);
}

return [
workspace,
target,
Expand Down
37 changes: 37 additions & 0 deletions packages/plugin-essentials/sources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,43 @@ export interface Hooks {
descriptor: Descriptor,
) => Promise<void>;

/**
* Called before a new dependency is added to a workspace. Note that this
* hook is only called by the CLI commands like `yarn add` - manually adding
* the dependencies into the manifest and running `yarn install` won't
* trigger it.
*/
beforeWorkspaceDependencyAddition?: (
workspace: Workspace,
target: suggestUtils.Target,
descriptor: Descriptor,
) => Promise<void>;

/**
* Called before a dependency range is replaced inside a workspace. Note that
* this hook is only called by the CLI commands like `yarn add` or `yarn up` -
* manually updating the dependencies from the manifest and running
* `yarn install` won't trigger it.
*/
beforeWorkspaceDependencyReplacement?: (
workspace: Workspace,
target: suggestUtils.Target,
fromDescriptor: Descriptor,
toDescriptor: Descriptor,
) => Promise<void>;

/**
* Called before a dependency range is removed from a workspace. Note that
* this hook is only called by the CLI commands like `yarn remove` - manually
* removing the dependencies from the manifest and running `yarn install`
* won't trigger it.
*/
beforeWorkspaceDependencyRemoval?: (
workspace: Workspace,
target: suggestUtils.Target,
descriptor: Descriptor,
) => Promise<void>;

/**
* Called by `yarn info`. The `extra` field is the set of parameters passed
* to the `-X,--extra` flag. Calling `registerData` will add a new set of
Expand Down
26 changes: 26 additions & 0 deletions scripts/plugin-before-hooks-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module.exports = {
name: `@yarnpkg/plugin-before-hooks-test`,
factory: () => {
return {
hooks: {
beforeWorkspaceDependencyAddition: async (workspace, target, descriptor) => {
if (descriptor.name === `no-deps`) {
descriptor.range = `^1.0.0`;
}
},

beforeWorkspaceDependencyReplacement: async (workspace, target, fromDescriptor, toDescriptor) => {
if (toDescriptor.name === `no-deps`) {
toDescriptor.range = `^2.0.0`;
}
},

beforeWorkspaceDependencyRemoval: async (workspace, target, descriptor) => {
if (descriptor.name === `no-deps`) {
throw new Error(`Cannot remove no-deps - it is protected`);
}
},
},
};
},
};