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: Remove devDependencies from list output when environment is production #4092

Merged
merged 4 commits into from
Aug 18, 2017
Merged

Fix: Remove devDependencies from list output when environment is production #4092

merged 4 commits into from
Aug 18, 2017

Conversation

olingern
Copy link
Contributor

@olingern olingern commented Aug 4, 2017

Summary

Fixes: #3788.

This PR loads devDependencies from the manifest, re-builds the patterns package structure, and filters packages if the NODE_ENV is production.

Test plan

Added new tests.

@olingern olingern changed the title Add helper env functions Remove devDependencies from list command when environment is production Aug 4, 2017
@olingern olingern changed the title Remove devDependencies from list command when environment is production Remove devDependencies from list output when environment is production Aug 4, 2017
@BYK
Copy link
Member

BYK commented Aug 4, 2017

Related #4095.

src/constants.js Outdated
}

export function isProduction(env: Env): boolean {
return getEnv(env) === 'production';
Copy link
Member

Choose a reason for hiding this comment

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

You should make 'development and 'production strings constants too ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@BYK So, I looked around the project and noticed 'production' is used elsewhere. I'll add the constants, and maybe open a subsequent PR to update the cases where 'production' is referring to NODE_ENV

export async function run(config: Config, reporter: Reporter, flags: Object, args: Array<string>): Promise<void> {
const lockfile = await Lockfile.fromDirectory(config.lockfileFolder, reporter);
const install = new Install(flags, config, reporter, lockfile);
const {requests: depRequests, patterns, workspaceLayout} = await install.fetchRequestFromCwd();


Copy link
Member

Choose a reason for hiding this comment

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

These extra blank lines would probably fail lint.

await install.resolver.init(depRequests, {
isFlat: install.flags.flat,
isFrozen: install.flags.frozenLockfile,
workspaceLayout,
});

let pkgPatterns = [];
if (isProduction(process.env)) {
Copy link
Member

Choose a reason for hiding this comment

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

I would just use process.env at all times instead of passing this. Also this should probably be a method on config since we also support passing --prod flag. Not sure that flag applies to list but we can make it so?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about that, but it seemed that it would decrease the testability of isProduction

Copy link
Member

Choose a reason for hiding this comment

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

You should be able to mock process.env during tests ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added tests for these. Feels a little weird to store and restore the NODE_ENV instead of injecting, but if there's no reason for other portions of the code to care about process.env, then this makes sense :)

let pkgPatterns = [];
if (isProduction(process.env)) {
const devDeps = getDevDeps(manifest);
pkgPatterns = patterns.filter(pattern => !devDeps.includes(pattern));
Copy link
Member

Choose a reason for hiding this comment

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

I would convert devDeps to a Set for faster look up. Technically, both can be sets since this is a set subtraction but the Set class doesn't have that method unless you use a third-party library.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. This is probably O(n)^n since filter loops over the entire collection and then includes checks the entire collection for existence there, too.

devDeps being a set could get the filtering down to constant access to see if a package is a dependency, but I don't see how converting patterns to a set would benefit this particular piece of code, since its creation would be linear as well. I could be blindly missing something, though :)

Copy link
Member

Choose a reason for hiding this comment

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

Where n is the length of devDeps and m being the length of patterns it should go as follows:

  • O(n) for creating the set
  • O(1) for each look up
  • O(m) for creating the other set
  • O(m) for walking over the set

So I think you're right. Just converting one to a set for faster look up should be the best :)

Copy link
Member

Choose a reason for hiding this comment

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

Oh wait, there's one important nuance tho:

If m > n which usually is the case, then converting m to a set and then going over devDeps (O(n)) and removing items from the set should be faster.

Copy link
Contributor Author

@olingern olingern Aug 5, 2017

Choose a reason for hiding this comment

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

Hmm ...

So, let's say a is our dependencies and b is our dev dependencies.

For production, we're looking for the subset, c, which is found by a - b

In both cases where the length of a > b and the length of b > a you would have to create a set of the largest ( or smallest depending on where you want to take the linear hit ) and then iterate over the other collection to find the subset of a - b.

I think converting devDeps to a set will get us to O(n) + m, which is pretty okay :)

@BYK
Copy link
Member

BYK commented Aug 9, 2017

Well, you broke the build :D

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.

Please fix the build.

@@ -10,3 +10,45 @@ test('getPathKey', () => {
expect(getPathKey('linux', {})).toBe('PATH');
expect(getPathKey('darwin', {})).toBe('PATH');
});

test('NODE_ENV_DEV', () => expect(NODE_ENV_DEV).toBe('development'));
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 think these are useful :)

@olingern
Copy link
Contributor Author

olingern commented Aug 11, 2017

@BYK Build fixed. Was going to test stdout. Let me know what you think:

1.) Install packages from a fixture that has a dev dependency
2.) Run in development. Make sure devDependency is in the output
3.) Run in production. Make sure dev dependency is not in the output

@BYK
Copy link
Member

BYK commented Aug 14, 2017

@olingern that plans sounds good to me. Keep in mind that you can create your own small fixtures using file: protocol so you don't need "real" packages for anything.

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.

I have a few comments but overall looks good.

I can land this as it is but if you want to go back one last round to address my comments, I'm good with that too. Holding the merge until I hear back from you :)

const trees = [makeTree('gulp@3.9.1', {color: 'bold'}), makeTree('gulp-babel@6.1.2', {color: 'bold'})];
test('lists all dependencies when not production', (): Promise<void> => {
const nodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
Copy link
Member

Choose a reason for hiding this comment

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

Mocking isProduction may be the better approach here since that should also be affected by the --production flag on the CLI.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gotcha


rprtr.tree('list', trees);

expect(tree).toEqual(rprtr.getBuffer());
Copy link
Member

Choose a reason for hiding this comment

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

May be instead of constructing the tree manually, you can just do expect(reporter.getBuffer()).toMatchSnapshot()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm new-er to Jest, and just watched @cpojer's talk on Jest's new features. As I understand it, snapshots don't ensure validity, but keep a historical record (snapshot) of the current state. I could see snapshots being useful for quick smoke tests, but I'm curious to how you would use them here.

Copy link
Member

Choose a reason for hiding this comment

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

Here's my reasoning:

If you manually tested this and know this output is correct, we can safely assume that the output should not change unless the behavior changes. Thus, we can store this sample output as a reference and in this output we should/should not have devDependencies based on the prod state. Makes sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If, for some reason, the functionality of how yarn list changed -- .toMatchSnapshot() would catch the error, but it wouldn't give a diff on why / how to fix it (or maybe it would, I'm not privy to snapshot's expected / result output).

Copy link
Member

Choose a reason for hiding this comment

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

but it wouldn't give a diff on why / how to fix it (or maybe it would, I'm not privy to snapshot's expected / result output).

It would :) It'd say "I expected this, and I got this other thing instead. Here's the diff."

@@ -10,3 +10,22 @@ test('getPathKey', () => {
expect(getPathKey('linux', {})).toBe('PATH');
expect(getPathKey('darwin', {})).toBe('PATH');
});

test('isProduction', () => {
Copy link
Member

Choose a reason for hiding this comment

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

Protip: you can have describe('isProduction') at the top that wraps all this and then put each separate scenario in their own test() calls.

Btw. changing globals like process.env.NODE_ENV in tests is a bit dangerous since we run tests in parallel. It may yield to other problems. A no-to-great way is to define isProduction as follows that lets you do dependency injection:

function isProduction(env = process.env) {
    return env.NODE_ENV === 'production';
}

Copy link
Contributor Author

@olingern olingern Aug 14, 2017

Choose a reason for hiding this comment

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

Yah, wasn't sure of the overall test structure / style. It seemed that mosts tests are at parent level to each file, so I didn't know if nesting tests inside of a describe was good or bad style here.

Btw. changing globals like process.env.NODE_ENV in tests is a bit dangerous since we run tests in parallel

Agreed. Wasn't sure of how to successfully mock process.env.NODE_ENV, but your advice above sorts that out :)

Copy link
Member

Choose a reason for hiding this comment

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

It seemed that mosts tests are at parent level to each file, so I didn't know if nesting tests inside of a describe was good or bad style here.

Yeah, that's something I want to address soon. Apologies for the confusion :(

@@ -0,0 +1,8 @@
{
"dependencies": {
"left-pad": "^1.1.3"
Copy link
Member

Choose a reason for hiding this comment

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

Instead of relying on "real" dependencies, you can just construct local packages which may simplify and speed up things.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmmm ... could you point me to a test that follows that pattern? I was basing most of my structure off of the no-args tests / fixtures

Copy link
Member

Choose a reason for hiding this comment

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

@@ -180,21 +181,36 @@ export function filterTree(tree: Tree, filters: Array<string>): boolean {
return notDim && (found || hasChildren);
}

export function getDevDeps(manifest: Object): Set<string> {
const devDepSet = new Set();
Object.keys(manifest.devDependencies).forEach(key => devDepSet.add(`${key}@${manifest.devDependencies[key]}`));
Copy link
Member

Choose a reason for hiding this comment

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

Even better:

return new Set(Object.keys(manifest.devDependencies).map(key => `${key}@${manifest.devDependencies[key]}`));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice, will amend

await install.resolver.init(depRequests, {
isFlat: install.flags.flat,
isFrozen: install.flags.frozenLockfile,
workspaceLayout,
});

let pkgPatterns = [];
Copy link
Member

Choose a reason for hiding this comment

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

May be a more explanatory name than pkgPatterns? May be usedPatterns or activePatterns?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. activePatterns has a nice "ring" to it.

@BYK
Copy link
Member

BYK commented Aug 14, 2017

@olingern ping me when you push the final update then!

@olingern
Copy link
Contributor Author

@BYK I have couple questions / comments, but will get around to making all the fixes later today. 🎉

load devDeps from manifest and filter if production

add constants for node env and dont pass in env to getEnv

add tests for NODE_ENV_DEV, NODE_ENV_PROD, getEnv, and isProduction

remove extra line break

Test list output dependent upon environment

rename pkgPatterns

update list test with snapshot and use local pkg
@olingern
Copy link
Contributor Author

@BYK Made most of the updates, but couldn't find a way to mock isProduction via requiring the constants file.

Seems like there may be an answer here, but I'll have to take a look at it tomorrow.

@BYK
Copy link
Member

BYK commented Aug 15, 2017

Seems like there may be an answer here, but I'll have to take a look at it tomorrow.

That seems like the right place for me :) Seems like you have a few lint issues (yarn lint to see them locally). Fix them and we can merge the patch!

Thanks a lot for all the follow-ups, very much appreciated!

@olingern
Copy link
Contributor Author

@arcanis I can make sure linting / tests pass today

@arcanis
Copy link
Member

arcanis commented Aug 17, 2017

That would be great 😃 I tried running prettier, but unfortunately some Flow errors remain

@olingern
Copy link
Contributor Author

@BYK @arcanis linting fixed, tests updated

@BYK BYK changed the title Remove devDependencies from list output when environment is production Fix: Remove devDependencies from list output when environment is production Aug 18, 2017
@BYK BYK merged commit 2952f3c into yarnpkg:master Aug 18, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants