Skip to content

Commit

Permalink
feat(esm): ability to decorate ESM module name before importing it (m…
Browse files Browse the repository at this point in the history
  • Loading branch information
j0tunn authored Dec 4, 2022
1 parent fc4ac58 commit 73bb819
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 10 deletions.
7 changes: 5 additions & 2 deletions lib/mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,8 @@ Mocha.prototype.loadFiles = function (fn) {
* @see {@link Mocha#addFile}
* @see {@link Mocha#run}
* @see {@link Mocha#unloadFiles}
* @param {Object} [options] - Settings object.
* @param {Function} [options.esmDecorator] - Function invoked on esm module name right before importing it. By default will passthrough as is.
* @returns {Promise}
* @example
*
Expand All @@ -437,7 +439,7 @@ Mocha.prototype.loadFiles = function (fn) {
* .then(() => mocha.run(failures => process.exitCode = failures ? 1 : 0))
* .catch(() => process.exitCode = 1);
*/
Mocha.prototype.loadFilesAsync = function () {
Mocha.prototype.loadFilesAsync = function ({esmDecorator} = {}) {
var self = this;
var suite = this.suite;
this.lazyLoadFiles(true);
Expand All @@ -450,7 +452,8 @@ Mocha.prototype.loadFilesAsync = function () {
function (file, resultModule) {
suite.emit(EVENT_FILE_REQUIRE, resultModule, file, self);
suite.emit(EVENT_FILE_POST_REQUIRE, global, file, self);
}
},
esmDecorator
);
};

Expand Down
28 changes: 20 additions & 8 deletions lib/nodejs/esm-utils.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
const path = require('path');
const url = require('url');

const formattedImport = async file => {
const forward = x => x;

const formattedImport = async (file, esmDecorator = forward) => {
if (path.isAbsolute(file)) {
try {
return await import(url.pathToFileURL(file));
return await exports.doImport(esmDecorator(url.pathToFileURL(file)));
} catch (err) {
// This is a hack created because ESM in Node.js (at least in Node v15.5.1) does not emit
// the location of the syntax error in the error thrown.
Expand All @@ -27,15 +29,17 @@ const formattedImport = async file => {
throw err;
}
}
return import(file);
return exports.doImport(esmDecorator(file));
};

exports.requireOrImport = async file => {
exports.doImport = async file => import(file);

exports.requireOrImport = async (file, esmDecorator) => {
if (path.extname(file) === '.mjs') {
return formattedImport(file);
return formattedImport(file, esmDecorator);
}
try {
return dealWithExports(await formattedImport(file));
return dealWithExports(await formattedImport(file, esmDecorator));
} catch (err) {
if (
err.code === 'ERR_MODULE_NOT_FOUND' ||
Expand Down Expand Up @@ -85,10 +89,18 @@ function dealWithExports(module) {
}
}

exports.loadFilesAsync = async (files, preLoadFunc, postLoadFunc) => {
exports.loadFilesAsync = async (
files,
preLoadFunc,
postLoadFunc,
esmDecorator
) => {
for (const file of files) {
preLoadFunc(file);
const result = await exports.requireOrImport(path.resolve(file));
const result = await exports.requireOrImport(
path.resolve(file),
esmDecorator
);
postLoadFunc(file, result);
}
};
46 changes: 46 additions & 0 deletions test/node-unit/esm-utils.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use strict';

const esmUtils = require('../../lib/nodejs/esm-utils');
const sinon = require('sinon');
const url = require('url');

describe('esm-utils', function () {
beforeEach(function () {
sinon.stub(esmUtils, 'doImport').resolves({});
});

afterEach(function () {
sinon.restore();
});

describe('loadFilesAsync()', function () {
it('should not decorate imported module if no decorator passed', async function () {
await esmUtils.loadFilesAsync(
['/foo/bar.mjs'],
() => {},
() => {}
);

expect(
esmUtils.doImport.firstCall.args[0].toString(),
'to be',
url.pathToFileURL('/foo/bar.mjs').toString()
);
});

it('should decorate imported module with passed decorator', async function () {
await esmUtils.loadFilesAsync(
['/foo/bar.mjs'],
() => {},
() => {},
x => `${x}?foo=bar`
);

expect(
esmUtils.doImport.firstCall.args[0].toString(),
'to be',
`${url.pathToFileURL('/foo/bar.mjs').toString()}?foo=bar`
);
});
});
});
19 changes: 19 additions & 0 deletions test/node-unit/mocha.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ describe('Mocha', function () {
stubs.Suite = sinon.stub().returns(stubs.suite);
stubs.Suite.constants = {};
stubs.ParallelBufferedRunner = sinon.stub().returns({});
stubs.esmUtils = {
loadFilesAsync: sinon.stub()
};
const runner = Object.assign(sinon.createStubInstance(EventEmitter), {
runAsync: sinon.stub().resolves(0),
globals: sinon.stub(),
Expand All @@ -66,6 +69,7 @@ describe('Mocha', function () {
'../../lib/suite.js': stubs.Suite,
'../../lib/nodejs/parallel-buffered-runner.js':
stubs.ParallelBufferedRunner,
'../../lib/nodejs/esm-utils': stubs.esmUtils,
'../../lib/runner.js': stubs.Runner,
'../../lib/errors.js': stubs.errors
})
Expand Down Expand Up @@ -219,6 +223,21 @@ describe('Mocha', function () {
});
});

describe('loadFilesAsync()', function () {
it('shoud pass esmDecorator to actual load function', async function () {
const esmDecorator = x => `${x}?foo=bar`;

await mocha.loadFilesAsync({esmDecorator});

expect(stubs.esmUtils.loadFilesAsync, 'was called once');
expect(
stubs.esmUtils.loadFilesAsync.firstCall.args[3],
'to be',
esmDecorator
);
});
});

describe('unloadFiles()', function () {
it('should delegate Mocha.unloadFile() for each item in its list of files', function () {
mocha.files = [DUMB_FIXTURE_PATH, DUMBER_FIXTURE_PATH];
Expand Down

0 comments on commit 73bb819

Please sign in to comment.