Skip to content

Commit

Permalink
feat: Add support for loading config from ESM modules (#7)
Browse files Browse the repository at this point in the history
Fixes #6
  • Loading branch information
coreyfarrell committed Oct 5, 2019
1 parent 79e3da7 commit bc5ea3e
Show file tree
Hide file tree
Showing 24 changed files with 228 additions and 60 deletions.
3 changes: 2 additions & 1 deletion .taprc
@@ -1,4 +1,5 @@
{
"test-ignore": "helpers",
"check-coverage": true
"esm": false,
"nyc-arg": "--nycrc-path=nyc-settings.js"
}
65 changes: 41 additions & 24 deletions index.js
Expand Up @@ -2,18 +2,35 @@

const fs = require('fs');
const path = require('path');
const {promisify} = require('util');
const camelcase = require('camelcase');
const findUp = require('find-up');
const resolveFrom = require('resolve-from');

const readFile = promisify(fs.readFile);

const standardConfigFiles = [
'.nycrc',
'.nycrc.json',
'.nycrc.yml',
'.nycrc.yaml',
'nyc.config.js'
'nyc.config.js',
'nyc.config.cjs',
'nyc.config.mjs'
];

async function moduleLoader(file) {
try {
return require(file);
} catch (error) {
if (error.code !== 'ERR_REQUIRE_ESM') {
throw error;
}

return require('./load-esm')(file);
}
}

function camelcasedConfig(config) {
const results = {};
for (const [field, value] of Object.entries(config)) {
Expand All @@ -23,13 +40,13 @@ function camelcasedConfig(config) {
return results;
}

function findPackage(options) {
async function findPackage(options) {
const cwd = options.cwd || process.env.NYC_CWD || process.cwd();
const pkgPath = findUp.sync('package.json', {cwd});
const pkgPath = await findUp('package.json', {cwd});
if (pkgPath) {
return {
cwd: path.dirname(pkgPath),
pkgConfig: JSON.parse(fs.readFileSync(pkgPath, 'utf8')).nyc || {}
pkgConfig: JSON.parse(await readFile(pkgPath, 'utf8')).nyc || {}
};
}

Expand All @@ -39,38 +56,37 @@ function findPackage(options) {
};
}

function actualLoad(configFile) {
async function actualLoad(configFile) {
if (!configFile) {
return {};
}

const configExt = path.extname(configFile).toLowerCase();
switch (configExt) {
case '.js':
case '.mjs':
return moduleLoader(configFile);
case '.cjs':
return require(configFile);
case '.yml':
case '.yaml':
return require('js-yaml').load(
fs.readFileSync(configFile, 'utf8')
);
return require('js-yaml').load(await readFile(configFile, 'utf8'));
default:
return JSON.parse(
fs.readFileSync(configFile, 'utf8')
);
return JSON.parse(await readFile(configFile, 'utf8'));
}
}

function applyExtends(config, filename, loopCheck = new Set()) {
async function applyExtends(config, filename, loopCheck = new Set()) {
config = camelcasedConfig(config);
if (Object.prototype.hasOwnProperty.call(config, 'extends')) {
if ('extends' in config) {
const extConfigs = [].concat(config.extends);
if (extConfigs.some(e => typeof e !== 'string')) {
throw new TypeError(`${filename} contains an invalid 'extends' option`);
}

delete config.extends;
const filePath = path.dirname(filename);
return extConfigs.reduce((config, extConfig) => {
for (const extConfig of extConfigs) {
const configFile = resolveFrom.silent(filePath, extConfig) ||
resolveFrom.silent(filePath, './' + extConfig);
if (!configFile) {
Expand All @@ -82,27 +98,28 @@ function applyExtends(config, filename, loopCheck = new Set()) {
}

loopCheck.add(configFile);
return {
...config,
...applyExtends(actualLoad(configFile), configFile, loopCheck)
};
}, config);
Object.assign(
config,
// eslint-disable-next-line no-await-in-loop
await applyExtends(await actualLoad(configFile), configFile, loopCheck)
);
}
}

return config;
}

function loadNycConfig(options = {}) {
const {cwd, pkgConfig} = findPackage(options);
async function loadNycConfig(options = {}) {
const {cwd, pkgConfig} = await findPackage(options);
const configFiles = [].concat(options.nycrcPath || standardConfigFiles);
const configFile = findUp.sync(configFiles, {cwd});
const configFile = await findUp(configFiles, {cwd});
if (options.nycrcPath && !configFile) {
throw new Error(`Requested configuration file ${options.nycrcPath} not found`);
}

const config = {
...applyExtends(pkgConfig, path.join(cwd, 'package.json')),
...applyExtends(actualLoad(configFile), configFile)
...(await applyExtends(pkgConfig, path.join(cwd, 'package.json'))),
...(await applyExtends(await actualLoad(configFile), configFile))
};

const arrayFields = ['require', 'extension', 'exclude', 'include'];
Expand Down
10 changes: 10 additions & 0 deletions load-esm.js
@@ -0,0 +1,10 @@
'use strict';

module.exports = async filename => {
const mod = await import(filename);
if ('default' in mod === false) {
throw new Error(`${filename} has no default export`);
}

return mod.default;
};
13 changes: 13 additions & 0 deletions nyc-settings.js
@@ -0,0 +1,13 @@
'use strict';

const {hasImport} = require('./test/helpers');

const include = [
'index.js'
];

if (hasImport) {
include.push('load-esm.js');
}

module.exports = {include};
16 changes: 11 additions & 5 deletions package.json
Expand Up @@ -23,16 +23,22 @@
"homepage": "https://github.com/istanbuljs/load-nyc-config#readme",
"dependencies": {
"camelcase": "^5.3.1",
"find-up": "^4.0.0",
"find-up": "^4.1.0",
"js-yaml": "^3.13.1",
"resolve-from": "^5.0.0"
},
"devDependencies": {
"standard-version": "^6.0.1",
"tap": "^14.0.0",
"xo": "^0.24.0"
"standard-version": "^7.0.0",
"tap": "^14.6.5",
"xo": "^0.25.3"
},
"xo": {
"ignores": "tap-snapshots/**"
"ignores": [
"test/fixtures/extends/invalid.*",
"tap-snapshots/**"
],
"rules": {
"require-atomic-updates": 0
}
}
}
24 changes: 24 additions & 0 deletions tap-snapshots/test-basic.js-TAP.test.js
Expand Up @@ -30,6 +30,18 @@ Object {
}
`

exports[`test/basic.js TAP esm nyc-config-js-type-module > must match snapshot 1`] = `
Object {
"all": false,
}
`

exports[`test/basic.js TAP esm nyc-config-mjs > must match snapshot 1`] = `
Object {
"all": false,
}
`

exports[`test/basic.js TAP extends > must match snapshot 1`] = `
Object {
"all": false,
Expand Down Expand Up @@ -65,6 +77,18 @@ Object {
}
`

exports[`test/basic.js TAP nyc-config-async > must match snapshot 1`] = `
Object {
"all": false,
}
`

exports[`test/basic.js TAP nyc-config-cjs > must match snapshot 1`] = `
Object {
"all": false,
}
`

exports[`test/basic.js TAP nyc-config-js > must match snapshot 1`] = `
Object {
"all": false,
Expand Down
2 changes: 1 addition & 1 deletion tap-snapshots/test-env-nyc-cwd.js-TAP.test.js
Expand Up @@ -5,7 +5,7 @@
* Make sure to inspect the output below. Do not ignore changes!
*/
'use strict'
exports[`test/env-nyc-cwd.js TAP > must match snapshot 1`] = `
exports[`test/env-nyc-cwd.js TAP env-nyc-cwd > must match snapshot 1`] = `
Object {
"all": true,
}
Expand Down
2 changes: 1 addition & 1 deletion tap-snapshots/test-process-cwd.js-TAP.test.js
Expand Up @@ -5,7 +5,7 @@
* Make sure to inspect the output below. Do not ignore changes!
*/
'use strict'
exports[`test/process-cwd.js TAP > must match snapshot 1`] = `
exports[`test/process-cwd.js TAP process-cwd > must match snapshot 1`] = `
Object {
"all": true,
}
Expand Down
54 changes: 41 additions & 13 deletions test/basic.js
@@ -1,40 +1,68 @@
const t = require('tap');
const {fixturePath, basicTest} = require('./helpers');
const {fixturePath, basicTest, hasImport, hasESM} = require('./helpers');
const {loadNycConfig} = require('..');

t.test('options.nycrcPath points to non-existent file', t => {
t.test('options.nycrcPath points to non-existent file', async t => {
const cwd = fixturePath();
const nycrcPath = fixturePath('does-not-exist.json');
t.throws(() => loadNycConfig({cwd, nycrcPath}));
t.end();
await t.rejects(loadNycConfig({cwd, nycrcPath}));
});

t.test('no-config-file', basicTest);
t.test('nycrc-no-ext', basicTest);
t.test('nycrc-json', basicTest);
t.test('nycrc-yml', basicTest);
t.test('nycrc-yaml', basicTest);
t.test('nyc-config-cjs', basicTest);
t.test('nyc-config-js', basicTest);
t.test('nyc-config-async', basicTest);
t.test('array-field-fixup', basicTest);
t.test('camel-decamel', basicTest);
t.test('extends', basicTest);
t.test('extends-array-empty', basicTest);
t.test('extends-array', basicTest);

t.test('extends failures', t => {
const errorConfigs = ['looper1.json', 'invalid.json', 'missing.json'];
t.test('extends failures', async t => {
const cwd = fixturePath('extends');
errorConfigs.map(f => fixturePath('extends', f)).forEach(nycrcPath => {
t.throws(() => loadNycConfig({cwd, nycrcPath}));
const files = {
'looper1.json': /Circular extended configurations/,
'invalid.json': /contains an invalid 'extends' option/,
'invalid.js': /Unexpected identifier/,
'invalid.cjs': /Unexpected identifier/,
'missing.json': /Could not resolve configuration file/
};
if (hasImport && await hasESM()) {
files['invalid.mjs'] = /has no default export/;
}

const tests = Object.entries(files).map(([file, error]) => {
return t.rejects(loadNycConfig({
cwd,
nycrcPath: fixturePath('extends', file)
}), error, file);
});
t.end();

await Promise.all(tests);
});

t.test('no package.json', t => {
t.test('no package.json', async t => {
const cwd = '/';
const nycrcPath = fixturePath('nycrc-no-ext', '.nycrc');

t.matchSnapshot(loadNycConfig({cwd}), 'no config');
t.matchSnapshot(loadNycConfig({cwd, nycrcPath}), 'explicit .nycrc');
t.end();
t.matchSnapshot(await loadNycConfig({cwd}), 'no config');
t.matchSnapshot(await loadNycConfig({cwd, nycrcPath}), 'explicit .nycrc');
});

if (hasImport) {
t.test('esm', async t => {
if (await hasESM()) {
if (process.versions.node.split('.')[0] >= 12) {
await t.test('nyc-config-js-type-module', basicTest);
}

await t.test('nyc-config-mjs', basicTest);
} else {
t.pass('we have import but it doesn\'t support ES modules');
}
});
}
11 changes: 6 additions & 5 deletions test/env-nyc-cwd.js
Expand Up @@ -2,10 +2,11 @@ const t = require('tap');
const {fixturePath} = require('./helpers');
const {loadNycConfig} = require('..');

const saved = process.env.NYC_CWD;
process.env.NYC_CWD = fixturePath('no-config-file');
t.test('env-nyc-cwd', async t => {
const saved = process.env.NYC_CWD;
process.env.NYC_CWD = fixturePath('no-config-file');

t.matchSnapshot(loadNycConfig());
t.end();
t.matchSnapshot(await loadNycConfig());

process.env.NYC_CWD = saved;
process.env.NYC_CWD = saved;
});
1 change: 1 addition & 0 deletions test/fixtures/extends/invalid.cjs
@@ -0,0 +1 @@
syntax error!
1 change: 1 addition & 0 deletions test/fixtures/extends/invalid.js
@@ -0,0 +1 @@
syntax error!
2 changes: 2 additions & 0 deletions test/fixtures/extends/invalid.mjs
@@ -0,0 +1,2 @@
// This is not supported, config must be provided in the default export.
export const all = true;
12 changes: 12 additions & 0 deletions test/fixtures/nyc-config-async/nyc.config.js
@@ -0,0 +1,12 @@
'use strict';

const {promisify} = require('util');

const delay = promisify(setTimeout);

async function loadConfig() {
await delay(10);
return {all: false};
}

module.exports = loadConfig();
5 changes: 5 additions & 0 deletions test/fixtures/nyc-config-async/package.json
@@ -0,0 +1,5 @@
{
"nyc": {
"all": true
}
}
3 changes: 3 additions & 0 deletions test/fixtures/nyc-config-cjs/nyc.config.cjs
@@ -0,0 +1,3 @@
'use strict';

module.exports = {all: false};
2 changes: 2 additions & 0 deletions test/fixtures/nyc-config-cjs/package.json
@@ -0,0 +1,2 @@
{
}
3 changes: 3 additions & 0 deletions test/fixtures/nyc-config-js-type-module/nyc.config.js
@@ -0,0 +1,3 @@
export default {
all: false
};
6 changes: 6 additions & 0 deletions test/fixtures/nyc-config-js-type-module/package.json
@@ -0,0 +1,6 @@
{
"type": "module",
"nyc": {
"all": true
}
}

0 comments on commit bc5ea3e

Please sign in to comment.