Skip to content

Commit 0b28ef5

Browse files
committed
feat(project): Inherit configuration with yargs-like "extends"
Fixes #1281
1 parent 9de2362 commit 0b28ef5

File tree

19 files changed

+243
-7
lines changed

19 files changed

+243
-7
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "./circular.json",
3+
"loglevel": "warn"
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "./base.json",
3+
"loglevel": "error"
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "./circular.json",
3+
"version": "1.0.0"
4+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"packages": [
3+
"base-pkgs/*"
4+
],
5+
"command": {
6+
"list": {
7+
"json": true
8+
}
9+
},
10+
"version": "ignored"
11+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "./recursive.json",
3+
"version": "1.0.0"
4+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"extends": "./base.json",
3+
"packages": [
4+
"recursive-pkgs/*"
5+
],
6+
"command": {
7+
"list": {
8+
"private": false
9+
}
10+
}
11+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "unresolved",
3+
"version": "1.0.0"
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "local-package",
3+
"version": "1.0.0"
4+
}

core/project/__fixtures__/extends/node_modules/local-package/config.js

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/project/__fixtures__/extends/node_modules/local-package/package.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/project/__fixtures__/extends/node_modules/local-package/subpath.js

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "extends",
3+
"version": "0.0.0-root",
4+
"private": true,
5+
"devDependencies": {
6+
"local-package": "2.0.0"
7+
}
8+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "pkg-prop",
3+
"version": "0.0.0-root",
4+
"private": true,
5+
"lerna": {
6+
"loglevel": "success",
7+
"command": {
8+
"publish": {
9+
"loglevel": "verbose"
10+
}
11+
},
12+
"version": "1.0.0"
13+
}
14+
}

core/project/__tests__/project.test.js

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,9 @@ describe("Project", () => {
6666
});
6767

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

72-
expect(repo.config).toEqual({});
71+
expect(new Project().config).toEqual({});
7372
});
7473

7574
it("errors when lerna.json is not valid JSON", async () => {
@@ -84,6 +83,91 @@ describe("Project", () => {
8483
expect(err.prefix).toBe("JSONError");
8584
}
8685
});
86+
87+
it("returns parsed rootPkg.lerna", async () => {
88+
const cwd = await initFixture("pkg-prop");
89+
const project = new Project(cwd);
90+
91+
expect(project.config).toEqual({
92+
command: {
93+
publish: {
94+
loglevel: "verbose",
95+
},
96+
},
97+
loglevel: "success",
98+
version: "1.0.0",
99+
});
100+
});
101+
102+
it("extends local shared config", async () => {
103+
const cwd = await initFixture("extends");
104+
const project = new Project(cwd);
105+
106+
expect(project.config).toEqual({
107+
packages: ["custom-local/*"],
108+
version: "1.0.0",
109+
});
110+
});
111+
112+
it("extends local shared config subpath", async () => {
113+
const cwd = await initFixture("extends");
114+
115+
await fs.writeJSON(path.resolve(cwd, "lerna.json"), {
116+
extends: "local-package/subpath",
117+
version: "1.0.0",
118+
});
119+
120+
const project = new Project(cwd);
121+
122+
expect(project.config).toEqual({
123+
packages: ["subpath-local/*"],
124+
version: "1.0.0",
125+
});
126+
});
127+
128+
it("extends config recursively", async () => {
129+
const cwd = await initFixture("extends-recursive");
130+
const project = new Project(cwd);
131+
132+
expect(project.config).toEqual({
133+
command: {
134+
list: {
135+
json: true,
136+
private: false,
137+
},
138+
},
139+
packages: ["recursive-pkgs/*"],
140+
version: "1.0.0",
141+
});
142+
});
143+
144+
it("throws an error when extend target is unresolvable", async () => {
145+
const cwd = await initFixture("extends-unresolved");
146+
147+
try {
148+
// eslint-disable-next-line no-unused-vars
149+
const project = new Project(cwd);
150+
console.log(project);
151+
} catch (err) {
152+
expect(err.message).toMatch("must be locally-resolvable");
153+
}
154+
155+
expect.assertions(1);
156+
});
157+
158+
it("throws an error when extend target is circular", async () => {
159+
const cwd = await initFixture("extends-circular");
160+
161+
try {
162+
// eslint-disable-next-line no-unused-vars
163+
const project = new Project(cwd);
164+
console.log(project);
165+
} catch (err) {
166+
expect(err.message).toMatch("cannot be circular");
167+
}
168+
169+
expect.assertions(1);
170+
});
87171
});
88172

89173
describe("get .version", () => {

core/project/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const writeJsonFile = require("write-json-file");
1010

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

1415
class Project {
1516
constructor(cwd) {
@@ -36,6 +37,8 @@ class Project {
3637
delete obj.config.commands;
3738
}
3839

40+
obj.config = applyExtends(obj.config, path.dirname(obj.filepath));
41+
3942
return obj;
4043
},
4144
});

core/project/lib/apply-extends.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use strict";
2+
3+
const path = require("path");
4+
const resolveFrom = require("resolve-from");
5+
const ValidationError = require("@lerna/validation-error");
6+
const shallowExtend = require("./shallow-extend");
7+
8+
module.exports = applyExtends;
9+
10+
function applyExtends(config, cwd, seen = new Set()) {
11+
let defaultConfig = {};
12+
13+
if ("extends" in config) {
14+
let pathToDefault;
15+
16+
try {
17+
pathToDefault = resolveFrom(cwd, config.extends);
18+
} catch (err) {
19+
throw new ValidationError("ERESOLVED", "Config .extends must be locally-resolvable", err);
20+
}
21+
22+
if (seen.has(pathToDefault)) {
23+
throw new ValidationError("ECIRCULAR", "Config .extends cannot be circular", seen);
24+
}
25+
26+
seen.add(pathToDefault);
27+
28+
// eslint-disable-next-line import/no-dynamic-require, global-require
29+
defaultConfig = require(pathToDefault);
30+
delete config.extends; // eslint-disable-line no-param-reassign
31+
32+
defaultConfig = applyExtends(defaultConfig, path.dirname(pathToDefault), seen);
33+
}
34+
35+
return shallowExtend(config, defaultConfig);
36+
}

core/project/lib/shallow-extend.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"use strict";
2+
3+
module.exports = shallowExtend;
4+
5+
function shallowExtend(json, defaults = {}) {
6+
return Object.keys(json).reduce((obj, key) => {
7+
const val = json[key];
8+
9+
if (Array.isArray(val)) {
10+
// always clobber arrays, merging isn't worth unexpected complexity
11+
obj[key] = val.slice();
12+
} else if (val && typeof val === "object") {
13+
obj[key] = shallowExtend(val, obj[key]);
14+
} else {
15+
obj[key] = val;
16+
}
17+
18+
return obj;
19+
}, defaults);
20+
}

core/project/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"url": "https://github.com/evocateur"
1414
},
1515
"files": [
16-
"index.js"
16+
"index.js",
17+
"lib"
1718
],
1819
"main": "index.js",
1920
"engines": {
@@ -37,6 +38,7 @@
3738
"glob-parent": "^3.1.0",
3839
"load-json-file": "^4.0.0",
3940
"npmlog": "^4.1.2",
41+
"resolve-from": "^4.0.0",
4042
"write-json-file": "^2.3.0"
4143
}
4244
}

package-lock.json

Lines changed: 11 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)