Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
22 changes: 14 additions & 8 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: test

on:
push:
branches: [ dev ]
branches: [ master, dev ]
pull_request:
branches: [ dev ]
branches: [ master, dev ]
workflow_dispatch:

jobs:
Expand All @@ -17,7 +17,7 @@ jobs:

strategy:
matrix:
node-version: [6.x, 8.x, 10.x, 12.x, 14.x, 16.x, 17.x]
node-version: [6.x, 8.x, 10.x, 12.x, 14.x, 16.x, 17.x, 18.x]
fail-fast: false

steps:
Expand All @@ -32,21 +32,27 @@ jobs:
- run: npm run testonly

test-esm:
# CJS + ESM tests for Node 18+
runs-on: ubuntu-latest
# CJS + ESM tests for Node 20.19+ (mocha 2.x loads .mjs via require(),
# which needs require(esm) support - not available on Node 18)
runs-on: ${{ matrix.os }}

strategy:
matrix:
node-version: [18.x, 20.x, 22.x]
os: [ubuntu-latest, windows-latest]
node-version: [20.x, 22.x]
fail-fast: false

steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js ${{ matrix.node-version }} on ${{ matrix.os }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: node -v
- run: npm -v
- run: npm install
- run: npm run test:all
# Two CJS config-loading tests fail on Windows for reasons unrelated to
# the ESM loader (they predate it), so the CJS suite stays Linux-only
- run: npm run testonly
if: runner.os == 'Linux'
- run: npm run test:esm
6 changes: 3 additions & 3 deletions esm-loader.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Provides resolve hooks for ES modules

import { readFileSync, existsSync, statSync } from 'node:fs'
import { join, resolve as pathResolve, dirname } from 'node:path'
import { join, resolve as pathResolve, dirname, isAbsolute } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'

let aliases = {}
Expand Down Expand Up @@ -39,7 +39,7 @@ export function init (options = {}) {
const pkgAliases = pkg._moduleAliases || {}
for (const alias in pkgAliases) {
const target = pkgAliases[alias]
aliases[alias] = target.startsWith('/') ? target : join(base, target)
aliases[alias] = isAbsolute(target) ? target : join(base, target)
}

// Load _moduleDirectories
Expand Down Expand Up @@ -138,7 +138,7 @@ export async function resolve (specifier, context, nextResolve) {
const resolved = resolveAlias(specifier, context.parentURL)
if (resolved) {
// If absolute path, convert to file URL
if (resolved.startsWith('/')) {
if (isAbsolute(resolved)) {
return { url: pathToFileURL(resolved).href, shortCircuit: true }
}
// Otherwise let Node resolve it (could be npm package)
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
},
"scripts": {
"test": "npm run lint && npm run testonly",
"testonly": "NODE_ENV=test mocha test/specs.js",
"testonly-watch": "NODE_ENV=test mocha -w test/specs.js",
"test:esm": "NODE_ENV=test mocha test/esm/unit.mjs test/esm/integration.js",
"testonly": "mocha test/specs.js",
"testonly-watch": "mocha -w test/specs.js",
"test:esm": "mocha test/esm/unit.mjs test/esm/integration.js",
"test:all": "npm run testonly && npm run test:esm",
"lint": "standard"
},
Expand Down
6 changes: 6 additions & 0 deletions register.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Usage: node --import module-alias/register ./app.mjs

import { register } from 'node:module'
import { isAbsolute } from 'node:path'
import { pathToFileURL } from 'node:url'

// Check Node version for registerHooks support (22.15+)
const [major, minor] = process.versions.node.split('.').map(Number)
Expand All @@ -18,6 +20,10 @@ if (hasRegisterHooks) {
resolve (specifier, context, nextResolve) {
const resolved = resolveAlias(specifier, context.parentURL)
if (resolved) {
// If absolute path, convert to file URL
if (isAbsolute(resolved)) {
return { url: pathToFileURL(resolved).href, shortCircuit: true }
}
return nextResolve(resolved, context)
}
return nextResolve(specifier, context)
Expand Down
9 changes: 5 additions & 4 deletions test/esm/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ var expect = require('chai').expect
var execSync = require('child_process').execSync
var path = require('path')
var semver = require('semver')
var pathToFileURL = require('url').pathToFileURL

describe('ESM Integration', function () {
// Skip all ESM tests on Node < 18.19.0
Expand All @@ -17,10 +18,10 @@ describe('ESM Integration', function () {
describe('basic alias', function () {
it('should resolve alias in ESM with --import flag', function () {
var fixture = path.join(fixturesDir, 'basic')
var registerPath = path.join(moduleAliasRoot, 'register.mjs')
var registerUrl = pathToFileURL(path.join(moduleAliasRoot, 'register.mjs')).href

var result = execSync(
'node --import ' + registerPath + ' app.mjs',
'node --import ' + registerUrl + ' app.mjs',
{ cwd: fixture, encoding: 'utf8' }
)

Expand All @@ -31,10 +32,10 @@ describe('ESM Integration', function () {
describe('moduleDirectories', function () {
it('should resolve modules from custom directories', function () {
var fixture = path.join(fixturesDir, 'module-dirs')
var registerPath = path.join(moduleAliasRoot, 'register.mjs')
var registerUrl = pathToFileURL(path.join(moduleAliasRoot, 'register.mjs')).href

var result = execSync(
'node --import ' + registerPath + ' app.mjs',
'node --import ' + registerUrl + ' app.mjs',
{ cwd: fixture, encoding: 'utf8' }
)

Expand Down
46 changes: 41 additions & 5 deletions test/esm/unit.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-env mocha */
import chai from 'chai'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { isPathMatchesAlias, resolveAlias, reset, addAlias, addAliases } from '../../esm-loader.mjs'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { isPathMatchesAlias, resolveAlias, reset, addAlias, addAliases, resolve } from '../../esm-loader.mjs'

const { expect } = chai
const __dirname = path.dirname(fileURLToPath(import.meta.url))
Expand Down Expand Up @@ -76,7 +76,7 @@ describe('ESM Loader', function () {
})

const result = resolveAlias('react-dom/server')
expect(result).to.equal('/path/to/server')
expect(result).to.equal(path.join('/path/to/server'))
})

it('should handle multiple aliases', function () {
Expand All @@ -85,8 +85,44 @@ describe('ESM Loader', function () {
'@utils': '/path/to/utils'
})

expect(resolveAlias('@lib/foo')).to.equal('/path/to/lib/foo')
expect(resolveAlias('@utils/bar')).to.equal('/path/to/utils/bar')
expect(resolveAlias('@lib/foo')).to.equal(path.join('/path/to/lib', 'foo'))
expect(resolveAlias('@utils/bar')).to.equal(path.join('/path/to/utils', 'bar'))
})
})

describe('resolve hook', function () {
afterEach(function () {
reset()
})

it('should convert absolute paths to file URLs', async function () {
// Platform-native absolute path (C:\... on Windows, /... on POSIX)
const targetPath = path.join(__dirname, '../src')
addAlias('@src', targetPath)

const result = await resolve('@src/foo', {}, () => {
throw new Error('nextResolve should not be called for absolute paths')
})
expect(result.url).to.equal(pathToFileURL(path.join(targetPath, 'foo')).href)
expect(result.shortCircuit).to.equal(true)
})

it('should pass non-absolute resolved specifiers to nextResolve', async function () {
addAlias('lodash-alias', 'lodash')

const result = await resolve('lodash-alias/fp', {}, (specifier) => {
return { url: 'resolved:' + specifier }
})
expect(result.url).to.equal('resolved:' + path.join('lodash', 'fp'))
})

it('should pass non-aliased specifiers through unchanged', async function () {
addAlias('@src', '/some/path')

const result = await resolve('other-module', {}, (specifier) => {
return { url: 'resolved:' + specifier }
})
expect(result.url).to.equal('resolved:other-module')
})
})
})
Loading