Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Add extends syntax #103

Merged
merged 23 commits into from Oct 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"
}