Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: should try to use esm module first #28

Merged
merged 11 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions .github/workflows/nodejs-14.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:

jobs:
build:
name: Test on Node.js 14
runs-on: ubuntu-latest

steps:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/nodejs-16.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:

jobs:
build:
name: Test on Node.js 16
runs-on: ubuntu-latest

steps:
Expand Down
15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
"description": "Utils for all egg projects",
"scripts": {
"lint": "eslint src test --ext ts",
"pretest": "npm run prepublishOnly",
"test": "npm run lint -- --fix && npm run test-local",
"pretest": "npm run clean && npm run lint -- --fix && npm run prepublishOnly",
"test": "npm run test-local",
"test-local": "egg-bin test",
"preci": "npm run prepublishOnly",
"ci": "npm run lint && egg-bin cov && npm run prepublishOnly",
"prepublishOnly": "tshy && tshy-after"
"preci": "npm run clean && npm run lint && npm run prepublishOnly",
"ci": "egg-bin cov",
"clean": "rimraf dist",
"prepublishOnly": "tshy && tshy-after && attw --pack"
},
"keywords": [
"egg",
Expand All @@ -29,15 +30,17 @@
"license": "MIT",
"dependencies": {},
"devDependencies": {
"@arethetypeswrong/cli": "^0.17.2",
"@eggjs/tsconfig": "1",
"@types/mocha": "10",
"@types/node": "22",
"coffee": "5",
"egg-bin": "6",
"eslint": "8",
"eslint-config-egg": "14",
"mm": "3",
"mm": "4",
"npminstall": "7",
"rimraf": "6",
"runscript": "2",
"tshy": "3",
"tshy-after": "1",
Expand Down
175 changes: 136 additions & 39 deletions src/import.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { debuglog } from 'node:util';
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
import { pathToFileURL, fileURLToPath } from 'node:url';
import path from 'node:path';
import fs from 'node:fs';

Expand All @@ -16,6 +16,8 @@
}

const isESM = typeof require === 'undefined';
const nodeMajorVersion = parseInt(process.versions.node.split('.', 1)[0], 10);
const supportImportMetaResolve = nodeMajorVersion >= 18;

let _customRequire: NodeRequire;
function getRequire() {
Expand All @@ -39,18 +41,90 @@
return _supportTypeScript;
}

function tryToGetTypeScriptMainFile(pkg: any, baseDir: string): string | undefined {
function tryToResolveFromFile(filepath: string): string | undefined {
// "type": "module", try index.mjs then index.js
const type = isESM ? 'module' : 'commonjs';
let mainIndexFile = '';
if (type === 'module') {
mainIndexFile = filepath + '.mjs';
if (fs.existsSync(mainIndexFile)) {
debug('[tryToResolveFromFile] %o, use index.mjs, type: %o', mainIndexFile, type);
return mainIndexFile;
}
mainIndexFile = filepath + '.js';
if (fs.existsSync(mainIndexFile)) {
debug('[tryToResolveFromFile] %o, use index.js, type: %o', mainIndexFile, type);
return mainIndexFile;
}
} else {
// "type": "commonjs", try index.js then index.cjs
mainIndexFile = filepath + '.cjs';
if (fs.existsSync(mainIndexFile)) {
debug('[tryToResolveFromFile] %o, use index.cjs, type: %o', mainIndexFile, type);
return mainIndexFile;
}
mainIndexFile = filepath + '.js';
if (fs.existsSync(mainIndexFile)) {
debug('[tryToResolveFromFile] %o, use index.js, type: %o', mainIndexFile, type);
return mainIndexFile;
}
}

Check warning on line 71 in src/import.ts

View check run for this annotation

Codecov / codecov/patch

src/import.ts#L60-L71

Added lines #L60 - L71 were not covered by tests

if (!isSupportTypeScript()) {
return;
}

Check warning on line 75 in src/import.ts

View check run for this annotation

Codecov / codecov/patch

src/import.ts#L74-L75

Added lines #L74 - L75 were not covered by tests

// for the module under development
mainIndexFile = filepath + '.ts';
if (fs.existsSync(mainIndexFile)) {
debug('[tryToResolveFromFile] %o, use index.ts, type: %o', mainIndexFile, type);
return mainIndexFile;
}
}

function tryToResolveByDirnameFromPackage(dirname: string, pkg: any): string | undefined {
// try to read pkg.main or pkg.module first
// "main": "./dist/commonjs/index.js",
// "module": "./dist/esm/index.js"
const defaultMainFile = isESM ? pkg.module ?? pkg.main : pkg.main;
if (defaultMainFile) {
const mainIndexFilePath = path.join(baseDir, defaultMainFile);
const mainIndexFilePath = path.join(dirname, defaultMainFile);
if (fs.existsSync(mainIndexFilePath)) {
debug('[tryToGetTypeScriptMainFile] %o, use pkg.main or pkg.module: %o, isESM: %s',
debug('[tryToResolveByDirnameFromPackage] %o, use pkg.main or pkg.module: %o, isESM: %s',
mainIndexFilePath, defaultMainFile, isESM);
return;
return mainIndexFilePath;
}
}

// "type": "module", try index.mjs then index.js
const type = pkg?.type ?? (isESM ? 'module' : 'commonjs');
if (type === 'module') {
const mainIndexFilePath = path.join(dirname, 'index.mjs');
if (fs.existsSync(mainIndexFilePath)) {
debug('[tryToResolveByDirnameFromPackage] %o, use index.mjs, pkg.type: %o', mainIndexFilePath, type);
return mainIndexFilePath;
}

Check warning on line 106 in src/import.ts

View check run for this annotation

Codecov / codecov/patch

src/import.ts#L104-L106

Added lines #L104 - L106 were not covered by tests
const mainIndexMjsFilePath = path.join(dirname, 'index.js');
if (fs.existsSync(mainIndexMjsFilePath)) {
debug('[tryToResolveByDirnameFromPackage] %o, use index.js, pkg.type: %o', mainIndexMjsFilePath, type);
return mainIndexMjsFilePath;
}
} else {
// "type": "commonjs", try index.cjs then index.js
const mainIndexFilePath = path.join(dirname, 'index.cjs');
if (fs.existsSync(mainIndexFilePath)) {
debug('[tryToResolveByDirnameFromPackage] %o, use index.cjs, pkg.type: %o', mainIndexFilePath, type);
return mainIndexFilePath;

Check warning on line 117 in src/import.ts

View check run for this annotation

Codecov / codecov/patch

src/import.ts#L116-L117

Added lines #L116 - L117 were not covered by tests
}
const mainIndexCjsFilePath = path.join(dirname, 'index.js');
if (fs.existsSync(mainIndexCjsFilePath)) {
debug('[tryToResolveByDirnameFromPackage] %o, use index.js, pkg.type: %o', mainIndexCjsFilePath, type);
return mainIndexCjsFilePath;
}
}

if (!isSupportTypeScript()) {
return;

Check warning on line 127 in src/import.ts

View check run for this annotation

Codecov / codecov/patch

src/import.ts#L127

Added line #L127 was not covered by tests
}

// for the module under development
Expand All @@ -60,56 +134,67 @@
// ".": "./src/index.ts"
// }
// }
const mainIndexFile = pkg.tshy?.exports?.['.'];
if (mainIndexFile) {
const mainIndexFilePath = path.join(baseDir, mainIndexFile);
if (fs.existsSync(mainIndexFilePath)) {
return mainIndexFilePath;
}
const mainIndexFile = pkg.tshy?.exports?.['.'] ?? 'index.ts';
const mainIndexFilePath = path.join(dirname, mainIndexFile);
if (fs.existsSync(mainIndexFilePath)) {
return mainIndexFilePath;
}
}

function tryToResolveByDirname(dirname: string): string | undefined {
const pkgFile = path.join(dirname, 'package.json');
if (fs.existsSync(pkgFile)) {
const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf-8'));
return tryToResolveByDirnameFromPackage(dirname, pkg);
}
}

export function importResolve(filepath: string, options?: ImportResolveOptions) {
// support typescript import on absolute path
if (path.isAbsolute(filepath) && isSupportTypeScript()) {
const pkgFile = path.join(filepath, 'package.json');
if (fs.existsSync(pkgFile)) {
const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf-8'));
const mainFile = tryToGetTypeScriptMainFile(pkg, filepath);
if (mainFile) {
debug('[importResolve] %o, use typescript main file: %o', filepath, mainFile);
return mainFile;
let moduleFilePath: string | undefined;
if (path.isAbsolute(filepath)) {
const stat = fs.statSync(filepath, { throwIfNoEntry: false });
if (stat?.isDirectory()) {
moduleFilePath = tryToResolveByDirname(filepath);
if (moduleFilePath) {
debug('[importResolve] %o => %o', filepath, moduleFilePath);
return moduleFilePath;
}
}
if (!stat) {
moduleFilePath = tryToResolveFromFile(filepath);
if (moduleFilePath) {
debug('[importResolve] %o => %o', filepath, moduleFilePath);
return moduleFilePath;
}
}
}
const cwd = process.cwd();
const paths = options?.paths ?? [ cwd ];
const moduleFilePath = getRequire().resolve(filepath, {
paths,
});

if (isESM) {
if (supportImportMetaResolve) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
moduleFilePath = fileURLToPath(import.meta.resolve(filepath));
} else {
moduleFilePath = getRequire().resolve(filepath);
}

Check warning on line 179 in src/import.ts

View check run for this annotation

Codecov / codecov/patch

src/import.ts#L178-L179

Added lines #L178 - L179 were not covered by tests
} else {
const cwd = process.cwd();
const paths = options?.paths ?? [ cwd ];
moduleFilePath = require.resolve(filepath, {
paths,
});
}

Check warning on line 186 in src/import.ts

View check run for this annotation

Codecov / codecov/patch

src/import.ts#L181-L186

Added lines #L181 - L186 were not covered by tests
debug('[importResolve] %o, options: %o => %o', filepath, options, moduleFilePath);
return moduleFilePath;
}

export async function importModule(filepath: string, options?: ImportModuleOptions) {
const moduleFilePath = importResolve(filepath, options);
let obj: any;
if (typeof require === 'function') {
// commonjs
obj = require(moduleFilePath);
debug('[importModule] require %o => %o', filepath, obj);
if (obj?.__esModule === true && 'default' in obj) {
// 兼容 cjs 模拟 esm 的导出格式
// {
// __esModule: true,
// default: { fn: [Function: fn], foo: 'bar', one: 1 }
// }
obj = obj.default;
}
} else {
if (isESM) {
// esm
debug('[importModule] await import start: %o', filepath);
const fileUrl = pathToFileURL(moduleFilePath).toString();
debug('[importModule] await import start: %o', fileUrl);
obj = await import(fileUrl);
debug('[importModule] await import end: %o => %o', filepath, obj);
// {
Expand Down Expand Up @@ -151,6 +236,18 @@
obj = obj.default;
}
}
} else {
// commonjs
obj = require(moduleFilePath);
debug('[importModule] require %o => %o', filepath, obj);
if (obj?.__esModule === true && 'default' in obj) {
// 兼容 cjs 模拟 esm 的导出格式
// {
// __esModule: true,
// default: { fn: [Function: fn], foo: 'bar', one: 1 }
// }
obj = obj.default;
}

Check warning on line 250 in src/import.ts

View check run for this annotation

Codecov / codecov/patch

src/import.ts#L240-L250

Added lines #L240 - L250 were not covered by tests
}
debug('[importModule] return %o => %o', filepath, obj);
return obj;
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/ts-module/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
4 changes: 2 additions & 2 deletions test/framework.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import path from 'node:path';
import { strict as assert } from 'node:assert';
import fs from 'node:fs';
import mm from 'mm';
import { mm, restore } from 'mm';
import { getFrameworkPath } from '../src/index.js';
import { getFilepath, testDir } from './helper.js';

describe('test/framework.test.ts', () => {
afterEach(mm.restore);
afterEach(restore);

it('should exist when specify baseDir', () => {
it('should get egg by default but not exist', () => {
Expand Down
22 changes: 16 additions & 6 deletions test/import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@ describe('test/import.test.ts', () => {
describe('importResolve()', () => {
it('should work on cjs', () => {
assert.equal(importResolve(getFilepath('cjs')), getFilepath('cjs/index.js'));
assert.equal(importResolve(getFilepath('cjs/exports')), getFilepath('cjs/exports.js'));
});

it('should work on esm', () => {
assert.equal(importResolve(getFilepath('esm')), getFilepath('esm/index.js'));
});

it('should work on ts-module', () => {
assert.equal(importResolve(getFilepath('ts-module')), getFilepath('ts-module/index.ts'));
});

it('should work on typescript without dist', () => {
assert.equal(importResolve(getFilepath('tshy')), getFilepath('tshy/src/index.ts'));
});

it('should work on typescript with dist', () => {
assert.equal(importResolve(getFilepath('tshy-dist')), getFilepath('tshy-dist/dist2/commonjs/index.js'));
assert.equal(importResolve(getFilepath('tshy-dist')), getFilepath('tshy-dist/dist2/esm/index.js'));
});
});

Expand Down Expand Up @@ -132,24 +137,29 @@ describe('test/import.test.ts', () => {

it('should work on ts-module', async () => {
let obj = await importModule(getFilepath('ts-module'));
assert.deepEqual(Object.keys(obj), [ 'default', 'one' ]);
assert.deepEqual(Object.keys(obj), [ 'one', 'default' ]);
assert.equal(obj.one, 1);
assert.deepEqual(obj.default, { foo: 'bar' });

obj = await importModule(getFilepath('ts-module'), { importDefaultOnly: true });
assert.deepEqual(obj, { foo: 'bar' });

obj = await importModule(getFilepath('ts-module/exports'));
assert.deepEqual(Object.keys(obj), [ 'foo', 'one' ]);
assert.equal(obj.foo, 'bar');
assert.equal(obj.one, 1);
if (process.version.startsWith('v23.')) {
// support `module.exports` on Node.js >=23
assert.deepEqual(Object.keys(obj), [ 'default', 'module.exports' ]);
} else {
assert.deepEqual(Object.keys(obj), [ 'default' ]);
}
assert.equal(obj.default.foo, 'bar');
assert.equal(obj.default.one, 1);

obj = await importModule(getFilepath('ts-module/exports'), { importDefaultOnly: true });
assert.deepEqual(Object.keys(obj), [ 'foo', 'one' ]);
assert.equal(obj.foo, 'bar');
assert.equal(obj.one, 1);

obj = await importModule(getFilepath('ts-module/exports.ts'));
obj = await importModule(getFilepath('ts-module/exports.ts'), { importDefaultOnly: true });
assert.deepEqual(Object.keys(obj), [ 'foo', 'one' ]);
assert.equal(obj.foo, 'bar');
assert.equal(obj.one, 1);
Expand Down
Loading
Loading