Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat!: Support extends syntax in config files (#103)
  • Loading branch information
phated committed Nov 22, 2021
1 parent d671e76 commit 68c9db7
Show file tree
Hide file tree
Showing 19 changed files with 665 additions and 151 deletions.
127 changes: 102 additions & 25 deletions index.js
Expand Up @@ -10,8 +10,10 @@ var mapValues = require('object.map');
var fined = require('fined');

var findCwd = require('./lib/find_cwd');
var arrayFind = require('./lib/array_find');
var findConfig = require('./lib/find_config');
var fileSearch = require('./lib/file_search');
var needsLookup = require('./lib/needs_lookup');
var parseOptions = require('./lib/parse_options');
var silentRequire = require('./lib/silent_require');
var buildConfigName = require('./lib/build_config_name');
Expand All @@ -24,14 +26,14 @@ function Liftoff(opts) {
}
util.inherits(Liftoff, EE);

Liftoff.prototype.requireLocal = function (module, basedir) {
Liftoff.prototype.requireLocal = function (moduleName, basedir) {
try {
this.emit('preload:before', module);
var result = require(resolve.sync(module, { basedir: basedir }));
this.emit('preload:success', module, result);
this.emit('preload:before', moduleName);
var result = require(resolve.sync(moduleName, { basedir: basedir }));
this.emit('preload:success', moduleName, result);
return result;
} catch (e) {
this.emit('preload:failure', module, e);
this.emit('preload:failure', moduleName, e);
}
};

Expand All @@ -52,6 +54,98 @@ Liftoff.prototype.buildEnvironment = function (opts) {
// calculate current cwd
var cwd = findCwd(opts);

var exts = this.extensions;
var eventEmitter = this;

function findAndRegisterLoader(pathObj, defaultObj) {
var found = fined(pathObj, defaultObj);
if (!found) {
return;
}
if (isPlainObject(found.extension)) {
registerLoader(eventEmitter, found.extension, found.path, cwd);
}
return found.path;
}

function getModulePath(cwd, xtends) {
// If relative, we need to use fined to look up the file. If not, assume a node_module
if (needsLookup(xtends)) {
var defaultObj = { cwd: cwd, extensions: exts };
// Using `xtends` like this should allow people to use a string or any object that fined accepts
var foundPath = findAndRegisterLoader(xtends, defaultObj);
if (!foundPath) {
var name;
if (typeof xtends === 'string') {
name = xtends;
} else {
name = xtends.path || xtends.name;
}
var msg = 'Unable to locate one of your extends.';
if (name) {
msg += ' Looking for file: ' + path.resolve(cwd, name);
}
throw new Error(msg);
}
return foundPath;
}

return xtends;
}

var visited = {};
function loadConfig(cwd, xtends, preferred) {
var configFilePath = getModulePath(cwd, xtends);

if (visited[configFilePath]) {
throw new Error(
'We encountered a circular extend for file: ' +
configFilePath +
'. Please remove the recursive extends.'
);
}
var configFile;
try {
configFile = require(configFilePath);
} catch (e) {
// TODO: Consider surfacing the `require` error
throw new Error(
'Encountered error when loading config file: ' + configFilePath
);
}
visited[configFilePath] = true;
if (configFile && configFile.extends) {
var nextCwd = path.dirname(configFilePath);
return loadConfig(nextCwd, configFile.extends, configFile);
}
// Always extend into an empty object so we can call `delete` on `config.extends`
var config = extend(true /* deep */, {}, configFile, preferred);
delete config.extends;
return config;
}

var configFiles = {};
if (isPlainObject(this.configFiles)) {
configFiles = mapValues(this.configFiles, function (searchPaths, fileStem) {
var defaultObj = { name: fileStem, cwd: cwd, extensions: exts };

var foundPath = arrayFind(searchPaths, function (pathObj) {
return findAndRegisterLoader(pathObj, defaultObj);
});

return foundPath;
});
}

var config = mapValues(configFiles, function (startingLocation) {
var defaultConfig = {};
if (!startingLocation) {
return defaultConfig;
}

return loadConfig(cwd, startingLocation, defaultConfig);
});

// if cwd was provided explicitly, only use it for searching config
if (opts.cwd) {
searchPaths = [cwd];
Expand Down Expand Up @@ -85,8 +179,8 @@ Liftoff.prototype.buildEnvironment = function (opts) {

// TODO: break this out into lib/
// locate local module and package next to config or explicitly provided cwd
/* eslint one-var: 0 */
var modulePath, modulePackage;
var modulePath;
var modulePackage;
try {
var delim = path.delimiter;
var paths = process.env.NODE_PATH ? process.env.NODE_PATH.split(delim) : [];
Expand Down Expand Up @@ -117,24 +211,6 @@ Liftoff.prototype.buildEnvironment = function (opts) {
}
}

var exts = this.extensions;
var eventEmitter = this;

var configFiles = {};
if (isPlainObject(this.configFiles)) {
var notfound = { path: null };
configFiles = mapValues(this.configFiles, function (prop, name) {
var defaultObj = { name: name, cwd: cwd, extensions: exts };
return mapValues(prop, function (pathObj) {
var found = fined(pathObj, defaultObj) || notfound;
if (isPlainObject(found.extension)) {
registerLoader(eventEmitter, found.extension, found.path, cwd);
}
return found.path;
});
});
}

return {
cwd: cwd,
preload: preload,
Expand All @@ -145,6 +221,7 @@ Liftoff.prototype.buildEnvironment = function (opts) {
modulePath: modulePath,
modulePackage: modulePackage || {},
configFiles: configFiles,
config: config,
};
};

Expand Down
18 changes: 18 additions & 0 deletions lib/array_find.js
@@ -0,0 +1,18 @@
'use strict';

function arrayFind(arr, fn) {
if (!Array.isArray(arr)) {
return;
}

var idx = 0;
while (idx < arr.length) {
var result = fn(arr[idx]);
if (result) {
return result;
}
idx++;
}
}

module.exports = arrayFind;
18 changes: 18 additions & 0 deletions lib/needs_lookup.js
@@ -0,0 +1,18 @@
'use strict';

var isPlainObject = require('is-plain-object').isPlainObject;

function needsLookup(xtends) {
if (typeof xtends === 'string' && xtends[0] === '.') {
return true;
}

if (isPlainObject(xtends)) {
// Objects always need lookup because they can't be used with `require()`
return true;
}

return false;
}

module.exports = needsLookup;
3 changes: 3 additions & 0 deletions test/fixtures/configfiles-extends/circular1.json
@@ -0,0 +1,3 @@
{
"extends": "./circular2"
}
3 changes: 3 additions & 0 deletions test/fixtures/configfiles-extends/circular2.json
@@ -0,0 +1,3 @@
{
"extends": "./circular1"
}
4 changes: 4 additions & 0 deletions test/fixtures/configfiles-extends/empty.json
@@ -0,0 +1,4 @@
{
"extends": "",
"ccc": "ddd"
}
4 changes: 4 additions & 0 deletions test/fixtures/configfiles-extends/extend-config.json
@@ -0,0 +1,4 @@
{
"aaa": "CCC",
"bbb": "BBB"
}
4 changes: 4 additions & 0 deletions test/fixtures/configfiles-extends/extend-missing.json
@@ -0,0 +1,4 @@
{
"extends": "./local-missing",
"ccc": "ddd"
}
4 changes: 4 additions & 0 deletions test/fixtures/configfiles-extends/load-empty.json
@@ -0,0 +1,4 @@
{
"extends": "./empty",
"aaa": "bbb"
}
4 changes: 4 additions & 0 deletions test/fixtures/configfiles-extends/local-missing.json
@@ -0,0 +1,4 @@
{
"extends": "./not-exists",
"aaa": "bbb"
}
5 changes: 5 additions & 0 deletions test/fixtures/configfiles-extends/missing-invalid-obj.json
@@ -0,0 +1,5 @@
{
"extends": {
"foo": "bar"
}
}
6 changes: 6 additions & 0 deletions test/fixtures/configfiles-extends/missing-name-obj.json
@@ -0,0 +1,6 @@
{
"extends": {
"name": "not-exists"
},
"foo": "bar"
}
6 changes: 6 additions & 0 deletions test/fixtures/configfiles-extends/missing-path-obj.json
@@ -0,0 +1,6 @@
{
"extends": {
"path": "./not-exists"
},
"foo": "bar"
}
4 changes: 4 additions & 0 deletions test/fixtures/configfiles-extends/npm-missing.json
@@ -0,0 +1,4 @@
{
"extends": "not-installed",
"foo": "bar"
}
3 changes: 3 additions & 0 deletions test/fixtures/configfiles-extends/null.json
@@ -0,0 +1,3 @@
{
"extends": null
}
4 changes: 4 additions & 0 deletions test/fixtures/configfiles-extends/testconfig.json
@@ -0,0 +1,4 @@
{
"extends": "./extend-config",
"aaa": "AAA"
}
1 change: 1 addition & 0 deletions test/fixtures/configfiles-extends/throws.js
@@ -0,0 +1 @@
throw new Error('Kaboom');
3 changes: 3 additions & 0 deletions test/fixtures/configfiles/testconfig.json
@@ -0,0 +1,3 @@
{
"aaa": "AAA"
}

0 comments on commit 68c9db7

Please sign in to comment.