Skip to content

Commit

Permalink
WIP: Solves #403, adds Metalsmith.imports method
Browse files Browse the repository at this point in the history
  • Loading branch information
webketje committed Mar 19, 2024
1 parent 7a2f38b commit bdf4bed
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 28 deletions.
40 changes: 16 additions & 24 deletions bin/metalsmith
Original file line number Diff line number Diff line change
Expand Up @@ -120,33 +120,25 @@ async function buildCommand({ config, ...cliOptions }) {
/**
* Plugins.
*/

if (!(spec instanceof Metalsmith)) {
normalize(spec.plugins).forEach(function (plugin) {
for (const name in plugin) {
const opts = plugin[name]
let mod

try {
const local = resolve(confRelativeDir, name)
const npm = resolve(confRelativeDir, 'node_modules', name)

if (exists(local) || exists(`${local}.js`)) {
mod = require(local)
} else if (exists(npm)) {
mod = require(npm)
} else {
mod = require(name)
}
} catch (e) {
fatal(`failed to require plugin "${name}".`)
let plugins = []
try {
plugins = await Promise.all(normalize(spec.plugins).map(async function (pluginDef) {
for (const name in pluginDef) {
const plugin = await metalsmith.imports(name)
const options = pluginDef[name]
return { name, plugin, options }
}
}))
} catch (err) {
fatal(`${err.stack}`)
}

try {
metalsmith.use(mod(opts))
} catch (e) {
fatal(`error using plugin "${name}"...`, `${e.message}\n\n${e.stack}`)
}
plugins.forEach(({ plugin, options, name }) => {
try {
metalsmith.use(plugin(options))
} catch (e) {
fatal(`Error: Initialization of plugin "${name}" resulted in error: `, `${e.message}\n\n${e.stack}`)
}
})
}
Expand Down
59 changes: 59 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -785,3 +785,62 @@ Metalsmith.prototype.writeFile = function (file, data, callback) {
return result
}
}

/**
*
* @param {string} specifier
* @param {string} [name]
* @returns {Promise<*>}
*/
Metalsmith.prototype.imports = function (specifier, name) {
// if specifier is falsy, throw
if (!specifier) {
const err = new Error(`Metalsmith.import() - "${specifier}" is not a valid import specifier.`)
return Promise.reject(err)
}

if (!path.isAbsolute(specifier) && specifier.startsWith('.')) {
// commonjs extensionless import compat, doesn't work for index.js dir imports though
if (!path.extname(specifier)) specifier = `${specifier}.js`
specifier = this.path(specifier)
}

// if specifier is not a string, consider it resolved
if (typeof specifier !== 'string') return Promise.resolve(specifier)

let load = (mod) => import(mod)
const json = specifier.endsWith('.json')
if (json) {
load = (mod) => readFile(mod, 'utf-8')
}

return load(specifier)
.catch(() => {
throw new Error(`Metalsmith.import() cannot find module "${specifier}".`)
})
.then((s) => {
if (json) {
try {
return JSON.parse(s)
} catch (err) {
throw new Error(`File "${specifier}" contains invalid JSON.`)
}
}
if (arguments.length === 1) {
const namedExports = Object.keys(s).filter((d) => d !== 'default')
// return first named export
if (namedExports.length === 1) return s[namedExports[0]]
// fall back to default
if (Reflect.has(s, 'default')) return s.default
// if there are multiple named exports but no default, act as if instructed import * from specifier
return s
}

// if export name exists (either explicit, or implicit 'default') return it, else throw
if (Reflect.has(s, name)) {
return s[name]
}

throw new Error(`Metalsmith.import() cannot import "${name}" from "${specifier}`)
})
}
3 changes: 3 additions & 0 deletions test/fixtures/imports/cjs-default-export.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = function() {
return 'cjs-default-export'
}
8 changes: 8 additions & 0 deletions test/fixtures/imports/cjs-multiple-exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
first() {
return 'first-export'
},
second() {
return 'second-export'
}
}
3 changes: 3 additions & 0 deletions test/fixtures/imports/cjs-single-named-export.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exports.keyNameDoesntMatter = () => {
return 'cjs-single-named-export'
}
3 changes: 3 additions & 0 deletions test/fixtures/imports/esm-default-export.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function() {
return 'esm-default-export'
}
6 changes: 6 additions & 0 deletions test/fixtures/imports/esm-multiple-exports.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function first() {
return 'first-export'
}
export function second() {
return 'second-export'
}
3 changes: 3 additions & 0 deletions test/fixtures/imports/esm-single-named-export.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const keyNameDoesntMatter = function() {
return 'esm-single-named-export'
}
5 changes: 5 additions & 0 deletions test/fixtures/imports/local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"default": {
"key": "value"
}
}
85 changes: 81 additions & 4 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,83 @@ describe('Metalsmith', function () {
})
})

describe('#imports', function () {
const ms = Metalsmith(fixture('imports'))

it('should auto-import CJS default export', async () => {
const mod = await ms.imports('./cjs-default-export.js')
assert.strictEqual(typeof mod, 'function')
assert.strictEqual(mod(), 'cjs-default-export')
})

it('should auto-import ESM default export', async () => {
const mod = await ms.imports('./esm-default-export.mjs')
assert.strictEqual(typeof mod, 'function')
assert.strictEqual(mod(), 'esm-default-export')
})

it('should import ESM named export', async () => {
const mod = await ms.imports('./esm-multiple-exports.mjs', 'first')
assert.strictEqual(typeof mod, 'function')
assert.strictEqual(mod(), 'first-export')
})

it('should import CJS named export', async () => {
const mod = await ms.imports('./cjs-multiple-exports.js', 'first')
assert.strictEqual(typeof mod, 'function')
assert.strictEqual(mod(), 'first-export')
})

it('should import CJS single named export', async () => {
let mod = await ms.imports('./cjs-single-named-export.js', 'keyNameDoesntMatter')
assert.strictEqual(typeof mod, 'function')
assert.strictEqual(mod(), 'cjs-single-named-export')
mod = await ms.imports('./cjs-single-named-export.js')
assert.strictEqual(typeof mod, 'function')
assert.strictEqual(mod(), 'cjs-single-named-export')
})

it('should import ESM single named export', async () => {
let mod = await ms.imports('./esm-single-named-export.mjs', 'keyNameDoesntMatter')
assert.strictEqual(typeof mod, 'function')
assert.strictEqual(mod(), 'esm-single-named-export')
mod = await ms.imports('./esm-single-named-export.mjs')
assert.strictEqual(typeof mod, 'function')
assert.strictEqual(mod(), 'esm-single-named-export')
})

it('should import all when no default export', async () => {
const mod = await ms.imports('./esm-multiple-exports.mjs')
assert.strictEqual(typeof mod, 'object')
assert.deepStrictEqual(Object.keys(mod), ['first', 'second'])
})

it('should import JSON', async () => {
const mod = await ms.imports('./local.json')
assert.strictEqual(typeof mod, 'object')
assert.deepStrictEqual(mod.default, { key: 'value' })
})

it('should error on invalid specifiers', async () => {
const argSets = [[], ['./nonExistantLocalModule.js'], ['./cjs-single-named-export.js', 'nonExistantExport']]

const results = await Promise.all(
argSets.map((args) => {
return ms.imports(...args).catch((err) => {
const toCheck = err.toString().split(/\n| module "| from "/)[0]
return Promise.resolve(toCheck)
})
})
)

assert.deepStrictEqual(results, [
'Error: Metalsmith.import() - "undefined" is not a valid import specifier.',
'Error: Metalsmith.import() cannot find',
'Error: Metalsmith.import() cannot import "nonExistantExport"'
])
})
})

describe('#read', function () {
it('should read from a source directory', function (done) {
const m = Metalsmith(fixture('read'))
Expand Down Expand Up @@ -1507,17 +1584,17 @@ describe('CLI', function () {
it('should error when failing to require a plugin', function (done) {
exec(bin, { cwd: fixture('cli-no-plugin') }, function (err) {
assert(err)
assert(~err.message.indexOf('failed to require plugin "metalsmith-non-existant".'))
const expected = 'Metalsmith · Error: Metalsmith.import() cannot find module "metalsmith-non-existant".'
assert.ok(err.message.includes(expected))
done()
})
})

it('should error when failing to use a plugin', function (done) {
exec(bin, { cwd: fixture('cli-broken-plugin') }, function (err) {
assert(err)
assert(~err.message.indexOf('error using plugin "./plugin"...'))
assert(~err.message.indexOf('Break!'))
assert(~err.message.indexOf('at module.exports'))
const expected = 'Metalsmith · Error: Initialization of plugin "./plugin" resulted in error:'
assert.ok(err.message.includes(expected))
done()
})
})
Expand Down

0 comments on commit bdf4bed

Please sign in to comment.