From 03eb8e176fceb86f68a9b4491d8fa73e9b888805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 31 Aug 2026 11:11:26 +0800 Subject: [PATCH] fix: resolve ESM imports from hoisted dependencies --- .github/workflows/test.yml | 1 + package.json | 5 +- src/babelPluginImportLib2Es.ts | 27 +++++++---- test/babelPluginImportLib2Es.test.js | 68 ++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 12 deletions(-) create mode 100644 test/babelPluginImportLib2Es.test.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6b70005..6fa1000 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,3 +22,4 @@ jobs: - run: npm install --legacy-peer-deps - run: npm run lint - run: npm run build + - run: npm test diff --git a/package.json b/package.json index 24fddee..d48a1df 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "lint": "npm run lint:es", "lint:es": "eslint \"{src,test}/**/*.{js,jsx,ts,tsx}\"", "prepare": "husky", - "prepublishOnly": "father doctor && npm run build && rc-np" + "prepublishOnly": "father doctor && npm run build && npm test && rc-np", + "test": "node --test" }, "commitlint": { "extends": [ @@ -41,8 +42,8 @@ "devDependencies": { "@commitlint/cli": "^21.2.0", "@commitlint/config-conventional": "^21.2.0", - "@eslint/js": "^10.0.1", "@eslint/compat": "^2.1.0", + "@eslint/js": "^10.0.1", "@rc-component/np": "^1.0.4", "@types/fs-extra": "^11.0.4", "eslint": "^10.6.0", diff --git a/src/babelPluginImportLib2Es.ts b/src/babelPluginImportLib2Es.ts index 7e7c839..5220ba7 100644 --- a/src/babelPluginImportLib2Es.ts +++ b/src/babelPluginImportLib2Es.ts @@ -1,19 +1,26 @@ /** * migrate from https://github.com/umijs/father/blob/2.x/packages/father-build/src/importLibToEs.js */ -import fs from 'fs'; -import { dirname, join } from 'path'; - const cwd = process.cwd(); -function replacePath(path: any) { - if (path.node.source && /\/lib\//.test(path.node.source.value)) { - const esModule = path.node.source.value.replace('/lib/', '/es/'); - const esPath = dirname(join(cwd, `node_modules/${esModule}`)); +export function replaceLibWithEs(moduleName: string, paths = [cwd]) { + if (!/\/lib\//.test(moduleName)) { + return moduleName; + } + + const esModule = moduleName.replace('/lib/', '/es/'); - if (fs.existsSync(esPath)) { - path.node.source.value = esModule; - } + try { + require.resolve(esModule, { paths }); + return esModule; + } catch { + return moduleName; + } +} + +function replacePath(path: any) { + if (path.node.source) { + path.node.source.value = replaceLibWithEs(path.node.source.value); } } diff --git a/test/babelPluginImportLib2Es.test.js b/test/babelPluginImportLib2Es.test.js new file mode 100644 index 0000000..119face --- /dev/null +++ b/test/babelPluginImportLib2Es.test.js @@ -0,0 +1,68 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { afterEach, test } = require('node:test'); + +const { replaceLibWithEs } = require('../dist/babelPluginImportLib2Es'); + +const fixtures = []; + +afterEach(() => { + fixtures.splice(0).forEach((fixture) => { + fs.rmSync(fixture, { recursive: true, force: true }); + }); +}); + +function createFixture({ withEsm }) { + const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'father-plugin-')); + const consumer = path.join(fixture, 'packages', 'consumer'); + const packageDir = path.join(fixture, 'node_modules', 'fixture-icons'); + + fixtures.push(fixture); + fs.mkdirSync(consumer, { recursive: true }); + fs.mkdirSync(path.join(packageDir, 'lib', 'asn'), { recursive: true }); + fs.writeFileSync( + path.join(packageDir, 'package.json'), + '{"name":"fixture-icons"}', + ); + fs.writeFileSync( + path.join(packageDir, 'lib', 'asn', 'Smile.js'), + 'module.exports = {};', + ); + + if (withEsm) { + fs.mkdirSync(path.join(packageDir, 'es', 'asn'), { recursive: true }); + fs.writeFileSync( + path.join(packageDir, 'es', 'asn', 'Smile.js'), + 'export default {};', + ); + } + + return consumer; +} + +test('resolves ESM modules from a hoisted node_modules directory', () => { + const consumer = createFixture({ withEsm: true }); + + assert.equal( + replaceLibWithEs('fixture-icons/lib/asn/Smile', [consumer]), + 'fixture-icons/es/asn/Smile', + ); +}); + +test('keeps the CommonJS path when the ESM module is unavailable', () => { + const consumer = createFixture({ withEsm: false }); + + assert.equal( + replaceLibWithEs('fixture-icons/lib/asn/Smile', [consumer]), + 'fixture-icons/lib/asn/Smile', + ); +}); + +test('ignores imports outside lib directories', () => { + assert.equal( + replaceLibWithEs('fixture-icons/legacy/Smile'), + 'fixture-icons/legacy/Smile', + ); +});