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

Automatic hoisting of common dependencies #507

Merged
merged 9 commits into from
Jan 31, 2017
Merged

Conversation

gigabo
Copy link
Contributor

@gigabo gigabo commented Jan 18, 2017

The main thing preventing React Server from migrating back to Lerna is the lack of automatic hoisting. When I started trying to port that back from Asini, though, I realized that it was built on top of some other improvements... so here's a stack of changes that culminates with an option to automatically hoist common dependencies to the repo root.

The original PRs from Asini for these are:

There was previously some discussion of hoisting in Lerna here.

When I originally did this for Asini I did some perf testing against Babel. I also had a branch with experimental Yarn support, so I compared them. Hoisting actually provided a bigger perf improvement than Yarn. But the best part is they can be combined. Here are bootstrap times on my laptop for Babel with and without hoisting/Yarn:

              hoist

            no    yes
          +-----+-----+
        no| 30s | 12s |
yarn      |-----+-----+
       yes| 17s |  7s |
          +-----+-----+

@lukebatchelor
Copy link
Contributor

Sweet! Will this be opt-in, or automatic? I recall someone here spiking something like this and having issues, so hoping this wont stop us from upgrading in the future (more hoping it just works and we can have the sweet speed increases)

@Reinmar
Copy link

Reinmar commented Jan 19, 2017

Sounds great! I guess that this would help a lot with #505.

@Reinmar
Copy link

Reinmar commented Jan 19, 2017

I tested it for CKEditor 5 and the install time shortened from like 5 minutes to 35 seconds :)

@gigabo
Copy link
Contributor Author

gigabo commented Jan 19, 2017

@lukebatchelor It's opt-in with --hoist on the command-line or "hoist" in lerna.json.

Default behavior is unchanged.

The hoist option is boolean with no argument (hoist everything), but it takes an optional glob. There is also a --no-hoist, which takes precedence, so you can easily hoist everything except some subset.

@Reinmar That's awesome! 🚀

@gigabo
Copy link
Contributor Author

gigabo commented Jan 19, 2017

Reviewers please take a look at the original three PRs listed in the description above. There are three distinct change sets here. The main additional enhancement beyond hoisting is exposure of a .getOptions() method on command objects. This provides values from both lerna.json and the command-line. It allows for easy, consistent configuration of all flags at the global, command and invocation levels.

if (rootVersion) {
this.logger.warn(
`"${pkg}" package depends on ${name}@${version}, ` +
`which differs from the hoisted ${name}@${rootVersion}.`
Copy link
Member

Choose a reason for hiding this comment

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

Is this really worth a warning? I can see an argument for warning about devDependencies, but warning for production dependencies seems overwrought.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It means that something you wanted to have hoisted couldn't be hoisted. That's at best going to slow down your bootstrap, and at worst going to cause errors due to multiple instances of a dependency when you expected one to be shared. This warning has saved us a few times in React Server!

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, this warning has helped me out in the past too, especially if you want to have the same dependency version across all packages.


// Get the version required by the repo root (if any).
// If the root doesn't have a dependency on this package then we'll
// install the most common dependency there.
Copy link
Member

Choose a reason for hiding this comment

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

Having used this previously with asini, I'm concerned about how hoisting all packages, even ones not specified in the root package.json, all the way to the root makes future npm commands (ls, outdated, shrinkwrap, etc) basically worthless due to "extraneous package" errors.

I am all for recognizing when the root package.json has a matching version already, and doing the appropriate binary linking if necessary. Could we perhaps install the "common among packages but not in root" dependencies in a sibling of the packages themselves? (In the case of multiple packages locations, the closest common ancestor would suffice)

For example, with the default 'packages/*' glob, any common dependencies not present in the root would install into ./packages/node_modules, thus preserving the usefulness of root npm commands while gaining the benefits of hoisting. Does that sound reasonable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IMO it only really makes sense to put deps in the leaves or the root. Anywhere else is going to either require special configuration or have weird behavior (or both). Would a lerna clean help with the interference with npm commands at the root?

Actually come to think of it I don't think lerna clean will remove anything at the root. 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

@evocateur i experimented with trying to get commands such as npm outdated to work by symlinking the hoisted deps at the root to the node_modules of each package. this caused lots of issues with the way npm treats symlinks. IMO, it wasn't worth it.

one of the other problems i ran into was forgetting to add a dependency to a package's package.json file because it was hoisted from another package and everything worked as expected. i thought of creating a tool for checking these sorts of things, but never got around to it.

@evocateur
Copy link
Member

evocateur commented Jan 24, 2017 via email

@evocateur
Copy link
Member

evocateur commented Jan 24, 2017 via email

@gigabo
Copy link
Contributor Author

gigabo commented Jan 28, 2017

Updated to apply cleanly to current master.

Any 👍 👎 on this? Would be nice to get React Server back onto Lerna.

Copy link
Member

@evocateur evocateur left a comment

Choose a reason for hiding this comment

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

LGTM

If I feel the need for npm outdated or shrinkwrapping, I guess there's always --nohoist...

Copy link
Contributor

@doug-wade doug-wade left a comment

Choose a reason for hiding this comment

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

@gigabo one of the things that I have found frustrating about working with React Server is that asini's hoisting makes it impossible to work on a single module as if it were an npm packags. I find using npm or yarn directly on single node modules makes local development much easier for me, and makes lerna repos more accessible for developers who are used to working on single node modules. For instance, currently in asini if I am in react-server-gulp-module-tagger and run npm run lint, I get an sh: eslint: command not found because eslint is at the project root. I think redfin/react-server#859 is another example.

Personally, I would prefer not to merge this until we've found a way to seed each modules' node_modules/.bin with the tools listed in devDependencies, so that each module in the monorepo still works like a single node module in a propoerly-bootstrapped monorepo. I think you should be able to adapt #357 to such a purpose, or use the utilities we already use for linking internal modules bin scripts.

gigabo and others added 8 commits January 30, 2017 13:27
* Add an option sieve for all commands

Allow config:

- At the top level of `lerna.json`
- In `commands.<command>` in `lerna.json`
- On the command-line

Each of those supercedes previous values.

Example:

```json
{
  "lerna": "x.x.x",
  "version": "1.2.0",
  "ignore": "unrelated-package",
  "commands": {
    "publish": {
      "ignore": "test-package"
    }
  }
}
```

This deprecates:

- `bootstrapConfig.ignore`
- `publishConfig.ignore`

These still work, with the _lowest_ precedence.  Their use triggers a warning.

* Use objectAssign for node 0.10 & 0.12 compatibility

* Factor out lernaJson default empty object

* Slicker legacy option handling courtesy of @rygine

* Just pass options to `buildPackageGraph`

Cleaner interface.

No need to send the whole command object through just for one method.

* Add tests for legacy options

* Add some explanation of the config sieve to the README
Groundwork for pluggable commands.

Ensure that command classes:

- Are named `*Command`
- Are uniquely named
- Extend the `Command` base class
This is taken from the base commit on the "enhanced bootstrap" PR.
* Resurrect @rygine's groundwork for common dep hoisting

This is taken from the base commit on the "enhanced bootstrap" PR.

* Whitespace for legibility

Easier on my old eyes while I'm trying to grok this. :)

* Update a logger statement

This method was renamed while this change set was in flight.

* Always warn on un-shared dep (even if already installed)

* Only install deps in the root if not already satisfied

* Simplify most common version determination

* Simplify getDependenciesToInstall

Mostly just so I'm sure I understand it.

* Pass dependents back from getDependenciesToInstall

We'll need those to link binaries from the root.

* Actually hoist

* Update commentary

* Link binaries for hosted dependencies

* Don't override an explicit root dependency

* Gate hoisting with --hoist and --nohoist options

* Update a test to expect slight change to install order

* Add a test and fix a bug it uncovered :D

Scoped packages weren't hoisted with `--hoist`.

* Add a test for --nohoist

* Prune hoisted deps from package dirs

* Factor out dependent package fetch

* Add some documentation about hoisting

* Root version may have no dependents

If it's depended on by the root package.json and others don't match.

* Only warn about mismatch with root if ther's a root version

* Start root installs first, since they're likely to take longest

* Make tests expect root install first

* Don't include null deps in root install

These will be present when deps are already satisfied at the root.

* Kill a vestigial comment
Ugh.  Botched squash and merge.  Surprised it worked at all.
Who would put a package at the repo root.  That's madness.

This eliminates a race condition that causes sporadic failure to bootstrap
_this repository specifically_ with hoisting.

Will follow up with a PR to actually enable said hoisting.
@gigabo
Copy link
Contributor Author

gigabo commented Jan 30, 2017

I guess there's always --nohoist...

That's the default. Nothing is hoisted without --hoist.

@gigabo
Copy link
Contributor Author

gigabo commented Jan 30, 2017

I would prefer not to merge this until we've found a way to seed each modules' node_modules/.bin

The executable is linked into each dependent package. That is one of the key benefits provided by automatic vs manual hoisting of devDependencies.

This broke recently in Asini (and horked React Server). We haven't gotten to fixing it yet because we're hoping that we'll be able to migrate back to Lerna and halt development on Asini. That regression is not part of this PR.

@doug-wade
Copy link
Contributor

@gigabo OOoooooOOOohhh. Sorry, I misunderstood what was afoot. Lgtm.

getOptions(...objects) {

// Items lower down override items higher up.
return objectAssign(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think an Object.assign here will get you past the appveyor failure

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ooh, nice can we use real Object.assign now! 🤘

@noherczeg
Copy link
Contributor

@doug-wade When should we expect an npm release containing this incredible addition? :)

@doug-wade
Copy link
Contributor

iiuc @hzoo is working on one now. If that is the case, I doubt it'd be more than a couple of days.

@doug-wade
Copy link
Contributor

And it's out! https://twitter.com/left_pad/status/826857968930992128 @noherczeg

DxCx added a commit to DxCx/graphql-server that referenced this pull request Feb 2, 2017
@noherczeg
Copy link
Contributor

@gigabo I'm currently at 2.0.0-beta.36, ran npm run lerna bootstrap --hoist, and nothing changed. Every dependency is kept (dl-ed, and copied) at it's respective package. What am I missing?

@gigabo
Copy link
Contributor Author

gigabo commented Feb 2, 2017

Yeah, looking into it now. The port from Asini seems not to have gone well. :(

@gigabo
Copy link
Contributor Author

gigabo commented Feb 2, 2017

@noherczeg Hmm... actually I tracked down the hoist slowness to a poor interaction with #547. Filed #568 as a patch for that.

That wouldn't affect where things are installed, though. It just makes bootstrap slower than it needs to be.

When you run bootstrap do you see warnings about version mismatches preventing hoisting?

@noherczeg
Copy link
Contributor

@gigabo I'm not seeing a single warning during bootstrap. There might be a misunderstanding btw, what I practically meant is that "hoisting does not work at all". Have I missed some prerequisite, or it should just work if I run npm run lerna bootstrap --hoist?

@gigabo
Copy link
Contributor Author

gigabo commented Feb 2, 2017

I wonder if npm is swallowing your option. You could try terminating npm's options, like:

npm run lerna bootstrap -- --hoist

Or you could try putting "hoist": true into your lerna.json.

@noherczeg
Copy link
Contributor

@gigabo Yes, it was my bad, sorry :(

It does work, thank you!

@lock
Copy link

lock bot commented Dec 27, 2018

This thread has been automatically locked because there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Dec 27, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants