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
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Initial work on extends syntax
phated committed Oct 18, 2021
commit 85249441c11046b78325aa47d389b03e4759bd93
102 changes: 83 additions & 19 deletions index.js
Original file line number Diff line number Diff line change
@@ -52,6 +52,87 @@ Liftoff.prototype.buildEnvironment = function (opts) {
// calculate current cwd
var cwd = findCwd(opts);

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

var visited = {};

function getModulePath(cwd, xtends) {
if (typeof xtends === 'string') {
xtends = { path: xtends };
}

// TODO: is-relative
if (xtends.path[0] === '.') {
var defaultObj = { cwd: cwd, extensions: exts };
// Using `xtends` like this should allow people to use a string or any object that fined accepts
var found = fined(xtends, defaultObj);
if (!found) {
// TODO: Should this actually error on not found?
throw new Error('Unable to find extends file: ' + xtends.path);
}
if (isPlainObject(found.extension)) {
registerLoader(eventEmitter, found.extension, found.path, cwd);
}
return found.path;
}

return xtends.path;
}

function loadConfig(cwd, xtends, prev) {
var configFilePath = getModulePath(cwd, xtends);
if (visited[configFilePath]) {
// TODO: should this error on recursive or just exit with the current result??
throw new Error('We encountered a recursive extend for file: ' + configFilePath + '. Please remove the recursive extends.');
}
// TODO: should this error if the module could not be required?
var configFile = silentRequire(configFilePath);
visited[configFilePath] = true;
if (configFile && configFile.extends) {
return loadConfig(path.dirname(configFilePath), configFile.extends, configFile);
}
return extend(true /* deep */, prev, configFile || {});
}

var configFiles = {};
var config = {};
if (isPlainObject(this.configFiles)) {
configFiles = mapValues(this.configFiles, function (searchPaths, name /* key */) {
var defaultObj = { name: name, cwd: cwd, extensions: exts };
var found = searchPaths.reduce(function (result, pathObj) {
// Short circuit once found.
// TODO: `find` utility?
if (result) {
return result;
}

return fined(pathObj, defaultObj);
}, undefined);

if (!found) {
// TODO: what here?
return;
}
if (isPlainObject(found.extension)) {
registerLoader(eventEmitter, found.extension, found.path, cwd);
}
return found.path;
});

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

var config = loadConfig(cwd, startingLocation, defaultConfig);
// TODO: better filter?
delete config.extends;
return config;
});
}

// if cwd was provided explicitly, only use it for searching config
if (opts.cwd) {
searchPaths = [cwd];
@@ -95,7 +176,7 @@ Liftoff.prototype.buildEnvironment = function (opts) {
paths: paths,
});
modulePackage = silentRequire(fileSearch('package.json', [modulePath]));
} catch (e) {}
} catch (e) { }

// if we have a configuration but we failed to find a local module, maybe
// we are developing against ourselves?
@@ -117,24 +198,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 +208,7 @@ Liftoff.prototype.buildEnvironment = function (opts) {
modulePath: modulePath,
modulePackage: modulePackage || {},
configFiles: configFiles,
config: config,
};
};

4 changes: 4 additions & 0 deletions test/fixtures/configfiles-extends/extend-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"aaa": "CCC",
"bbb": "BBB"
}
4 changes: 4 additions & 0 deletions test/fixtures/configfiles-extends/testconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./extend-config",
"aaa": "AAA"
}
3 changes: 3 additions & 0 deletions test/fixtures/configfiles/testconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"aaa": "AAA"
}
298 changes: 243 additions & 55 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -110,11 +110,11 @@ describe('Liftoff', function () {
var fp = path.resolve(cwd, 'package.json');
expect(stdout).toEqual(
JSON.stringify(require(fp)) +
'\n' +
path.resolve(cwd, 'main.js') +
'\n' +
cwd +
'\n'
'\n' +
path.resolve(cwd, 'main.js') +
'\n' +
cwd +
'\n'
);
done();
}
@@ -135,7 +135,7 @@ describe('Liftoff', function () {

it(
'should use `index.js` if `main` property in package.json ' +
'does not exist',
'does not exist',
function (done) {
var fixturesDir = path.resolve(__dirname, 'fixtures');
var cwd = path.resolve(fixturesDir, 'developing_yourself/app2');
@@ -150,11 +150,11 @@ describe('Liftoff', function () {
var fp = './fixtures/developing_yourself/app2/package.json';
expect(stdout).toEqual(
JSON.stringify(require(fp)) +
'\n' +
path.resolve(cwd, 'index.js') +
'\n' +
cwd +
'\n'
'\n' +
path.resolve(cwd, 'index.js') +
'\n' +
cwd +
'\n'
);
done();
}
@@ -411,9 +411,9 @@ describe('Liftoff', function () {
expect(stderr).toEqual('');
expect(stdout).toEqual(
"saw respawn [ '--lazy' ]\n" +
'preload:success coffeescript/register\n' +
'execute\n' +
''
'preload:success coffeescript/register\n' +
'execute\n' +
''
);
done();
}
@@ -466,75 +466,263 @@ describe('Liftoff', function () {
var app = new Liftoff({
name: 'myapp',
configFiles: {
index: {
currentdir: '.',
test: {
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).to.deep.equal({
testconfig: path.resolve('./test/fixtures/configfiles/testconfig.json'),
package: path.resolve('./package.json'),
});
done();
});
});

it('should use default cwd if not specified', function (done) {
var app = new Liftoff({
name: 'myapp',
configFiles: {
testconfig: [
{ path: '.', extensions: ['.js', '.json'] },
],
},
});
app.prepare({
cwd: 'test/fixtures/configfiles',
}, function (env) {
expect(env.configFiles).to.deep.equal({
testconfig: path.resolve('./test/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).to.deep.equal({
testconfig: path.resolve('./test/fixtures/configfiles/testconfig.json'),
});
done();
});
});

it.skip('should use default extensions if not specified', function (done) {
var app = new Liftoff({
extensions: { '.md': null, '.txt': null },
name: 'myapp',
configFiles: {
README: {
markdown: {
path: '.',
},
text: {
path: 'test/fixtures/configfiles',
},
findingup: {
path: 'test',
cwd: 'test/fixtures/configfiles',
findUp: true,
markdown2: {
path: '.',
extensions: ['.json', '.js'],
},
text2: {
path: 'test/fixtures/configfiles',
extensions: ['.json', '.js'],
},
},
package: {
currentdir: '.',
test: {
},
});
app.prepare({}, function (env) {
expect(env.configFiles).to.deep.equal({
README: {
markdown: path.resolve('./README.md'),
text: path.resolve('./test/fixtures/configfiles/README.txt'),
markdown2: null,
text2: null,
},
});
done();
});
});

it.skip('should use specified loaders', function (done) {
var logRequire = [];
var logFailure = [];

var app = new Liftoff({
extensions: {
'.md': './test/fixtures/configfiles/require-md',
},
name: 'myapp',
configFiles: {
README: {
text_null: {
path: 'test/fixtures/configfiles',
},
findingup: {
path: 'test',
cwd: 'test/fixtures/configfiles',
findUp: true,
text_err: {
path: 'test/fixtures/configfiles',
extensions: {
'.txt': './test/fixtures/configfiles/require-non-exist',
},
},
text: {
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: {
path: 'test/fixtures/configfiles',
extensions: { // ignored
'.js': './test/fixtures/configfiles/require-js',
'.json': './test/fixtures/configfiles/require-json',
},
},
},
},
});
app.on('requireFail', function (moduleName, error) {
logFailure.push({ moduleName: moduleName, error: error });
});
app.on('require', function (moduleName, module) {
logRequire.push({ moduleName: moduleName, module: module });
});
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,
},
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,
},
});

expect(logRequire.length).to.equal(2);
expect(logRequire[0].moduleName)
.to.equal('./test/fixtures/configfiles/require-txt');
expect(logRequire[1].moduleName)
.to.equal('./test/fixtures/configfiles/require-md');

expect(logFailure.length).to.equal(1);
expect(logFailure[0].moduleName)
.to.equal('./test/fixtures/configfiles/require-non-exist');

expect(require(env.configFiles.README.markdown))
.to.equal('Load README.md by require-md');
expect(require(env.configFiles.README.text)).to
.to.equal('Load README.txt by require-txt');
expect(require(env.configFiles.index.test))
.to.deep.equal({ aaa: 'AAA' });
done();
});
});
});

it('should use default cwd if not specified', function (done) {
describe('config', function () {
it('should be empty if not specified', function (done) {
var app = new Liftoff({
name: 'myapp',
});
app.prepare({}, function (env) {
expect(env.config).to.deep.equal({});
done();
});
});

it('TODO: name', function (done) {
var app = new Liftoff({
name: 'myapp',
configFiles: {
index: {
cwd: {
path: '.',
extensions: ['.js', '.json'],
},
testconfig: ['test/fixtures/configfiles'],
},
});
app.prepare({}, function (env) {
expect(env.config).to.deep.equal({
testconfig: {
aaa: 'AAA',
},
});
done();
});
});

it('TODO: name with extends', function (done) {
var app = new Liftoff({
name: 'myapp',
configFiles: {
testconfig: ['test/fixtures/configfiles-extends'],
},
});
app.prepare(
{
cwd: 'test/fixtures/configfiles',
app.prepare({}, function (env) {
expect(env.config).to.deep.equal({
testconfig: {
aaa: 'CCC',
bbb: 'BBB',
},
});
done();
});
});

it.skip('should use default cwd if not specified', function (done) {
var app = new Liftoff({
name: 'myapp',
configFiles: {
testconfig: [
{ path: '.', extensions: ['.js', '.json'] },
],
},
function (env) {
expect(env.configFiles).toEqual({
index: {
cwd: path.resolve('./test/fixtures/configfiles/index.json'),
},
});
done();
}
);
});
app.prepare({
cwd: 'test/fixtures/configfiles',
}, function (env) {
expect(env.configFiles).to.deep.equal({
testconfig: path.resolve('./test/fixtures/configfiles/testconfig.json'),
});
done();
});
});

it('should use default extensions if not specified', function (done) {
it.skip('should use default extensions if not specified', function (done) {
var app = new Liftoff({
extensions: { '.md': null, '.txt': null },
name: 'myapp',
@@ -570,7 +758,7 @@ describe('Liftoff', function () {
});
});

it('should use specified loaders', function (done) {
it.skip('should use specified loaders', function (done) {
var logRequire = [];
var logFailure = [];