Skip to content

Commit

Permalink
Merge branch 'feature/pkg-main' into builded/petcommunities
Browse files Browse the repository at this point in the history
* feature/pkg-main:
  Bootstrap: copy `main` property from package.json
  2.0.0-beta.23
  fix execSync use
  Add `onlyExplicitUpdates` flag so that only packages that have changed have version bumps rather than all packages that depend on the updated packages (lerna#241)
  Re-introduce node 0.10 support (lerna#248)
  2.0.0-beta.22
  Consistent naming in README `import` section (lerna#243) [skip ci]
  Lerna import (lerna#173)
  Revert "Use sync-exec for node 0.10" (lerna#242)
  Revert "Fix bootstrap install to use quotes around versions (lerna#235)"
  2.0.0-beta.21
  Fix bootstrap install to use quotes around versions (lerna#235)
  typo [skip ci]

# Conflicts:
#	src/commands/BootstrapCommand.js
  • Loading branch information
LoicMahieu committed Jun 28, 2016
2 parents 6444811 + 00de51f commit bf8f894
Show file tree
Hide file tree
Showing 17 changed files with 363 additions and 12 deletions.
30 changes: 29 additions & 1 deletion README.md
Expand Up @@ -20,7 +20,7 @@ repositories is *messy* and difficult to track, and testing across repositories
gets complicated really fast.

To solve these (and many other) problems, some projects will organize their
codebases into multi-package repostories (sometimes called [monorepos](https://github.com/babel/babel/blob/master/doc/design/monorepo.md)). Projects like [Babel](https://github.com/babel/babel/tree/master/packages), [React](https://github.com/facebook/react/tree/master/packages), [Angular](https://github.com/angular/angular/tree/master/modules),
codebases into multi-package repositories (sometimes called [monorepos](https://github.com/babel/babel/blob/master/doc/design/monorepo.md)). Projects like [Babel](https://github.com/babel/babel/tree/master/packages), [React](https://github.com/facebook/react/tree/master/packages), [Angular](https://github.com/angular/angular/tree/master/modules),
[Ember](https://github.com/emberjs/ember.js/tree/master/packages), [Meteor](https://github.com/meteor/meteor/tree/devel/packages), [Jest](https://github.com/facebook/jest/tree/master/packages), and many others develop all of their packages within a
single repository.

Expand Down Expand Up @@ -384,6 +384,21 @@ $ lerna exec --scope my-component -- ls -la
$ lerna exec --concurrency 1 -- ls -la
```

### import

```sh
$ lerna import <path-to-external-repository>
```

Import the package at `<path-to-external-repository>`, with commit history,
into `packages/<directory-name>`. Original commit authors, dates and messages
are preserved. Commits are applied to the current branch.

This is useful for gathering pre-existing standalone packages into a Lerna
repo. Each commit is modified to make changes relative to the package
directory. So, for example, the commit that added `package.json` will
instead add `packages/<directory-name>/package.json`.

## Misc

Lerna will log to a `lerna-debug.log` file (same as `npm-debug.log`) when it encounters an error running a command.
Expand Down Expand Up @@ -461,3 +476,16 @@ The `ignore` flag, when used with the `bootstrap` command, can also be set in `l

> Hint: The glob is matched against the package name defined in `package.json`,
> not the directory name the package lives in.
#### --only-explicit-updates

Only will bump versions for packages that have been updated explicitly rather than cross-dependencies.

> This may not make sense for a major version bump since other packages that depend on the updated packages wouldn't be updated.
```sh
$ lerna updated --only-explicit-updates
$ lerna publish --only-explicit-updates
```

Ex: in Babel, `babel-types` is depended upon by all packages in the monorepo (over 100). However, Babel uses `^` for most of it's dependencies so it isn't necessary to bump the versions of all packages if only `babel-types` is updated. This option allows only the packages that have been explicitly updated to make a new version.
1 change: 1 addition & 0 deletions bin/lerna.js
Expand Up @@ -12,6 +12,7 @@ var cli = meow([
" bootstrap Link together local packages and npm install remaining package dependencies",
" publish Publish updated packages to npm",
" updated Check which packages have changed since the last release",
" import Import a package with git history from an external repository",
" clean Remove the node_modules directory from all packages",
" diff Diff all packages or a single package since the last release",
" init Initialize a lerna repo",
Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "lerna",
"version": "2.0.0-beta.20",
"version": "2.0.0-beta.23",
"description": "Tool for managing JavaScript projects with multiple packages",
"main": "lib/index.js",
"scripts": {
Expand Down
13 changes: 6 additions & 7 deletions src/ChildProcessUtilities.js
Expand Up @@ -21,15 +21,14 @@ export default class ChildProcessUtilities {
});
}

static execSync(command) {
static execSync(command, opts) {
const mergedOpts = objectAssign({
encoding: "utf8"
}, opts);
if (child.execSync) {
return child.execSync(command, {
encoding: "utf8"
}).trim();
return child.execSync(command, mergedOpts).trim();
} else {
return syncExec(command, {
encoding: "utf8"
}).stdout.trim();
return syncExec(command, mergedOpts).stdout.trim();
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/UpdatedPackagesCollector.js
Expand Up @@ -127,7 +127,7 @@ export default class UpdatedPackagesCollector {

collectUpdates() {
return this.packages.filter(pkg => {
return this.updatedPackages[pkg.name] || this.dependents[pkg.name] || this.flags.canary;
return this.updatedPackages[pkg.name] || (this.flags.onlyExplicitUpdates ? false : this.dependents[pkg.name]) || this.flags.canary;
}).map(pkg => {
return new Update(pkg);
});
Expand Down
3 changes: 2 additions & 1 deletion src/commands/BootstrapCommand.js
Expand Up @@ -87,7 +87,8 @@ export default class BootstrapCommand extends Command {

const packageJsonFileContents = JSON.stringify({
name: name,
version: pkg.version
version: pkg.version,
main: pkg.main
}, null, " ");

async.parallel([
Expand Down
101 changes: 101 additions & 0 deletions src/commands/ImportCommand.js
@@ -0,0 +1,101 @@
import fs from "fs";
import path from "path";
import async from "async";
import Command from "../Command";
import progressBar from "../progressBar";
import PromptUtilities from "../PromptUtilities";
import ChildProcessUtilities from "../ChildProcessUtilities";

export default class ImportCommand extends Command {
initialize(callback) {
const inputPath = this.input[0];

if (!inputPath) {
return callback(new Error("Missing argument: Path to external repository"));
}

const externalRepoPath = path.resolve(inputPath);
const externalRepoBase = path.basename(externalRepoPath);

try {
const stats = fs.statSync(externalRepoPath);
if (!stats.isDirectory()) {
throw new Error(`Input path "${inputPath}" is not a directory`);
}
const packageJson = path.join(externalRepoPath, "package.json");
const packageName = require(packageJson).name;
if (!packageName) {
throw new Error(`No package name specified in "${packageJson}"`);
}
} catch (e) {
if (e.code === "ENOENT") {
return callback(new Error(`No repository found at "${inputPath}"`));
}
return callback(e);
}

this.targetDir = "packages/" + externalRepoBase;

try {
if (fs.statSync(this.targetDir)) {
return callback(new Error(`Target directory already exists "${this.targetDir}"`));
}
} catch (e) { /* Pass */ }

this.externalExecOpts = {
encoding: "utf8",
cwd: externalRepoPath
};

this.commits = this.externalExecSync("git log --format=\"%h\"").split("\n").reverse();

if (!this.commits.length) {
callback(new Error(`No git commits to import at "${inputPath}"`));
}

this.logger.info(`About to import ${this.commits.length} commits into from ${inputPath} into ${this.targetDir}`);

if (this.flags.yes) {
callback(null, true);
} else {
PromptUtilities.confirm("Are you sure you want to import these commits onto the current branch?", confirmed => {
if (confirmed) {
callback(null, true);
} else {
this.logger.info("Okay bye!");
callback(null, false);
}
});
}
}

externalExecSync(command) {
return ChildProcessUtilities.execSync(command, this.externalExecOpts).trim();
}

execute(callback) {
const replacement = "$1/" + this.targetDir;

progressBar.init(this.commits.length);

async.series(this.commits.map(sha => done => {
progressBar.tick(sha);

// Create a patch file for this commit and prepend the target directory
// to all affected files. This moves the git history for the entire
// external repository into the package subdirectory, commit by commit.
const patch = this.externalExecSync(`git format-patch -1 ${sha} --stdout`)
.replace(/^([-+]{3} [ab])/mg, replacement)
.replace(/^(diff --git a)/mg, replacement)
.replace(/^(diff --git \S+ b)/mg, replacement);

// Apply the modified patch to the current lerna repository, preserving
// original commit date, author and message.
ChildProcessUtilities.exec("git am", {}, done).stdin.end(patch);
}), err => {
progressBar.terminate();
this.logger.info("Import complete!");
callback(err, !err);
});
}
}
2 changes: 2 additions & 0 deletions src/index.js
@@ -1,6 +1,7 @@
import BootstrapCommand from "./commands/BootstrapCommand";
import PublishCommand from "./commands/PublishCommand";
import UpdatedCommand from "./commands/UpdatedCommand";
import ImportCommand from "./commands/ImportCommand";
import CleanCommand from "./commands/CleanCommand";
import DiffCommand from "./commands/DiffCommand";
import InitCommand from "./commands/InitCommand";
Expand All @@ -12,6 +13,7 @@ export const __commands__ = {
bootstrap: BootstrapCommand,
publish: PublishCommand,
updated: UpdatedCommand,
import: ImportCommand,
clean: CleanCommand,
diff: DiffCommand,
init: InitCommand,
Expand Down
2 changes: 1 addition & 1 deletion test/BootstrapCommand.js
Expand Up @@ -58,7 +58,7 @@ describe("BootstrapCommand", () => {
assert.equal(fs.readFileSync(path.join(testDir, "packages/package-2/node_modules/package-1/package.json")).toString(), "{\n \"name\": \"package-1\",\n \"version\": \"1.0.0\"\n}\n");

assert.equal(fs.readFileSync(path.join(testDir, "packages/package-3/node_modules/package-2/index.js")).toString(), "/**\n * @prefix\n */\nmodule.exports = require(\"" + path.join(testDir, "packages/package-2") + "\");\n");
assert.equal(fs.readFileSync(path.join(testDir, "packages/package-3/node_modules/package-2/package.json")).toString(), "{\n \"name\": \"package-2\",\n \"version\": \"1.0.0\"\n}\n");
assert.equal(fs.readFileSync(path.join(testDir, "packages/package-3/node_modules/package-2/package.json")).toString(), "{\n \"name\": \"package-2\",\n \"version\": \"1.0.0\",\n \"main\": \"lib/index.js\"\n}\n");

done();
} catch (err) {
Expand Down
148 changes: 148 additions & 0 deletions test/ImportCommand.js
@@ -0,0 +1,148 @@
import pathExists from "path-exists";
import assert from "assert";
import path from "path";
import fs from "fs";

import PromptUtilities from "../src/PromptUtilities";
import ChildProcessUtilities from "../src/ChildProcessUtilities";
import ImportCommand from "../src/commands/ImportCommand";
import exitWithCode from "./_exitWithCode";
import initFixture from "./_initFixture";
import initExternalFixture from "./_initExternalFixture";
import assertStubbedCalls from "./_assertStubbedCalls";

describe("ImportCommand", () => {

describe("import", () => {
let testDir, externalDir;

beforeEach(done => {
testDir = initFixture("ImportCommand/basic", done);
});

beforeEach(done => {
externalDir = initExternalFixture("ImportCommand/external", done);
});

it("should import into packages with commit history", done => {
const importCommand = new ImportCommand([externalDir], {});

importCommand.runValidations();
importCommand.runPreparations();

assertStubbedCalls([
[PromptUtilities, "confirm", { valueCallback: true }, [
{ args: ["Are you sure you want to import these commits onto the current branch?"], returns: true }
]],
]);

importCommand.runCommand(exitWithCode(0, err => {
if (err) return done(err);

try {
const lastCommit = ChildProcessUtilities.execSync("git log --format=\"%s\"", {encoding:"utf8"}).split("\n")[0];
const packageJson = path.join(testDir, "packages", path.basename(externalDir), "package.json");
assert.ok(!pathExists.sync(path.join(testDir, "lerna-debug.log")));
assert.ok(pathExists.sync(packageJson));
assert.equal(lastCommit, "Init external commit");
done();
} catch (err) {
done(err);
}
}));
});

it("should be possible to skip asking for confirmation", done => {

const importCommand = new ImportCommand([externalDir], {
yes: true
});

importCommand.runValidations();
importCommand.runPreparations();

importCommand.initialize(done);
});

it("should fail without an argument", done => {
const importCommand = new ImportCommand([], {});

importCommand.runValidations();
importCommand.runPreparations();

importCommand.runCommand(exitWithCode(1, err => {
const expect = "Missing argument: Path to external repository";
assert.equal((err || {}).message, expect);
done();
}));
});

it("should fail with a missing external directory", done => {
const missing = externalDir + "_invalidSuffix";
const importCommand = new ImportCommand([missing], {});

importCommand.runValidations();
importCommand.runPreparations();

importCommand.runCommand(exitWithCode(1, err => {
const expect = `No repository found at "${missing}"`;
assert.equal((err || {}).message, expect);
done();
}));
});

it("should fail with a missing package.json", done => {
const importCommand = new ImportCommand([externalDir], {});

const packageJson = path.join(externalDir, "package.json");

fs.unlinkSync(packageJson);

importCommand.runValidations();
importCommand.runPreparations();

importCommand.runCommand(exitWithCode(1, err => {
const expect = `Cannot find module '${packageJson}'`;
assert.equal((err || {}).message, expect);
done();
}));
});

it("should fail with no name in package.json", done => {
const importCommand = new ImportCommand([externalDir], {});

const packageJson = path.join(externalDir, "package.json");

fs.writeFileSync(packageJson, "{}");

importCommand.runValidations();
importCommand.runPreparations();

importCommand.runCommand(exitWithCode(1, err => {
const expect = `No package name specified in "${packageJson}"`;
assert.equal((err || {}).message, expect);
done();
}));
});

it("should fail if target directory exists", done => {
const importCommand = new ImportCommand([externalDir], {});

const targetDir = path.relative(
process.cwd(),
path.join(testDir, "packages", path.basename(externalDir))
);

fs.mkdirSync(targetDir);

importCommand.runValidations();
importCommand.runPreparations();

importCommand.runCommand(exitWithCode(1, err => {
const expect = `Target directory already exists "${targetDir}"`;
assert.equal((err || {}).message, expect);
done();
}));
});
});
});

0 comments on commit bf8f894

Please sign in to comment.