Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 70 additions & 3 deletions lib/get-exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

const getEsmExports = require('./get-esm-exports.js')
const { parse: parseCjs } = require('cjs-module-lexer')
const { readFileSync } = require('fs')
const { readFileSync, existsSync } = require('fs')
const { builtinModules } = require('module')
const { fileURLToPath, pathToFileURL } = require('url')
const { dirname } = require('path')
const { dirname, join } = require('path')

function addDefault (arr) {
return new Set(['default', ...arr])
Expand All @@ -27,6 +27,64 @@ function getExportsForNodeBuiltIn (name) {

const urlsBeingProcessed = new Set() // Guard against circular imports.

/**
* This function looks for the package.json which contains the specifier trying to resolve.
* Once the package.json file has been found, we extract the file path from the specifier
* @param {*} specifier The specifier that is being search for inside the imports object
* @param {*} fromUrl The url from which the search starts from
* @returns file to url to file export
*/
function resolvePackageImports (specifier, fromUrl) {
if (!specifier.startsWith('#')) {
return null
}

try {
const fromPath = fileURLToPath(fromUrl)
let currentDir = dirname(fromPath)

// search for package.json file which has the real url to export
while (currentDir !== dirname(currentDir)) {
const packageJsonPath = join(currentDir, 'package.json')

if (existsSync(packageJsonPath)) {
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))

if (packageJson.imports && packageJson.imports[specifier]) {
const imports = packageJson.imports[specifier]

// Look for path inside packageJson
let resolvedPath
if (Object.prototype.toString.call(imports) === '[object Object]') {
const requireSpecifier = imports.require
const importSpecifier = imports.import
// look for the possibility of require and import which is standard for CJS/ESM
if (requireSpecifier || importSpecifier) {
// trying to resolve based on order of importance
resolvedPath = requireSpecifier.node || requireSpecifier.default || importSpecifier.node || importSpecifier.default
} else if (imports.node || imports.default) {
resolvedPath = imports.node || imports.default
}
} else if (typeof imports === 'string') {
resolvedPath = imports
}

if (resolvedPath) {
return resolvedPath
}
}
// return if we find a package.json but did not find an import
return null
}
currentDir = dirname(currentDir)
}
} catch (error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} catch (error) {
} catch {

Alternatively, we could also just add the cause

throw new Error(`Failed to find sub path export: ${specifier}`)
}

return null
}

async function getCjsExports (url, context, parentLoad, source) {
if (urlsBeingProcessed.has(url)) {
return []
Expand All @@ -46,7 +104,16 @@ async function getCjsExports (url, context, parentLoad, source) {
if (re === '.') {
re = './'
}
// Resolve the re-exported module relative to the current module.

// Entries in the import field should always start with #
if (re.startsWith('#')) {
re = resolvePackageImports(re, url)
if (!re) {
// Unable to resolve specifier import
return
}
}

const newUrl = pathToFileURL(require.resolve(re, { paths: [dirname(fileURLToPath(url))] })).href

if (newUrl.endsWith('.node') || newUrl.endsWith('.json')) {
Expand Down
17 changes: 17 additions & 0 deletions test/fixtures/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "test-fixtures",
"imports": {
"#main-entry-point": {
"require": {
"node": "./something.js",
"default": "./something.js"
},
"import": {
"node":"./something.mjs",
"default": "./something.js"
}
},
"#main-entry-point-string" : "./something.js",
"#main-entry-point-external" : "some-external-cjs-module"
}
}
1 change: 1 addition & 0 deletions test/fixtures/specifier-external.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = { ...require('#main-entry-point-external') }
1 change: 1 addition & 0 deletions test/fixtures/specifier-string.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = { ...require('#main-entry-point-string') }
1 change: 1 addition & 0 deletions test/fixtures/specifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = { ...require('#main-entry-point') }
11 changes: 11 additions & 0 deletions test/hook/specifier-external-imports.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { foo } from '../fixtures/specifier-external.js'
import Hook from '../../index.js'
import { strictEqual } from 'assert'

Hook((exports, name) => {
if (name.endsWith('fixtures/specifier-external.js')) {
exports.foo = 'bar2'
}
})

strictEqual(foo, 'bar2')
11 changes: 11 additions & 0 deletions test/hook/specifier-imports.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { foo } from '../fixtures/specifier.js'
import Hook from '../../index.js'
import { strictEqual } from 'assert'

Hook((exports, name) => {
if (name.endsWith('fixtures/specifier.js')) {
exports.foo = 1
}
})

strictEqual(foo, 1)
11 changes: 11 additions & 0 deletions test/hook/specifier-string-imports.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { foo } from '../fixtures/specifier-string.js'
import Hook from '../../index.js'
import { strictEqual } from 'assert'

Hook((exports, name) => {
if (name.endsWith('fixtures/specifier-string.js')) {
exports.foo = 1
}
})

strictEqual(foo, 1)