Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(linking): Handle missing package when using --ignore-optional #5059

Merged
merged 8 commits into from Mar 7, 2018

Conversation

kimjoar
Copy link
Contributor

@kimjoar kimjoar commented Dec 8, 2017

Summary

Closes #5054, closes #4876, closes #5152.

Currently when running yarn --ignore-optional required dependencies can be marked as optional because they exist in the tree of optionalDependencies of one of the dependencies (but it's required by some other non-optional dependency).

For example: once depends on wrappy, but wrappy is also in the chain from fsevents (glob -> inflight -> wrappy) which is an optional dependency of chokidar. So if the first wrappy comes from an "optional chain" when yarn processes it, it ends up being marked as _reference.optional = true, and the package hoister therefore doesn't mark it as required.

When running the released version of yarn with https://github.com/spalger/reproduce-issues/blob/master/yarn-ignores-non-optional-dependencies you'll see

~/dev/reproduce-issues/yarn-ignores-non-optional-dependencies
$ yarn --ignore-optional
yarn install v1.3.2-20171204.1856
[1/4] πŸ”  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] πŸ”—  Linking dependencies...
[4/4] πŸ“ƒ  Building fresh packages...
✨  Done in 0.17s.

~/dev/reproduce-issues/yarn-ignores-non-optional-dependencies
$ yarn check --verify-tree
yarn check v1.3.2-20171204.1856
error "once#wrappy" not installed
error An unexpected error occurred: "Found 1 errors.".

This fix checks whether or not the parent is marked as required, and if so, marking the dependency as required unless it's listed in the parent's optionalDependencies.

Test plan

I tested this implementation with both https://github.com/spalger/reproduce-issues/blob/master/yarn-ignores-non-optional-dependencies and #4876, and in both cases yarn check --verify-tree succeeded after running yarn --ignore-optional.

Also added automated tests.

@kaylie-alexa
Copy link
Member

Woot! Thanks @kjbekkelund, do you feel comfortable trying to resolve this bug? Otherwise I can take a look it. Let me know :)

@kimjoar
Copy link
Contributor Author

kimjoar commented Dec 8, 2017

@kaylieEB I've been exploring a bit, but I haven't had much progress yet. Would be great if you could take a look.

@kimjoar
Copy link
Contributor Author

kimjoar commented Dec 8, 2017

I think it's related to resolveToExistingVersion. In this case, when it resolves once to the one that already exists it switches optional from true to false, but it doesn't walk its dependencies.

So it feels like there's a need to walk the dependencies in addOptional (

this.optional = false;
) and switch it for them too.

Thoughts?

@kimjoar
Copy link
Contributor Author

kimjoar commented Dec 8, 2017

Ah, or maybe _propagateRequired is the location for a fix?

_propagateRequired() {

However, that depends on that _reference.optional value again, which is true instead of false here for wrappy

@buildsize
Copy link

buildsize bot commented Dec 9, 2017

This change will increase the build size from 10.51 MB to 10.51 MB, an increase of 762 bytes (0%)

File name Previous Size New Size Change
yarn-[version].noarch.rpm 910.44 KB 910.48 KB 38 bytes (0%)
yarn-[version].js 3.96 MB 3.96 MB 354 bytes (0%)
yarn-legacy-[version].js 4.11 MB 4.11 MB 352 bytes (0%)
yarn-v[version].tar.gz 915.84 KB 916.06 KB 226 bytes (0%)
yarn_[version]all.deb 676.22 KB 676.02 KB -208 bytes (0%)

@kimjoar
Copy link
Contributor Author

kimjoar commented Dec 9, 2017

@kaylieEB Looks like I got it running in the end.

The problem is that you need to know if it was listed in optionalDependencies when deciding whether or not to make them required. To fix this I re-used the hint field, which is already used in this way in e.g.

pushDeps('dependencies', projectManifestJson, {hint: null, optional: false}, true);
pushDeps('devDependencies', projectManifestJson, {hint: 'dev', optional: false}, !this.config.production);
pushDeps('optionalDependencies', projectManifestJson, {hint: 'optional', optional: true}, true);

Let me know if there's a better way to handle this.

@kimjoar kimjoar changed the title Test that fails because of missing deps after install Handle missing package when using --ignore-optional Dec 10, 2017
@kimjoar kimjoar force-pushed the bug/ignoring-too-many-deps branch 2 times, most recently from 16eee4d to 6437382 Compare December 10, 2017 19:24
@kimjoar
Copy link
Contributor Author

kimjoar commented Dec 10, 2017

With the latest fix, this should also handle #4876

Copy link
Member

@BYK BYK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good, thank you!

I've added some comments and curious what you think about those suggestions.

const isMarkedAsOptional = depinfo.pkg._reference && depinfo.pkg._reference.optional && this.ignoreOptional;
const depRef = depinfo.pkg._reference;

let isMarkedAsOptional = depRef && depRef.optional && this.ignoreOptional;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to reduce this to:

let isMarkedAsOptional = depRef && depRef.optional && this.ignoreOptional && !(info.isRequired && depRef.hint !== 'optional');

What do you think? The comment you added should stay though, it is great!

@@ -52,6 +53,7 @@ export default class PackageRequest {
config: Config;
registry: ResolverRegistryNames;
optional: boolean;
hint: ?string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind changing this to an enum both to avoid unnecessary string usage all over the app and to avoid potential typing errors.

My suggestion is to add something like the following into constants.js and use that:

const REQUEST_HINTS = Object.freeze({
    dev: Symbol('REQUEST_HINT_DEV'),
    optional: Symbol('REQUEST_HINT_OPTIONAL'),
});

// If it's marked as optional, but the parent is required and the
// dependency was not listed in `optionalDependencies`, then we mark the
// dependency as required.
if (isMarkedAsOptional && info.isRequired && depRef && depRef.hint !== 'optional') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not use depRef.isRequired instead of this? Or may be add depRef.isOptional?

Copy link
Contributor Author

@kimjoar kimjoar Dec 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that at first, but it fails with "deeper optionals". E.g.

foo (in `optionalDependencies`) -> bar -> baz
quux -> bar -> baz

Here both bar and baz will have _reference.isOptional === true because of foo. However, as this code doesn't mark the reference when performing this logic (only setting depinfo.isRequired = true), baz will stay optional as bar._reference.optional === true. If we changed the logic to also do a depRef.setOptional(false) in addition to setting depinfo.isRequired this approach would work. That was my initial fix, but I changed to this approach as I wasn't sure about doing that work from the package hoister. Thoughts?

@kimjoar
Copy link
Contributor Author

kimjoar commented Dec 14, 2017

@BYK I ended up doing:

export type RequestHint = 'dev' | 'optional' | 'resolution' | 'workspaces';

as this field seems to be used for a couple things. That's likely a sign this should be cleaned up(?) Given

const getNameFromHint = hint => (hint ? `${hint}Dependencies` : 'dependencies');
it definitely feels like the intention was to codify what type of dependency it is.

Thoughts? I'd of course prefer to not extend the scope of this PR ;)

@kimjoar
Copy link
Contributor Author

kimjoar commented Jan 4, 2018

@BYK Just back from my holidays, so I thought I'd ping you about my suggestion above.

@BYK
Copy link
Member

BYK commented Jan 5, 2018

@kjbekkelund I think the PR is fine but I have realized this is masking another issue which makes this part of the code as complex as it is right now which is marking all transient dependencies of optional dependencies as optional too, opening the door for potentially corrupt installations for optional dependencies.

Also we apparently already have a test case for this behavior:

test.concurrent('a sub-dependency should be non-optional if any parents mark it non-optional', async (): Promise<
void,
> => {
let thrown = false;
try {
await runInstall({}, 'failing-sub-dep-optional-and-normal', () => {});
} catch (err) {
thrown = true;
expect(err.message).toContain('sub-failing: Command failed');
}
expect(thrown).toEqual(true);
});

May be instead of adding all these new files, you can fix that test to cover transient dependencies (or just extend it).

What do you think?

tomdale added a commit to glimmerjs/glimmer-blueprint that referenced this pull request Feb 2, 2018
Ember CLI detects the yarn.lock file to decide whether to use Yarn or not. Pending yarnpkg/yarn#5059, we should switch to npm by default.
tomdale added a commit to glimmerjs/glimmer.js that referenced this pull request Feb 6, 2018
Ember CLI detects the yarn.lock file to decide whether to use Yarn or not. Pending yarnpkg/yarn#5059, we should switch to npm by default.
@kimjoar
Copy link
Contributor Author

kimjoar commented Mar 2, 2018

@BYK Finally had some time to get this done. Are you okey with this approach? Is the fs.readdir in the test okey, or do you want me to list separate fs.exists assertions? (as this ends up mentioning the integrity file)

Copy link
Member

@BYK BYK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! This new version looks solid. I think fs.readdir approach is actually neat and makes it more readable but I'll see what @arcanis thinks before merging.

Thanks a lot for coming back and pushing this forward! ❀️

const pushDeps = (
depType,
manifest: Object,
{hint, optional}: {hint: ?constants.RequestHint, optional: boolean},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why make hint optional here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of

pushDeps('dependencies', projectManifestJson, {hint: null, optional: false}, true);

Not sure if constants.RequestHint | null or ?constants.RequestHint is preferred(?) Or maybe add a production hint and always require hint?

@@ -124,3 +124,5 @@ export const VERSION_COLOR_SCHEME: {[key: string]: VersionColor} = {
};

export type VersionColor = 'red' | 'yellow' | 'green' | 'white';

export type RequestHint = 'dev' | 'optional' | 'resolution' | 'workspaces';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't see other hints being used anywhere else. Do we need anything other than 'dev' and 'optional' at the moment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolution is used in:

resolutionDeps = [...resolutionDeps, {registry, pattern, optional: false, hint: 'resolution'}];

and workspaces in:

pushDeps('workspaces', {workspaces: virtualDep}, {hint: 'workspaces', optional: false}, true);

@BYK
Copy link
Member

BYK commented Mar 7, 2018

I'll merge this since it fixes an important issue but I'd love to get answers to my final comments and maybe get a follow-up to address them.

@BYK BYK changed the title Handle missing package when using --ignore-optional fix(linking): Handle missing package when using --ignore-optional Mar 7, 2018
@BYK BYK merged commit 95b3dfe into yarnpkg:master Mar 7, 2018
jasongrout added a commit to jasongrout/jupyterlab that referenced this pull request Apr 27, 2018
This option is apparently fixed in yarn 1.6.0: yarnpkg/yarn#5059
jasongrout added a commit to jasongrout/jupyterlab that referenced this pull request Apr 27, 2018
This option is apparently fixed in yarn 1.6.0: yarnpkg/yarn#5059
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants