From 1091e327d7cf225f90c98ab9aad676efec723cfb Mon Sep 17 00:00:00 2001 From: Titus Date: Thu, 1 Apr 2021 13:20:14 +0200 Subject: [PATCH] Add support for ESM plugins/presets ESM modules can use `export default` to expose their plugin or preset. This does not add support for config files in ESM just yet. * Add support for plugins in ESM format w/ an `.mjs` extension * Add support for plugins in ESM format w/ a `.js` extension if the nearest `package.json` has a `type: 'module'` * Add support for interop bundles (CJS w/ `__esModule: true` field) --- lib/configuration.js | 87 ++++++++++------- lib/find-up.js | 73 ++++++++------ package.json | 1 + test/configuration-plugins.js | 97 ++++++++++++++++++- .../config-plugins-esm-interop/.foorc | 3 + .../config-plugins-esm-interop/one.txt | 0 .../config-plugins-esm-interop/test.js | 5 + test/fixtures/config-plugins-esm-js/.foorc | 3 + test/fixtures/config-plugins-esm-js/one.txt | 0 .../config-plugins-esm-js/package.json | 3 + test/fixtures/config-plugins-esm-js/test.js | 3 + test/fixtures/config-plugins-esm-mjs/.foorc | 3 + test/fixtures/config-plugins-esm-mjs/one.txt | 0 test/fixtures/config-plugins-esm-mjs/test.mjs | 3 + 14 files changed, 214 insertions(+), 67 deletions(-) create mode 100644 test/fixtures/config-plugins-esm-interop/.foorc create mode 100644 test/fixtures/config-plugins-esm-interop/one.txt create mode 100644 test/fixtures/config-plugins-esm-interop/test.js create mode 100644 test/fixtures/config-plugins-esm-js/.foorc create mode 100644 test/fixtures/config-plugins-esm-js/one.txt create mode 100644 test/fixtures/config-plugins-esm-js/package.json create mode 100644 test/fixtures/config-plugins-esm-js/test.js create mode 100644 test/fixtures/config-plugins-esm-mjs/.foorc create mode 100644 test/fixtures/config-plugins-esm-mjs/one.txt create mode 100644 test/fixtures/config-plugins-esm-mjs/test.mjs diff --git a/lib/configuration.js b/lib/configuration.js index 1110613..b7b0522 100644 --- a/lib/configuration.js +++ b/lib/configuration.js @@ -74,11 +74,13 @@ function load(filePath, callback) { return callback(error, file) } - callback(null, self.create()) + self.create().then(function (result) { + callback(null, result) + }, callback) } } -function create(buf, filePath) { +async function create(buf, filePath) { var self = this var fn = (filePath && loaders[path.extname(filePath)]) || defaultLoader var options = {prefix: self.pluginPrefix, cwd: self.cwd} @@ -100,21 +102,21 @@ function create(buf, filePath) { if (contents === undefined) { if (self.defaultConfig) { - merge( + await merge( result, self.defaultConfig, Object.assign({}, options, {root: self.cwd}) ) } } else { - merge( + await merge( result, contents, Object.assign({}, options, {root: path.dirname(filePath)}) ) } - merge(result, self.given, Object.assign({}, options, {root: self.cwd})) + await merge(result, self.given, Object.assign({}, options, {root: self.cwd})) return result } @@ -149,26 +151,22 @@ function loadJson(buf, filePath) { return result } -function merge(target, raw, options) { +async function merge(target, raw, options) { if (typeof raw === 'object' && raw !== null) { - addPreset(raw) + await addPreset(raw) } else { throw new Error('Expected preset, not `' + raw + '`') } return target - function addPreset(result) { + async function addPreset(result) { var plugins = result.plugins if (plugins === null || plugins === undefined) { // Empty. } else if (typeof plugins === 'object' && plugins !== null) { - if ('length' in plugins) { - addEach(plugins) - } else { - addIn(plugins) - } + await ('length' in plugins ? addEach(plugins) : addIn(plugins)) } else { throw new Error( 'Expected a list or object of plugins, not `' + plugins + '`' @@ -178,59 +176,80 @@ function merge(target, raw, options) { target.settings = Object.assign({}, target.settings, result.settings) } - function addEach(result) { + async function addEach(result) { var index = -1 var value while (++index < result.length) { value = result[index] - if (value !== null && typeof value === 'object' && 'length' in value) { - use.apply(null, value) - } else { - use(value) - } + // Keep order sequential instead of parallel. + // eslint-disable-next-line no-await-in-loop + await (value !== null && typeof value === 'object' && 'length' in value + ? use.apply(null, value) + : use(value)) } } - function addIn(result) { + async function addIn(result) { var key for (key in result) { - use(key, result[key]) + // Keep order sequential instead of parallel. + // eslint-disable-next-line no-await-in-loop + await use(key, result[key]) } } - function use(usable, value) { + async function use(usable, value) { if (typeof usable === 'string') { - addModule(usable, value) + await addModule(usable, value) } else if (typeof usable === 'function') { addPlugin(usable, value) } else { - merge(target, usable, options) + await merge(target, usable, options) } } - function addModule(id, value) { + async function addModule(id, value) { var fp = loadPlugin.resolve(id, {cwd: options.root, prefix: options.prefix}) + var ext var result if (fp) { - try { - result = require(fp) - } catch (error) { - throw fault( - 'Cannot parse script `%s`\n%s', - path.relative(options.root, fp), - error.stack - ) + ext = path.extname(fp) + + /* istanbul ignore next - To do next major: Tests don’t run on Node 10 */ + if (ext !== '.mjs') { + try { + result = require(fp) + } catch (error) { + if (ext !== '.cjs' && error.code === 'ERR_REQUIRE_ESM') { + ext = '.mjs' + } else { + throw fault( + 'Cannot parse script `%s`\n%s', + path.relative(options.root, fp), + error.stack + ) + } + } + + if (result && typeof result === 'object' && result.__esModule) { + result = result.default + } + } + + /* istanbul ignore next - To do next major: Tests don’t run on Node 10 */ + if (ext === '.mjs') { + result = (await import(fp)).default } try { if (typeof result === 'function') { addPlugin(result, value) } else { - merge( + await merge( target, result, Object.assign({}, options, {root: path.dirname(fp)}) diff --git a/lib/find-up.js b/lib/find-up.js index 786d9fb..7e91130 100644 --- a/lib/find-up.js +++ b/lib/find-up.js @@ -4,6 +4,7 @@ var fs = require('fs') var path = require('path') var fault = require('fault') var debug = require('debug')('unified-engine:find-up') +var wrap = require('trough/wrap') module.exports = FindUp @@ -68,23 +69,32 @@ function load(filePath, callback) { result.code = 'ENOENT' result.path = error.path result.syscall = error.syscall + loaded(result) } else { - try { - result = self.create(buf, self.givenFilePath) - debug('Read given file `%s`', self.givenFilePath) - } catch (error_) { - result = fault( - 'Cannot parse given file `%s`\n%s', - path.relative(self.cwd, self.givenFilePath), - error_.stack + wrap(self.create, onparse)(buf, self.givenFilePath) + } + + function onparse(error, result) { + if (error) { + debug(error.message) + loaded( + fault( + 'Cannot parse given file `%s`\n%s', + path.relative(self.cwd, self.givenFilePath), + error.stack + ) ) - debug(error_.message) + } else { + debug('Read given file `%s`', self.givenFilePath) + loaded(result) } } - givenFile = result - self.givenFile = result - applyAll(cbs, result) + function loaded(result) { + givenFile = result + self.givenFile = result + applyAll(cbs, result) + } } function find(directory) { @@ -117,7 +127,6 @@ function load(filePath, callback) { function done(error, buf) { var fp = path.join(directory, self.names[index]) - var contents /* istanbul ignore if - Hard to test. */ if (error) { @@ -125,33 +134,33 @@ function load(filePath, callback) { return next() } - error = fault( - 'Cannot read file `%s`\n%s', - path.relative(self.cwd, fp), - error.message - ) debug(error.message) - return found(error) - } - - try { - contents = self.create(buf, fp) - } catch (error_) { return found( fault( - 'Cannot parse file `%s`\n%s', + 'Cannot read file `%s`\n%s', path.relative(self.cwd, fp), - error_.message + error.message ) ) } - /* istanbul ignore else - maybe used in the future. */ - if (contents) { - debug('Read file `%s`', fp) - found(null, contents) - } else { - next() + wrap(self.create, onparse)(buf, fp) + + function onparse(error, result) { + if (error) { + found( + fault( + 'Cannot parse file `%s`\n%s', + path.relative(self.cwd, fp), + error.message + ) + ) + } else if (result) { + debug('Read file `%s`', fp) + found(null, result) + } else { + next() + } } } diff --git a/package.json b/package.json index 081ed79..155a583 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "remark-cli": "^9.0.0", "remark-preset-wooorm": "^8.0.0", "remark-toc": "^7.0.0", + "semver": "^6.0.0", "strip-ansi": "^6.0.0", "tape": "^5.0.0", "unified": "^9.0.0", diff --git a/test/configuration-plugins.js b/test/configuration-plugins.js index 08c3d94..b20a1a6 100644 --- a/test/configuration-plugins.js +++ b/test/configuration-plugins.js @@ -2,6 +2,7 @@ var path = require('path') var test = require('tape') +var semver = require('semver') var noop = require('./util/noop-processor') var spy = require('./util/spy') var engine = require('..') @@ -11,7 +12,9 @@ var join = path.join var fixtures = join(__dirname, 'fixtures') test('configuration', function (t) { - t.plan(6) + var esm = semver.gte(process.versions.node, '12.0.0') + + t.plan(esm ? 9 : 6) t.test('should cascade `plugins`', function (t) { var stderr = spy() @@ -45,6 +48,98 @@ test('configuration', function (t) { } }) + if (esm) { + t.test('should support an ESM plugin w/ an `.mjs` extname', function (t) { + var stderr = spy() + + // One more assertions is loaded in a plugin. + t.plan(2) + + engine( + { + processor: noop().use(addTest), + cwd: join(fixtures, 'config-plugins-esm-mjs'), + streamError: stderr.stream, + files: ['one.txt'], + rcName: '.foorc' + }, + onrun + ) + + function onrun(error, code) { + t.deepEqual( + [error, code, stderr()], + [null, 0, 'one.txt: no issues found\n'], + 'should work' + ) + } + + function addTest() { + this.t = t + } + }) + + t.test('should support an ESM plugin w/ a `.js` extname', function (t) { + var stderr = spy() + + // One more assertions is loaded in a plugin. + t.plan(2) + + engine( + { + processor: noop().use(addTest), + cwd: join(fixtures, 'config-plugins-esm-js'), + streamError: stderr.stream, + files: ['one.txt'], + rcName: '.foorc' + }, + onrun + ) + + function onrun(error, code) { + t.deepEqual( + [error, code, stderr()], + [null, 0, 'one.txt: no issues found\n'], + 'should work' + ) + } + + function addTest() { + this.t = t + } + }) + + t.test('should support a CJS plugin w/ interop flags', function (t) { + var stderr = spy() + + // One more assertions is loaded in a plugin. + t.plan(2) + + engine( + { + processor: noop().use(addTest), + cwd: join(fixtures, 'config-plugins-esm-interop'), + streamError: stderr.stream, + files: ['one.txt'], + rcName: '.foorc' + }, + onrun + ) + + function onrun(error, code) { + t.deepEqual( + [error, code, stderr()], + [null, 0, 'one.txt: no issues found\n'], + 'should work' + ) + } + + function addTest() { + this.t = t + } + }) + } + t.test('should handle failing plugins', function (t) { var stderr = spy() diff --git a/test/fixtures/config-plugins-esm-interop/.foorc b/test/fixtures/config-plugins-esm-interop/.foorc new file mode 100644 index 0000000..e4a3582 --- /dev/null +++ b/test/fixtures/config-plugins-esm-interop/.foorc @@ -0,0 +1,3 @@ +{ + "plugins": ["./test.js"] +} diff --git a/test/fixtures/config-plugins-esm-interop/one.txt b/test/fixtures/config-plugins-esm-interop/one.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/config-plugins-esm-interop/test.js b/test/fixtures/config-plugins-esm-interop/test.js new file mode 100644 index 0000000..474ff35 --- /dev/null +++ b/test/fixtures/config-plugins-esm-interop/test.js @@ -0,0 +1,5 @@ +exports.default = function () { + this.t.pass() +} + +exports.__esModule = true diff --git a/test/fixtures/config-plugins-esm-js/.foorc b/test/fixtures/config-plugins-esm-js/.foorc new file mode 100644 index 0000000..e4a3582 --- /dev/null +++ b/test/fixtures/config-plugins-esm-js/.foorc @@ -0,0 +1,3 @@ +{ + "plugins": ["./test.js"] +} diff --git a/test/fixtures/config-plugins-esm-js/one.txt b/test/fixtures/config-plugins-esm-js/one.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/config-plugins-esm-js/package.json b/test/fixtures/config-plugins-esm-js/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/test/fixtures/config-plugins-esm-js/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/fixtures/config-plugins-esm-js/test.js b/test/fixtures/config-plugins-esm-js/test.js new file mode 100644 index 0000000..233e04d --- /dev/null +++ b/test/fixtures/config-plugins-esm-js/test.js @@ -0,0 +1,3 @@ +export default function test() { + this.t.pass() +} diff --git a/test/fixtures/config-plugins-esm-mjs/.foorc b/test/fixtures/config-plugins-esm-mjs/.foorc new file mode 100644 index 0000000..725e46e --- /dev/null +++ b/test/fixtures/config-plugins-esm-mjs/.foorc @@ -0,0 +1,3 @@ +{ + "plugins": ["./test.mjs"] +} diff --git a/test/fixtures/config-plugins-esm-mjs/one.txt b/test/fixtures/config-plugins-esm-mjs/one.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/config-plugins-esm-mjs/test.mjs b/test/fixtures/config-plugins-esm-mjs/test.mjs new file mode 100644 index 0000000..233e04d --- /dev/null +++ b/test/fixtures/config-plugins-esm-mjs/test.mjs @@ -0,0 +1,3 @@ +export default function test() { + this.t.pass() +}