Skip to content

Commit

Permalink
Merge pull request #507 from gigabo/hoist
Browse files Browse the repository at this point in the history
Automatic hoisting of common dependencies
  • Loading branch information
doug-wade committed Jan 31, 2017
2 parents 0b016de + 84b81ab commit 6e9bf13
Show file tree
Hide file tree
Showing 19 changed files with 697 additions and 95 deletions.
83 changes: 69 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,10 @@ Check which `packages` have changed since the last release (the last git tag).

Lerna determines the last git tag created and runs `git diff --name-only v6.8.1` to get all files changed since that tag. It then returns an array of packages that have an updated file.


**Note that configuration for the `publish` command _also_ affects the
`updated` command. For example `config.publish.ignore`**

### clean

```sh
Expand Down Expand Up @@ -444,24 +448,26 @@ Running `lerna` without arguments will show all commands/options.
{
"lerna": "2.0.0-beta.31",
"version": "1.1.3",
"publishConfig": {
"ignore": [
"ignored-file",
"*.md"
]
},
"bootstrapConfig": {
"ignore": "component-*"
"commands": {
"publish": {
"ignore": [
"ignored-file",
"*.md"
]
},
"bootstrap": {
"ignore": "component-*"
}
},
"packages": ["packages/*"]
}
```

- `lerna`: the current version of Lerna being used.
- `version`: the current version of the repository.
- `publishConfig.ignore`: an array of globs that won't be included in `lerna updated/publish`. Use this to prevent publishing a new version unnecessarily for changes, such as fixing a `README.md` typo.
- `bootstrapConfig.ignore`: an glob that won't be bootstrapped when running the `lerna bootstrap` command.
- `bootstrapConfig.scope`: an glob that restricts which packages will be bootstrapped when running the `lerna bootstrap` command.
- `commands.publish.ignore`: an array of globs that won't be included in `lerna updated/publish`. Use this to prevent publishing a new version unnecessarily for changes, such as fixing a `README.md` typo.
- `commands.bootstrap.ignore`: an array of globs that won't be bootstrapped when running the `lerna bootstrap` command.
- `commands.bootstrap.scope`: an array of globs that restricts which packages will be bootstrapped when running the `lerna bootstrap` command.
- `packages`: Array of globs to use as package locations.


Expand Down Expand Up @@ -497,6 +503,29 @@ For example the `nsp` dependency is necessary in this case for `lerna run nsp`

### Flags

Options to Lerna can come from configuration (`lerna.json`) or on the command
line. Additionally options in config can live at the top level or may be
applied to specific commands.

Example:

```json
{
"lerna": "x.x.x",
"version": "1.2.0",
"exampleOption": "foo",
"command": {
"init": {
"exampleOption": "bar",
}
},
}
```

In this case `exampleOption` will be "foo" for all commands except `init`,
where it will be "bar". In all cases it may be overridden to "baz" on the
command-line with `--example-option=baz`.

#### --concurrency

How many threads to use when Lerna parallelizes the tasks (defaults to `4`)
Expand Down Expand Up @@ -525,16 +554,18 @@ Excludes a subset of packages when running a command.
$ lerna bootstrap --ignore component-*
```

The `ignore` flag, when used with the `bootstrap` command, can also be set in `lerna.json` under the `bootstrapConfig` key. The command-line flag will take precendence over this option.
The `ignore` flag, when used with the `bootstrap` command, can also be set in `lerna.json` under the `commands.bootstrap` key. The command-line flag will take precendence over this option.

**Example**

```javascript
{
"lerna": "2.0.0-beta.31",
"version": "0.0.0",
"bootstrapConfig": {
"ignore": "component-*"
"commands": {
"bootstrap": {
"ignore": "component-*"
}
}
}
```
Expand Down Expand Up @@ -587,6 +618,30 @@ By default, all tasks execute on packages in topologically sorted order as to re

Topological sorting can cause concurrency bottlenecks if there are a small number of packages with many dependents or if some packages take a disproportionately long time to execute. The `--no-sort` option disables sorting, instead executing tasks in an arbitrary order with maximum concurrency.

#### --hoist [glob]

Install external dependencies matching `glob` at the repo root so they're
available to all packages. Any binaries from these dependencies will be
linked into dependent package `node_modules/.bin/` directories so they're
available for npm scripts. If no `glob` is given the default is `**` (hoist
everything). This option only affects the `bootstrap` command.

```sh
$ lerna bootstrap --hoist
```

Note: If packages depend on different _versions_ of an external dependency,
the most commonly used version will be hoisted, and a warning will be emitted.

#### --nohoist [glob]

Do _not_ install external dependencies matching `glob` at the repo root. This
can be used to opt out of hoisting for certain dependencies.

```sh
$ lerna bootstrap --hoist --nohoist=babel-*
```

### Wizard

If you prefer some guidance for cli (in case you're about to start using lerna or introducing it to a new team), you might like [lerna-wizard](https://github.com/szarouski/lerna-wizard). It will lead you through a series of well-defined steps:
Expand Down
2 changes: 2 additions & 0 deletions bin/lerna.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ var cli = meow([
" --message, -m [msg] Use a custom commit message when creating the publish commit (only affects publish)",
" --exact Specify cross-dependency version numbers exactly rather than with a caret (^) (only affects publish)",
" --npm-tag [tagname] Publish packages with the specified npm dist-tag",
" --hoist [glob] Install external dependencies matching [glob] to the repo root. Use with no glob for all.",
" --nohoist [glob] Don't hoist external dependencies matching [glob] to the repo root",
" --scope [glob] Restricts the scope to package names matching the given glob (Works only in combination with the 'run', 'exec', 'clean', 'ls' and 'bootstrap' commands).",
" --ignore [glob] Ignores packages with names matching the given glob (Works only in combination with the 'run', 'exec', 'clean', 'ls' and 'bootstrap' commands).",
" --include-filtered-dependencies Flag to force lerna to include all dependencies and transitive dependencies when running 'bootstrap', even if they should not be included by the scope or ignore flags",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"cross-spawn": "^4.0.0",
"glob": "^7.0.6",
"inquirer": "^3.0.1",
"isarray": "^2.0.1",

This comment has been minimized.

Copy link
@wtgtybhertgeghgtwtg

wtgtybhertgeghgtwtg Jan 31, 2017

Contributor

Why does this add isarray?

This comment has been minimized.

Copy link
@gigabo

gigabo Jan 31, 2017

Contributor

Used here. This was originally written before we dropped 0.10/0.12 support. I think we can eliminate this now?

Nice catch @wtgtybhertgeghgtwtg!

This comment has been minimized.

Copy link
@wtgtybhertgeghgtwtg

wtgtybhertgeghgtwtg Jan 31, 2017

Contributor

You want I should file a PR for this?

This comment has been minimized.

Copy link
@gigabo

gigabo Jan 31, 2017

Contributor

Yes, please do! That will help us avoid losing track of it.

This comment has been minimized.

Copy link
@wtgtybhertgeghgtwtg

wtgtybhertgeghgtwtg Jan 31, 2017

Contributor

PR: #559.

"lodash.find": "^4.3.0",
"lodash.unionwith": "^4.2.0",
"meow": "^3.7.0",
Expand Down
75 changes: 73 additions & 2 deletions src/Command.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,47 @@ export default class Command {
this.toposort = !flags || (flags.sort == null ? true : flags.sort);
}

get name() {
// For a class named "FooCommand" this returns "foo".
return commandNameFromClassName(this.className);
}

get className() {
return this.constructor.name;
}

// Override this to inherit config from another command.
// For example `updated` inherits config from `publish`.
get otherCommandConfigs() {
return [];
}

getOptions(...objects) {

// Items lower down override items higher up.
return Object.assign(
{},

// Deprecated legacy options in `lerna.json`.
this._legacyOptions(),

// Global options from `lerna.json`.
this.repository.lernaJson,

// Option overrides for commands.
// Inherited options from `otherCommandConfigs` come before the current
// command's configuration.
...[...this.otherCommandConfigs, this.name]
.map((name) => (this.repository.lernaJson.command || {})[name]),

// For example, the item from the `packages` array in config.
...objects,

// CLI flags always override everything.
this.flags
);
}

run() {
this.logger.info("Lerna v" + this.lernaVersion);

Expand Down Expand Up @@ -95,8 +136,7 @@ export default class Command {
}

runPreparations() {
const scope = this.flags.scope || (this.configFlags && this.configFlags.scope);
const ignore = this.flags.ignore || (this.configFlags && this.configFlags.ignore);
const {scope, ignore} = this.getOptions();

if (scope) {
this.logger.info(`Scoping to packages that match '${scope}'`);
Expand Down Expand Up @@ -177,6 +217,16 @@ export default class Command {
}
}

_legacyOptions() {
return ["bootstrap", "publish"].reduce((opts, command) => {
if (this.name === command && this.repository.lernaJson[`${command}Config`]) {
logger.warn(`\`${command}Config.ignore\` is deprecated. Use \`commands.${command}.ignore\`.`);
opts.ignore = this.repository.lernaJson[`${command}Config`].ignore;
}
return opts;
}, {});
}

initialize() {
throw new Error("command.initialize() needs to be implemented.");
}
Expand All @@ -185,3 +235,24 @@ export default class Command {
throw new Error("command.execute() needs to be implemented.");
}
}

export function commandNameFromClassName(className) {
return className.replace(/Command$/, "").toLowerCase();
}

export function exposeCommands(commands) {
return commands.reduce((obj, cls) => {
const commandName = commandNameFromClassName(cls.name);
if (!cls.name.match(/Command$/)) {
throw new Error(`Invalid command class name "${cls.name}". Must end with "Command".`);
}
if (obj[commandName]) {
throw new Error(`Duplicate command: "${commandName}"`);
}
if (!Command.isPrototypeOf(cls)) {
throw new Error(`Command does not extend Command: "${cls.name}"`);
}
obj[commandName] = cls;
return obj;
}, {});
}
12 changes: 12 additions & 0 deletions src/NpmUtilities.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ChildProcessUtilities from "./ChildProcessUtilities";
import logger from "./logger";
import escapeArgs from "command-join";
import path from "path";
import semver from "semver";

export default class NpmUtilities {
@logger.logifyAsync()
Expand Down Expand Up @@ -48,4 +50,14 @@ export default class NpmUtilities {
static publishTaggedInDir(tag, directory, callback) {
ChildProcessUtilities.exec("cd " + escapeArgs(directory) + " && npm publish --tag " + tag, null, callback);
}

@logger.logifySync
static dependencyIsSatisfied(dir, dependency, needVersion) {
const packageJson = path.join(dir, dependency, "package.json");
try {
return semver.satisfies(require(packageJson).version, needVersion);
} catch (e) {
return false;
}
}
}
12 changes: 3 additions & 9 deletions src/Package.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,8 @@ export default class Package {
* @returns {Boolean}
*/
hasDependencyInstalled(dependency) {
const packageJson = path.join(this.nodeModulesLocation, dependency, "package.json");
try {
return semver.satisfies(
require(packageJson).version,
this.allDependencies[dependency]
);
} catch (e) {
return false;
}
return NpmUtilities.dependencyIsSatisfied(
this.nodeModulesLocation, dependency, this.allDependencies[dependency]
);
}
}
44 changes: 30 additions & 14 deletions src/PackageUtilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import path from "path";
import {sync as globSync} from "glob";
import minimatch from "minimatch";
import async from "async";
import isArray from "isarray";

export default class PackageUtilities {
static getGlobalVersion(versionPath) {
Expand Down Expand Up @@ -119,25 +120,40 @@ export default class PackageUtilities {
* @throws in case a given glob would produce an empty list of packages
*/
static _filterPackages(packages, glob, negate = false) {
if (typeof glob !== "undefined") {
packages = packages.filter((pkg) => {
if (negate) {
return !minimatch(pkg.name, glob);
} else {
return minimatch(pkg.name, glob);
}
});

if (!packages.length) {
throw new Error(`No packages found that match '${glob}'`);
}
} else {
// Always return a copy.
packages = packages.slice();
packages = packages.filter((pkg) => PackageUtilities.filterPackage(pkg, glob, negate));

if (!packages.length) {
throw new Error(`No packages found that match '${glob}'`);
}
return packages;
}

static filterPackage(pkg, glob, negate = false) {

// If there isn't a filter then we can just return the package.
if (!glob) return true;

// Include/exlude with no arguments implies splat.
// For example: `--hoist` is equivalent to `--hoist=**`.
// The double star here is to account for scoped packages.
if (glob === true) glob = "**";

if (!isArray(glob)) glob = [glob];

const maybeNegate = negate ? (v) => !v : (v) => v;

return glob.some((glob) => maybeNegate(minimatch(pkg.name, glob)));
}

static getFilteredPackage(pkg, {scope, ignore}) {

return (
PackageUtilities.filterPackage(pkg, scope) &&
PackageUtilities.filterPackage(pkg, ignore, true)
) && pkg;
}

static topologicallyBatchPackages(packagesToBatch, logger = null) {
// We're going to be chopping stuff out of this array, so copy it.
const packages = packagesToBatch.slice();
Expand Down

0 comments on commit 6e9bf13

Please sign in to comment.