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 4, 2024
1 parent 0d8d791 commit 8fcc894
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 0 deletions.
55 changes: 55 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -779,3 +779,58 @@ 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('.')) 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('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"
}
}
77 changes: 77 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,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

0 comments on commit 8fcc894

Please sign in to comment.