Skip to content

Commit

Permalink
Introduced support for package git dependency urls (coauthored with @…
Browse files Browse the repository at this point in the history
  • Loading branch information
gustaff-weldon committed Sep 29, 2017
1 parent 86db2fc commit 11b4445
Show file tree
Hide file tree
Showing 15 changed files with 569 additions and 15 deletions.
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,8 @@ Running `lerna` without arguments will show all commands/options.
}
},
"packages": ["packages/*"]
"useGitVersion": true,
"gitVersionPrefix": "v"
}
```

Expand All @@ -672,6 +674,23 @@ Running `lerna` without arguments will show all commands/options.
- `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.
- `useGitVersion`: a boolean (defaults to `false`) indicating if [git hosted urls](https://github.com/npm/hosted-git-info) should be allowed instead of plain version number. If enabled, Lerna will attempt to extract and save the interpackage dependency versions using git url-aware parser.
This allows packages to be distributed via git repos if eg. packages are private and [private npm repo is not an option](https://www.dotconferences.com/2016/05/fabien-potencier-monolithic-repositories-vs-many-repositories).
Please note that using `gitVersion` requires `publish` command to be used with `--exact` and is limited to urls with [`committish`](https://docs.npmjs.com/files/package.json#git-urls-as-dependencies) part present.

Example assuming 2 packages where `my-package-1` depends on `my-package-2`, for which `package.json` of `my-package-1` could be:
```
{
name: "my-package-1",
version: "1.0.0",
bin: "bin.js",
dependencies: { "my-package-2": "github:example-user/my-package-2#1.0.0" },
devDependencies: { "my-dev-dependency": "^1.0.0" },
peerDependencies: { "my-peer-dependency": "^1.0.0" }
}
```
- `gitVersionPrefix`: version prefix string (defaults to 'v') ignored when extracting version number from commitish part of git url. eg. given `github:example-user/my-package-2#v1.0.0`
and `gitVersionPrefix: 'v'` version will be read as `1.0.0`. Ignored if `useGitVersion` is set to `false`.


### Common `devDependencies`
Expand Down Expand Up @@ -896,9 +915,9 @@ May also be configured in `lerna.json`:

#### --use-workspaces

Enables integration with [Yarn Workspaces](https://github.com/yarnpkg/rfcs/blob/master/implemented/0000-workspaces-install-phase-1.md) (available since yarn@0.27+).
The values in the array are the commands in which Lerna will delegate operation to Yarn (currently only bootstrapping).
If `--use-workspaces` is true then `packages` will be overridden by the value from `package.json/workspaces.`
Enables integration with [Yarn Workspaces](https://github.com/yarnpkg/rfcs/blob/master/implemented/0000-workspaces-install-phase-1.md) (available since yarn@0.27+).
The values in the array are the commands in which Lerna will delegate operation to Yarn (currently only bootstrapping).
If `--use-workspaces` is true then `packages` will be overridden by the value from `package.json/workspaces.`
May also be configured in `lerna.json`:

```js
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"glob-parent": "^3.1.0",
"globby": "^6.1.0",
"graceful-fs": "^4.1.11",
"hosted-git-info": "^2.5.0",
"inquirer": "^3.2.2",
"is-ci": "^1.0.10",
"load-json-file": "^3.0.0",
Expand Down
22 changes: 22 additions & 0 deletions src/GitVersionParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { escapeRegExp } from "lodash";
import hostedGitInfo from "hosted-git-info";

export default class GitVersionParser {
constructor(versionPrefix = "v") {
this._gitUrlPattern = new RegExp(`(.+?#${escapeRegExp(versionPrefix)})(.+)$`);
}

parseVersion(version) {
const gitInfo = hostedGitInfo.fromUrl(version);
let targetMatches;

if (gitInfo && gitInfo.committish) {
targetMatches = this._gitUrlPattern.exec(version);
}

return {
prefix: targetMatches ? targetMatches[1] : null,
version: targetMatches ? targetMatches[2] : version
};
}
}
18 changes: 17 additions & 1 deletion src/Package.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import dedent from "dedent";
import log from "npmlog";
import path from "path";
import semver from "semver";
import _ from "lodash";

import dependencyIsSatisfied from "./utils/dependencyIsSatisfied";
import NpmUtilities from "./NpmUtilities";
Expand Down Expand Up @@ -60,12 +61,27 @@ export default class Package {
return this._package.scripts || {};
}

set versionSerializer(versionSerializer) {
const previousSerializer = this._versionSerializer;

this._versionSerializer = versionSerializer;

if (previousSerializer) {
this._package = previousSerializer.serialize(this._package);
}

if (versionSerializer) {
this._package = versionSerializer.deserialize(this._package);
}
}

isPrivate() {
return !!this._package.private;
}

toJSON() {
return this._package;
const pkg = _.cloneDeep(this._package);
return this._versionSerializer ? this._versionSerializer.serialize(pkg) : pkg;
}

/**
Expand Down
14 changes: 10 additions & 4 deletions src/PackageGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class PackageGraphNode {
* devDependencies that would normally be included.
*/
export default class PackageGraph {
constructor(packages, depsOnly = false) {
constructor(packages, depsOnly = false, versionParser) {
this.nodes = [];
this.nodesByName = {};

Expand All @@ -38,11 +38,17 @@ export default class PackageGraph {

for (let d = 0; d < depNames.length; d++) {
const depName = depNames[d];
const depVersion = dependencies[depName];
const packageNode = this.nodesByName[depName];

if (packageNode && semver.satisfies(packageNode.package.version, depVersion)) {
node.dependencies.push(depName);
if (packageNode) {
const depVersion = (versionParser
? versionParser.parseVersion(dependencies[depName]).version
: dependencies[depName]
);

if (semver.satisfies(packageNode.package.version, depVersion)) {
node.dependencies.push(depName);
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/PackageUtilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ export default class PackageUtilities {
return packages;
}

static getPackageGraph(packages, depsOnly) {
return new PackageGraph(packages, depsOnly);
static getPackageGraph(packages, depsOnly, versionParser) {
return new PackageGraph(packages, depsOnly, versionParser);
}

/**
Expand Down
20 changes: 18 additions & 2 deletions src/Repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import semver from "semver";
import dependencyIsSatisfied from "./utils/dependencyIsSatisfied";
import Package from "./Package";
import PackageUtilities from "./PackageUtilities";
import GitVersionParser from "./GitVersionParser";
import VersionSerializer from "./VersionSerializer";

const DEFAULT_PACKAGE_GLOB = "packages/*";

Expand Down Expand Up @@ -118,8 +120,22 @@ export default class Repository {
}

buildPackageGraph() {
this._packages = PackageUtilities.getPackages(this);
this._packageGraph = PackageUtilities.getPackageGraph(this._packages);
const packages = PackageUtilities.getPackages(this);
const packageGraph = PackageUtilities.getPackageGraph(packages, false, this.lernaJson.useGitVersion
? new GitVersionParser(this.lernaJson.gitVersionPrefix)
: null);

if (this.lernaJson.useGitVersion) {
packages.forEach((pkg) => {
pkg.versionSerializer = new VersionSerializer({
monorepoDependencies: packageGraph.get(pkg.name).dependencies,
versionParser: new GitVersionParser(this.lernaJson.gitVersionPrefix)
});
});
}

this._packages = packages;
this._packageGraph = packageGraph;
}

hasDependencyInstalled(depName, version) {
Expand Down
46 changes: 46 additions & 0 deletions src/VersionSerializer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export default class VersionSerializer {
constructor({ monorepoDependencies, versionParser }) {
this._monorepoDependencies = monorepoDependencies;
this._versionParser = versionParser;
this._dependenciesKeys = ["dependencies", "devDependencies", "peerDependencies"];
this._strippedPrefixes = new Map();
}

serialize(pkg) {
this._dependenciesKeys.forEach((key) => {
this._prependPrefix(pkg[key] || {});
});

return pkg;
}

deserialize(pkg) {
this._dependenciesKeys.forEach((key) => {
this._stripPrefix(pkg[key] || {});
});

return pkg;
}

_prependPrefix(dependencies) {
this._strippedPrefixes.forEach((prefix, name) => {
const version = dependencies[name];
if (version) {
dependencies[name] = `${prefix}${version}`;
}
});
}

_stripPrefix(dependencies) {
Object.keys(dependencies).forEach((name) => {
if (this._monorepoDependencies.includes(name)) {
const result = this._versionParser.parseVersion(dependencies[name]);

if (result.prefix) {
dependencies[name] = result.version;
this._strippedPrefixes.set(name, result.prefix);
}
}
});
}
}
6 changes: 6 additions & 0 deletions src/commands/PublishCommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ export default class PublishCommand extends Command {
this.gitRemote = this.options.gitRemote || "origin";
this.gitEnabled = !(this.options.canary || this.options.skipGit);

if (this.repository.lernaJson.useGitVersion && !this.options.exact) {
this.logger.error("config", ```Using git version without 'exact' option is not recommended.
Please make sure you publish with --exact.```);
}

if (this.options.canary) {
this.logger.info("canary", "enabled");
}
Expand Down Expand Up @@ -600,6 +605,7 @@ export default class PublishCommand extends Command {
});
}


commitAndTagUpdates() {
if (this.repository.isIndependent()) {
this.tags = this.gitCommitAndTagVersionForUpdates();
Expand Down
79 changes: 79 additions & 0 deletions test/GitVersionParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import log from "npmlog";

// file under test
import GitVersionParser from "../src/GitVersionParser";

// silence logs
log.level = "silent";

describe("GitVersionParser", () => {

describe("parseVersion - without prefix", () => {
let parser;

beforeEach(() => {
parser = new GitVersionParser("");
});

it("should work for semver version", () => {
expect(parser.parseVersion("0.0.2")).toEqual({
prefix: null,
version: "0.0.2"
});

expect(parser.parseVersion("~0.0.2")).toEqual({
prefix: null,
version: "~0.0.2"
});
});

it("should work for git url", () => {
expect(parser.parseVersion("github:user-foo/project-foo#v0.0.1")).toEqual({
prefix: "github:user-foo/project-foo#",
version: "v0.0.1"
});

expect(parser.parseVersion("git@github.com:user-foo/project-foo#0.0.5")).toEqual({
prefix: "git@github.com:user-foo/project-foo#",
version: "0.0.5"
});
});
});

describe("parseVersion - with version prefix", () => {
let parser;

beforeEach(() => {
parser = new GitVersionParser("v");
});

it("should work for semver version", () => {
expect(parser.parseVersion("0.0.2")).toEqual({
prefix: null,
version: "0.0.2"
});

expect(parser.parseVersion("~0.0.2")).toEqual({
prefix: null,
version: "~0.0.2"
});
});

it("should work for git url", () => {
expect(parser.parseVersion("github:user-foo/project-foo#v0.0.1")).toEqual({
prefix: "github:user-foo/project-foo#v",
version: "0.0.1"
});

expect(parser.parseVersion("git@github.com:user-foo/project-foo#0.0.5")).toEqual({
prefix: null,
version: "git@github.com:user-foo/project-foo#0.0.5"
});

expect(parser.parseVersion("git@github.com:user-foo/project-foo#v0.0.5")).toEqual({
prefix: "git@github.com:user-foo/project-foo#v",
version: "0.0.5"
});
});
});
});

0 comments on commit 11b4445

Please sign in to comment.