diff --git a/index.js b/index.js index 9e7957b..92c437b 100644 --- a/index.js +++ b/index.js @@ -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'); @@ -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); } }; @@ -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]; @@ -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) : []; @@ -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, @@ -145,6 +221,7 @@ Liftoff.prototype.buildEnvironment = function (opts) { modulePath: modulePath, modulePackage: modulePackage || {}, configFiles: configFiles, + config: config, }; }; diff --git a/lib/array_find.js b/lib/array_find.js new file mode 100644 index 0000000..71b2ca5 --- /dev/null +++ b/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; diff --git a/lib/needs_lookup.js b/lib/needs_lookup.js new file mode 100644 index 0000000..17d2d7d --- /dev/null +++ b/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; diff --git a/test/fixtures/configfiles-extends/circular1.json b/test/fixtures/configfiles-extends/circular1.json new file mode 100644 index 0000000..126f871 --- /dev/null +++ b/test/fixtures/configfiles-extends/circular1.json @@ -0,0 +1,3 @@ +{ + "extends": "./circular2" +} diff --git a/test/fixtures/configfiles-extends/circular2.json b/test/fixtures/configfiles-extends/circular2.json new file mode 100644 index 0000000..e97a47b --- /dev/null +++ b/test/fixtures/configfiles-extends/circular2.json @@ -0,0 +1,3 @@ +{ + "extends": "./circular1" +} diff --git a/test/fixtures/configfiles-extends/empty.json b/test/fixtures/configfiles-extends/empty.json new file mode 100644 index 0000000..c871e9e --- /dev/null +++ b/test/fixtures/configfiles-extends/empty.json @@ -0,0 +1,4 @@ +{ + "extends": "", + "ccc": "ddd" +} diff --git a/test/fixtures/configfiles-extends/extend-config.json b/test/fixtures/configfiles-extends/extend-config.json new file mode 100644 index 0000000..a019577 --- /dev/null +++ b/test/fixtures/configfiles-extends/extend-config.json @@ -0,0 +1,4 @@ +{ + "aaa": "CCC", + "bbb": "BBB" +} diff --git a/test/fixtures/configfiles-extends/extend-missing.json b/test/fixtures/configfiles-extends/extend-missing.json new file mode 100644 index 0000000..d520255 --- /dev/null +++ b/test/fixtures/configfiles-extends/extend-missing.json @@ -0,0 +1,4 @@ +{ + "extends": "./local-missing", + "ccc": "ddd" +} diff --git a/test/fixtures/configfiles-extends/load-empty.json b/test/fixtures/configfiles-extends/load-empty.json new file mode 100644 index 0000000..4596891 --- /dev/null +++ b/test/fixtures/configfiles-extends/load-empty.json @@ -0,0 +1,4 @@ +{ + "extends": "./empty", + "aaa": "bbb" +} diff --git a/test/fixtures/configfiles-extends/local-missing.json b/test/fixtures/configfiles-extends/local-missing.json new file mode 100644 index 0000000..6234773 --- /dev/null +++ b/test/fixtures/configfiles-extends/local-missing.json @@ -0,0 +1,4 @@ +{ + "extends": "./not-exists", + "aaa": "bbb" +} diff --git a/test/fixtures/configfiles-extends/missing-invalid-obj.json b/test/fixtures/configfiles-extends/missing-invalid-obj.json new file mode 100644 index 0000000..72a47c8 --- /dev/null +++ b/test/fixtures/configfiles-extends/missing-invalid-obj.json @@ -0,0 +1,5 @@ +{ + "extends": { + "foo": "bar" + } +} diff --git a/test/fixtures/configfiles-extends/missing-name-obj.json b/test/fixtures/configfiles-extends/missing-name-obj.json new file mode 100644 index 0000000..e5f82e7 --- /dev/null +++ b/test/fixtures/configfiles-extends/missing-name-obj.json @@ -0,0 +1,6 @@ +{ + "extends": { + "name": "not-exists" + }, + "foo": "bar" +} diff --git a/test/fixtures/configfiles-extends/missing-path-obj.json b/test/fixtures/configfiles-extends/missing-path-obj.json new file mode 100644 index 0000000..fe5d998 --- /dev/null +++ b/test/fixtures/configfiles-extends/missing-path-obj.json @@ -0,0 +1,6 @@ +{ + "extends": { + "path": "./not-exists" + }, + "foo": "bar" +} diff --git a/test/fixtures/configfiles-extends/npm-missing.json b/test/fixtures/configfiles-extends/npm-missing.json new file mode 100644 index 0000000..4c983b0 --- /dev/null +++ b/test/fixtures/configfiles-extends/npm-missing.json @@ -0,0 +1,4 @@ +{ + "extends": "not-installed", + "foo": "bar" +} diff --git a/test/fixtures/configfiles-extends/null.json b/test/fixtures/configfiles-extends/null.json new file mode 100644 index 0000000..9fd31cf --- /dev/null +++ b/test/fixtures/configfiles-extends/null.json @@ -0,0 +1,3 @@ +{ + "extends": null +} diff --git a/test/fixtures/configfiles-extends/testconfig.json b/test/fixtures/configfiles-extends/testconfig.json new file mode 100644 index 0000000..da13a7c --- /dev/null +++ b/test/fixtures/configfiles-extends/testconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "./extend-config", + "aaa": "AAA" +} diff --git a/test/fixtures/configfiles-extends/throws.js b/test/fixtures/configfiles-extends/throws.js new file mode 100644 index 0000000..e726442 --- /dev/null +++ b/test/fixtures/configfiles-extends/throws.js @@ -0,0 +1 @@ +throw new Error('Kaboom'); diff --git a/test/fixtures/configfiles/testconfig.json b/test/fixtures/configfiles/testconfig.json new file mode 100644 index 0000000..e966ffb --- /dev/null +++ b/test/fixtures/configfiles/testconfig.json @@ -0,0 +1,3 @@ +{ + "aaa": "AAA" +} diff --git a/test/index.js b/test/index.js index 37120cf..aa6264e 100644 --- a/test/index.js +++ b/test/index.js @@ -1,4 +1,5 @@ var path = require('path'); +var Module = require('module'); var exec = require('child_process').exec; var expect = require('expect'); @@ -21,9 +22,44 @@ var app = new Liftoff({ searchPaths: ['test/fixtures/search_path'], }); +// TODO: Consolidate between rechoir & liftoff +// save the original Module._extensions +var originalExtensions = Object.keys(Module._extensions); +var original = originalExtensions.reduce(function (result, key) { + result[key] = require.extensions[key]; + return result; +}, {}); +// save the original cache keys +var originalCacheKeys = Object.keys(require.cache); + +function cleanupCache(key) { + if (originalCacheKeys.indexOf(key) === -1) { + delete require.cache[key]; + } +} + +function cleanupExtensions(ext) { + if (originalExtensions.indexOf(ext) === -1) { + delete Module._extensions[ext]; + } else { + Module._extensions[ext] = original[ext]; + } +} + +function cleanup(done) { + // restore the require.cache to startup state + Object.keys(require.cache).forEach(cleanupCache); + // restore the original Module._extensions + Object.keys(Module._extensions).forEach(cleanupExtensions); + + done(); +} + describe('Liftoff', function () { this.timeout(5000); + beforeEach(cleanup); + describe('buildEnvironment', function () { it('should locate local module using cwd if no config is found', function (done) { var test = new Liftoff({ name: 'chai' }); @@ -93,7 +129,7 @@ describe('Liftoff', function () { it("should set cwd to match the directory of the config file as long as cwd wasn't explicitly provided", function (done) { expect(app.buildEnvironment().cwd).toEqual( - path.resolve('test/fixtures/search_path') + path.resolve(__dirname, './fixtures/search_path') ); done(); }); @@ -272,7 +308,10 @@ describe('Liftoff', function () { expect(err).toEqual(null); expect(stderr).toEqual( [ - path.resolve('test/fixtures/prepare-execute/v8flags_config.js'), + path.resolve( + __dirname, + './fixtures/prepare-execute/v8flags_config.js' + ), '123', ].join(' ') + '\n' ); @@ -291,7 +330,10 @@ describe('Liftoff', function () { expect(err).toEqual(null); expect(stderr).toEqual( [ - path.resolve('test/fixtures/prepare-execute/v8flags_config.js'), + path.resolve( + __dirname, + './fixtures/prepare-execute/v8flags_config.js' + ), '123', 'abc', ].join(' ') + '\n' @@ -334,7 +376,10 @@ describe('Liftoff', function () { expect(err).toEqual(null); expect(stderr).toEqual( [ - path.resolve('test/fixtures/prepare-execute/nodeflags_only.js'), + path.resolve( + __dirname, + './fixtures/prepare-execute/nodeflags_only.js' + ), '123', ].join(' ') + '\n' ); @@ -462,46 +507,42 @@ describe('Liftoff', function () { }); }); + it('excludes files if value is not an array', function (done) { + var app = new Liftoff({ + name: 'myapp', + configFiles: { + foo: 'bar', + }, + }); + app.prepare({}, function (env) { + expect(env.configFiles).toEqual({}); + done(); + }); + }); + it('should find multiple files if specified', function (done) { var app = new Liftoff({ name: 'myapp', configFiles: { - index: { - currentdir: '.', - test: { - path: 'test/fixtures/configfiles', - }, - findingup: { - path: 'test', - cwd: 'test/fixtures/configfiles', - findUp: true, - }, - }, - package: { - currentdir: '.', - test: { - path: 'test/fixtures/configfiles', - }, - findingup: { - path: 'test', - cwd: 'test/fixtures/configfiles', - findUp: true, - }, - }, + testconfig: [ + '.', + { path: 'test/fixtures/configfiles' }, + { path: 'test', cwd: 'text/fixtures/configfiles', findUp: true }, + ], + package: [ + '.', + { path: 'test/fixtures/configfiles' }, + { path: 'test', cwd: 'text/fixtures/configfiles', findUp: true }, + ], }, }); app.prepare({}, function (env) { expect(env.configFiles).toEqual({ - index: { - currentdir: path.resolve('./index.js'), - test: path.resolve('./test/fixtures/configfiles/index.json'), - findingup: path.resolve('./test/index.js'), - }, - package: { - currentdir: path.resolve('./package.json'), - test: null, - findingup: null, - }, + testconfig: path.resolve( + __dirname, + './fixtures/configfiles/testconfig.json' + ), + package: path.resolve(__dirname, '../package.json'), }); done(); }); @@ -511,12 +552,7 @@ describe('Liftoff', function () { var app = new Liftoff({ name: 'myapp', configFiles: { - index: { - cwd: { - path: '.', - extensions: ['.js', '.json'], - }, - }, + testconfig: [{ path: '.', extensions: ['.js', '.json'] }], }, }); app.prepare( @@ -525,54 +561,111 @@ describe('Liftoff', function () { }, function (env) { expect(env.configFiles).toEqual({ - index: { - cwd: path.resolve('./test/fixtures/configfiles/index.json'), - }, + testconfig: path.resolve( + __dirname, + './fixtures/configfiles/testconfig.json' + ), + }); + done(); + } + ); + }); + + it('should use dirname of configPath if no cwd is specified', function (done) { + var app = new Liftoff({ + name: 'myapp', + configFiles: { + testconfig: [{ path: '.', extensions: ['.js', '.json'] }], + }, + }); + app.prepare( + { + configPath: 'test/fixtures/configfiles/myapp.js', + }, + function (env) { + expect(env.configFiles).toEqual({ + testconfig: path.resolve( + __dirname, + './fixtures/configfiles/testconfig.json' + ), }); done(); } ); }); - it('should use default extensions if not specified', function (done) { + it('uses default extensions if not specified (.md)', function (done) { + var app = new Liftoff({ + extensions: { + '.md': './test/fixtures/configfiles/require-md', + }, + name: 'myapp', + configFiles: { + README: [{ path: '.' }], + }, + }); + app.prepare({}, function (env) { + expect(env.configFiles).toEqual({ + README: path.resolve(__dirname, '../README.md'), + }); + done(); + }); + }); + + it('use default extensions if not specified (.txt)', function (done) { + var app = new Liftoff({ + extensions: { + '.txt': './test/fixtures/configfiles/require-txt', + }, + name: 'myapp', + configFiles: { + README: [{ path: 'test/fixtures/configfiles' }], + }, + }); + app.prepare({}, function (env) { + expect(env.configFiles).toEqual({ + README: path.resolve(__dirname, './fixtures/configfiles/README.txt'), + }); + done(); + }); + }); + + it('does not use default extensions if specified (.js)', function (done) { var app = new Liftoff({ extensions: { '.md': null, '.txt': null }, name: 'myapp', configFiles: { - README: { - markdown: { - path: '.', - }, - text: { - path: 'test/fixtures/configfiles', - }, - markdown2: { - path: '.', - extensions: ['.json', '.js'], - }, - text2: { - path: 'test/fixtures/configfiles', - extensions: ['.json', '.js'], - }, - }, + README: [{ path: '.', extensions: ['.js'] }], }, }); app.prepare({}, function (env) { expect(env.configFiles).toEqual({ - README: { - markdown: path.resolve('./README.md'), - text: path.resolve('./test/fixtures/configfiles/README.txt'), - markdown2: null, - text2: null, - }, + README: undefined, }); done(); }); }); - it('should use specified loaders', function (done) { + it('does not use default extensions if specified (.json)', function (done) { + var app = new Liftoff({ + extensions: { '.md': null, '.txt': null }, + name: 'myapp', + configFiles: { + README: [ + { path: 'test/fixtures/configfiles', extensions: ['.json'] }, + ], + }, + }); + app.prepare({}, function (env) { + expect(env.configFiles).toEqual({ + README: undefined, + }); + done(); + }); + }); + + it('should use specified loaders (.md)', function (done) { var logRequire = []; - var logFailure = []; var app = new Liftoff({ extensions: { @@ -580,41 +673,105 @@ describe('Liftoff', function () { }, name: 'myapp', configFiles: { - README: { - text_null: { - path: 'test/fixtures/configfiles', - }, - text_err: { + README: [{ path: '.' }], + }, + }); + app.on('loader:success', function (moduleName, module) { + logRequire.push({ moduleName: moduleName, module: module }); + }); + app.prepare({}, function (env) { + expect(env.configFiles).toEqual({ + README: path.resolve(__dirname, '../README.md'), + }); + + expect(logRequire.length).toEqual(1); + expect(logRequire[0].moduleName).toEqual( + './test/fixtures/configfiles/require-md' + ); + + expect(require(env.configFiles.README)).toEqual( + 'Load README.md by require-md' + ); + done(); + }); + }); + + it('should use specified loaders (.txt)', function (done) { + var logRequire = []; + + var app = new Liftoff({ + name: 'myapp', + configFiles: { + README: [ + { path: 'test/fixtures/configfiles', - extensions: { - '.txt': './test/fixtures/configfiles/require-non-exist', - }, + extensions: { '.txt': './test/fixtures/configfiles/require-txt' }, }, - text: { + ], + }, + }); + app.on('loader:success', function (moduleName, module) { + logRequire.push({ moduleName: moduleName, module: module }); + }); + app.prepare({}, function (env) { + expect(env.configFiles).toEqual({ + README: path.resolve(__dirname, './fixtures/configfiles/README.txt'), + }); + + expect(logRequire.length).toEqual(1); + expect(logRequire[0].moduleName).toEqual( + './test/fixtures/configfiles/require-txt' + ); + + expect(require(env.configFiles.README)).toEqual( + 'Load README.txt by require-txt' + ); + done(); + }); + }); + + it('emits `loader:failure` but still resolves with invalid loaders', function (done) { + var logFailure = []; + + var app = new Liftoff({ + name: 'myapp', + configFiles: { + README: [ + { path: 'test/fixtures/configfiles', - extensions: { - '.txt': './test/fixtures/configfiles/require-txt', - }, - }, - markdown: { - path: '.', - }, - markdown_badext: { - path: '.', - extensions: { - '.txt': './test/fixtures/configfiles/require-txt', - }, - }, - markdown_badext2: { - path: '.', extensions: { '.txt': './test/fixtures/configfiles/require-non-exist', }, }, - }, - // Intrinsic extension-loader mappings are prioritized. - index: { - test: { + ], + }, + }); + app.on('loader:failure', function (moduleName, error) { + logFailure.push({ moduleName: moduleName, error: error }); + }); + app.prepare({}, function (env) { + expect(env.configFiles).toEqual({ + README: path.resolve(__dirname, './fixtures/configfiles/README.txt'), + }); + + expect(logFailure.length).toEqual(1); + expect(logFailure[0].moduleName).toEqual( + './test/fixtures/configfiles/require-non-exist' + ); + + done(); + }); + }); + + it('prioritizes intrinsic extension-loader mappings', function (done) { + var logRequire = []; + var logFailure = []; + + var app = new Liftoff({ + name: 'myapp', + configFiles: { + testconfig: [ + { path: 'test/fixtures/configfiles', extensions: { // ignored @@ -622,7 +779,7 @@ describe('Liftoff', function () { '.json': './test/fixtures/configfiles/require-json', }, }, - }, + ], }, }); app.on('loader:failure', function (moduleName, error) { @@ -633,41 +790,227 @@ describe('Liftoff', function () { }); app.prepare({}, function (env) { expect(env.configFiles).toEqual({ - README: { - text: path.resolve('./test/fixtures/configfiles/README.txt'), - text_null: null, - text_err: path.resolve('./test/fixtures/configfiles/README.txt'), - markdown: path.resolve('./README.md'), - markdown_badext: null, - markdown_badext2: null, + testconfig: path.resolve( + __dirname, + './fixtures/configfiles/testconfig.json' + ), + }); + + expect(logRequire.length).toEqual(0); + expect(logFailure.length).toEqual(0); + + expect(require(env.configFiles.testconfig)).toEqual({ aaa: 'AAA' }); + done(); + }); + }); + }); + + describe('config', function () { + it('is empty if `configFiles` is not specified', function (done) { + var app = new Liftoff({ + name: 'myapp', + }); + app.prepare({}, function (env) { + expect(env.config).toEqual({}); + done(); + }); + }); + + it('loads config if a `configFiles` is found and makes it available with the same key on `config`', function (done) { + var app = new Liftoff({ + name: 'myapp', + configFiles: { + testconfig: ['test/fixtures/configfiles'], + }, + }); + app.prepare({}, function (env) { + expect(env.config).toEqual({ + testconfig: { + aaa: 'AAA', }, - index: { - test: path.resolve('./test/fixtures/configfiles/index.json'), + }); + done(); + }); + }); + + it('loads and merges a config file specified if loaded file provides `extends` property', function (done) { + var app = new Liftoff({ + name: 'myapp', + configFiles: { + testconfig: ['test/fixtures/configfiles-extends'], + }, + }); + app.prepare({}, function (env) { + expect(env.config).toEqual({ + testconfig: { + aaa: 'AAA', // Comes from the base, which overrode `aaa: 'CCC'` in the `extends` + bbb: 'BBB', // Comes from the `extends` }, }); + done(); + }); + }); - expect(logRequire.length).toEqual(2); - expect(logRequire[0].moduleName).toEqual( - './test/fixtures/configfiles/require-txt' - ); - expect(logRequire[1].moduleName).toEqual( - './test/fixtures/configfiles/require-md' - ); + it('throws error on circular extends', function (done) { + var app = new Liftoff({ + name: 'myapp', + configFiles: { + circular1: ['test/fixtures/configfiles-extends'], + }, + }); + var circPath = path.resolve( + __dirname, + './fixtures/configfiles-extends/circular1.json' + ); + expect(function () { + app.prepare({}, function () {}); + }).toThrow(circPath); // Ensure that the error includes the circular path + done(); + }); - expect(logFailure.length).toEqual(1); - expect(logFailure[0].moduleName).toEqual( - './test/fixtures/configfiles/require-non-exist' - ); + it('gracefully handles a null-ish extends', function (done) { + var app = new Liftoff({ + name: 'myapp', + configFiles: { + null: ['test/fixtures/configfiles-extends'], + }, + }); + app.prepare({}, function (env) { + expect(env.config).toEqual({ + null: {}, + }); + done(); + }); + }); - expect(require(env.configFiles.README.markdown)).toEqual( - 'Load README.md by require-md' - ); - expect(require(env.configFiles.README.text)).toEqual( - 'Load README.txt by require-txt' - ); - expect(require(env.configFiles.index.test)).toEqual({ aaa: 'AAA' }); + it('stops processing extends on an empty (or null-ish) extends', function (done) { + var app = new Liftoff({ + name: 'myapp', + configFiles: { + 'load-empty': ['test/fixtures/configfiles-extends'], + }, + }); + app.prepare({}, function (env) { + expect(env.config).toEqual({ + 'load-empty': { + aaa: 'bbb', + ccc: 'ddd', + }, + }); done(); }); }); + + it('throws upon extends of missing local file', function (done) { + var app = new Liftoff({ + name: 'myapp', + configFiles: { + 'local-missing': ['test/fixtures/configfiles-extends'], + }, + }); + var missingPath = path.resolve( + __dirname, + './fixtures/configfiles-extends/not-exists' + ); + expect(function () { + app.prepare({}, function () {}); + }).toThrow(missingPath); + done(); + }); + + it('throws (with correct path) upon extends of missing deep in tree', function (done) { + var app = new Liftoff({ + name: 'myapp', + configFiles: { + 'extend-missing': ['test/fixtures/configfiles-extends'], + }, + }); + var missingPath = path.resolve( + __dirname, + './fixtures/configfiles-extends/not-exists' + ); + expect(function () { + app.prepare({}, function () {}); + }).toThrow(missingPath); + done(); + }); + + it('throws (with correct path) upon extends using `fined` object with `path`', function (done) { + var app = new Liftoff({ + name: 'myapp', + configFiles: { + 'missing-path-obj': ['test/fixtures/configfiles-extends'], + }, + }); + var missingPath = path.resolve( + __dirname, + './fixtures/configfiles-extends/not-exists' + ); + expect(function () { + app.prepare({}, function () {}); + }).toThrow(missingPath); + done(); + }); + + it('throws (with correct path) upon extends using `fined` object with `name`', function (done) { + var app = new Liftoff({ + name: 'myapp', + configFiles: { + 'missing-name-obj': ['test/fixtures/configfiles-extends'], + }, + }); + var missingPath = path.resolve( + __dirname, + './fixtures/configfiles-extends/not-exists' + ); + expect(function () { + app.prepare({}, function () {}); + }).toThrow(missingPath); + done(); + }); + + it('throws (without path) upon extends using invalid `fined` object', function (done) { + var app = new Liftoff({ + name: 'myapp', + configFiles: { + 'missing-invalid-obj': ['test/fixtures/configfiles-extends'], + }, + }); + expect(function () { + app.prepare({}, function () {}); + }).toThrow(/^Unable to locate one of your extends\.$/); // Ensure the error doesn't contain path + done(); + }); + + it('throws upon extends of missing npm module', function (done) { + var app = new Liftoff({ + name: 'myapp', + configFiles: { + 'npm-missing': ['test/fixtures/configfiles-extends'], + }, + }); + var missingModule = 'not-installed'; + expect(function () { + app.prepare({}, function () {}); + }).toThrow(missingModule); + done(); + }); + + it('throws upon extends if loading file errors', function (done) { + var app = new Liftoff({ + name: 'myapp', + configFiles: { + throws: ['test/fixtures/configfiles-extends'], + }, + }); + var errModulePath = path.resolve( + __dirname, + './fixtures/configfiles-extends/throws.js' + ); + expect(function () { + app.prepare({}, function () {}); + }).toThrow(errModulePath); + done(); + }); }); });