Skip to content

Commit

Permalink
feat(project): Inherit configuration with yargs-like "extends"
Browse files Browse the repository at this point in the history
Fixes #1281
  • Loading branch information
evocateur committed Mar 26, 2018
1 parent 9de2362 commit 0b28ef5
Show file tree
Hide file tree
Showing 19 changed files with 243 additions and 7 deletions.
4 changes: 4 additions & 0 deletions core/project/__fixtures__/extends-circular/base.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./circular.json",
"loglevel": "warn"
}
4 changes: 4 additions & 0 deletions core/project/__fixtures__/extends-circular/circular.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./base.json",
"loglevel": "error"
}
4 changes: 4 additions & 0 deletions core/project/__fixtures__/extends-circular/lerna.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./circular.json",
"version": "1.0.0"
}
11 changes: 11 additions & 0 deletions core/project/__fixtures__/extends-recursive/base.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"packages": [
"base-pkgs/*"
],
"command": {
"list": {
"json": true
}
},
"version": "ignored"
}
4 changes: 4 additions & 0 deletions core/project/__fixtures__/extends-recursive/lerna.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./recursive.json",
"version": "1.0.0"
}
11 changes: 11 additions & 0 deletions core/project/__fixtures__/extends-recursive/recursive.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "./base.json",
"packages": [
"recursive-pkgs/*"
],
"command": {
"list": {
"private": false
}
}
}
4 changes: 4 additions & 0 deletions core/project/__fixtures__/extends-unresolved/lerna.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "unresolved",
"version": "1.0.0"
}
4 changes: 4 additions & 0 deletions core/project/__fixtures__/extends/lerna.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "local-package",
"version": "1.0.0"
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions core/project/__fixtures__/extends/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "extends",
"version": "0.0.0-root",
"private": true,
"devDependencies": {
"local-package": "2.0.0"
}
}
14 changes: 14 additions & 0 deletions core/project/__fixtures__/pkg-prop/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "pkg-prop",
"version": "0.0.0-root",
"private": true,
"lerna": {
"loglevel": "success",
"command": {
"publish": {
"loglevel": "verbose"
}
},
"version": "1.0.0"
}
}
90 changes: 87 additions & 3 deletions core/project/__tests__/project.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,9 @@ describe("Project", () => {
});

it("defaults to an empty object", async () => {
const cwd = await initFixture("no-lerna-config");
const repo = new Project(cwd);
await initFixture("no-lerna-config");

expect(repo.config).toEqual({});
expect(new Project().config).toEqual({});
});

it("errors when lerna.json is not valid JSON", async () => {
Expand All @@ -84,6 +83,91 @@ describe("Project", () => {
expect(err.prefix).toBe("JSONError");
}
});

it("returns parsed rootPkg.lerna", async () => {
const cwd = await initFixture("pkg-prop");
const project = new Project(cwd);

expect(project.config).toEqual({
command: {
publish: {
loglevel: "verbose",
},
},
loglevel: "success",
version: "1.0.0",
});
});

it("extends local shared config", async () => {
const cwd = await initFixture("extends");
const project = new Project(cwd);

expect(project.config).toEqual({
packages: ["custom-local/*"],
version: "1.0.0",
});
});

it("extends local shared config subpath", async () => {
const cwd = await initFixture("extends");

await fs.writeJSON(path.resolve(cwd, "lerna.json"), {
extends: "local-package/subpath",
version: "1.0.0",
});

const project = new Project(cwd);

expect(project.config).toEqual({
packages: ["subpath-local/*"],
version: "1.0.0",
});
});

it("extends config recursively", async () => {
const cwd = await initFixture("extends-recursive");
const project = new Project(cwd);

expect(project.config).toEqual({
command: {
list: {
json: true,
private: false,
},
},
packages: ["recursive-pkgs/*"],
version: "1.0.0",
});
});

it("throws an error when extend target is unresolvable", async () => {
const cwd = await initFixture("extends-unresolved");

try {
// eslint-disable-next-line no-unused-vars
const project = new Project(cwd);
console.log(project);
} catch (err) {
expect(err.message).toMatch("must be locally-resolvable");
}

expect.assertions(1);
});

it("throws an error when extend target is circular", async () => {
const cwd = await initFixture("extends-circular");

try {
// eslint-disable-next-line no-unused-vars
const project = new Project(cwd);
console.log(project);
} catch (err) {
expect(err.message).toMatch("cannot be circular");
}

expect.assertions(1);
});
});

describe("get .version", () => {
Expand Down
3 changes: 3 additions & 0 deletions core/project/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const writeJsonFile = require("write-json-file");

const ValidationError = require("@lerna/validation-error");
const Package = require("@lerna/package");
const applyExtends = require("./lib/apply-extends");

class Project {
constructor(cwd) {
Expand All @@ -36,6 +37,8 @@ class Project {
delete obj.config.commands;
}

obj.config = applyExtends(obj.config, path.dirname(obj.filepath));

return obj;
},
});
Expand Down
36 changes: 36 additions & 0 deletions core/project/lib/apply-extends.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use strict";

const path = require("path");
const resolveFrom = require("resolve-from");
const ValidationError = require("@lerna/validation-error");
const shallowExtend = require("./shallow-extend");

module.exports = applyExtends;

function applyExtends(config, cwd, seen = new Set()) {
let defaultConfig = {};

if ("extends" in config) {
let pathToDefault;

try {
pathToDefault = resolveFrom(cwd, config.extends);
} catch (err) {
throw new ValidationError("ERESOLVED", "Config .extends must be locally-resolvable", err);
}

if (seen.has(pathToDefault)) {
throw new ValidationError("ECIRCULAR", "Config .extends cannot be circular", seen);
}

seen.add(pathToDefault);

// eslint-disable-next-line import/no-dynamic-require, global-require
defaultConfig = require(pathToDefault);
delete config.extends; // eslint-disable-line no-param-reassign

defaultConfig = applyExtends(defaultConfig, path.dirname(pathToDefault), seen);
}

return shallowExtend(config, defaultConfig);
}
20 changes: 20 additions & 0 deletions core/project/lib/shallow-extend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use strict";

module.exports = shallowExtend;

function shallowExtend(json, defaults = {}) {
return Object.keys(json).reduce((obj, key) => {
const val = json[key];

if (Array.isArray(val)) {
// always clobber arrays, merging isn't worth unexpected complexity
obj[key] = val.slice();
} else if (val && typeof val === "object") {
obj[key] = shallowExtend(val, obj[key]);
} else {
obj[key] = val;
}

return obj;
}, defaults);
}
4 changes: 3 additions & 1 deletion core/project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"url": "https://github.com/evocateur"
},
"files": [
"index.js"
"index.js",
"lib"
],
"main": "index.js",
"engines": {
Expand All @@ -37,6 +38,7 @@
"glob-parent": "^3.1.0",
"load-json-file": "^4.0.0",
"npmlog": "^4.1.2",
"resolve-from": "^4.0.0",
"write-json-file": "^2.3.0"
}
}
14 changes: 11 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0b28ef5

Please sign in to comment.