Skip to content

Commit

Permalink
Add support for ESM plugins/presets
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
wooorm committed Apr 1, 2021
1 parent 53be150 commit 1091e32
Show file tree
Hide file tree
Showing 14 changed files with 214 additions and 67 deletions.
87 changes: 53 additions & 34 deletions lib/configuration.js
Expand Up @@ -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}
Expand All @@ -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
}
Expand Down Expand Up @@ -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 + '`'
Expand All @@ -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)})
Expand Down
73 changes: 41 additions & 32 deletions lib/find-up.js
Expand Up @@ -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

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -117,41 +127,40 @@ 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) {
if (error.code === 'ENOENT') {
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()
}
}
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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",
Expand Down

0 comments on commit 1091e32

Please sign in to comment.