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

loader: fix package resolution for edge case #41218

Merged
7 changes: 6 additions & 1 deletion lib/internal/modules/esm/resolve.js
Expand Up @@ -467,7 +467,12 @@ function resolvePackageTargetString(
const composeResult = (resolved) => {
let format;
try {
format = getPackageType(resolved);
// Extension has higher priority than package.json type descriptor
if (StringPrototypeEndsWith(resolved.href, '.mjs')) {
format = 'module';
} else {
format = getPackageType(resolved);
dygabo marked this conversation as resolved.
Show resolved Hide resolved
}
dygabo marked this conversation as resolved.
Show resolved Hide resolved
} catch (err) {
if (err.code === 'ERR_INVALID_FILE_URL_PATH') {
const invalidModuleErr = new ERR_INVALID_MODULE_SPECIFIER(
Expand Down
240 changes: 163 additions & 77 deletions test/es-module/test-esm-resolve-type.js
Expand Up @@ -98,86 +98,172 @@ try {
}
};

// Create a dummy dual package
//
/**
* this creates following directory structure:
*
* ./node_modules:
* |-> my-dual-package
* |-> es
* |-> index.js
* |-> package.json [2]
* |-> lib
* |-> index.js
* |->package.json [1]
*
* [1] - main package.json of the package
* - it contains:
* - type: 'commonjs'
* - main: 'lib/mainfile.js'
* - conditional exports for 'require' (lib/index.js) and
* 'import' (es/index.js)
* [2] - package.json add-on for the import case
* - it only contains:
* - type: 'module'
*
* in case the package is consumed as an ESM by importing it:
* import * as my-package from 'my-dual-package'
* it will cause the resolve method to return:
* {
* url: '<base_path>/node_modules/my-dual-package/es/index.js',
* format: 'module'
* }
*
* following testcase ensures that resolve works correctly in this case
* returning the information as specified above. Source for 'url' value
* is [1], source for 'format' value is [2]
*/
function testDualPackageWithJsMainScriptAndModuleType() {
// Create a dummy dual package
//
/**
* this creates following directory structure:
dygabo marked this conversation as resolved.
Show resolved Hide resolved
*
* ./node_modules:
* |-> my-dual-package
* |-> es
* |-> index.js
* |-> package.json [2]
* |-> lib
* |-> index.js
* |->package.json [1]
*
* [1] - main package.json of the package
* - it contains:
* - type: 'commonjs'
* - main: 'lib/mainfile.js'
* - conditional exports for 'require' (lib/index.js) and
* 'import' (es/index.js)
* [2] - package.json add-on for the import case
* - it only contains:
* - type: 'module'
*
dygabo marked this conversation as resolved.
Show resolved Hide resolved
* in case the package is consumed as an ESM by importing it:
dygabo marked this conversation as resolved.
Show resolved Hide resolved
* import * as my-package from 'my-dual-package'
* it will cause the resolve method to return:
* {
* url: '<base_path>/node_modules/my-dual-package/es/index.js',
* format: 'module'
* }
*
* following testcase ensures that resolve works correctly in this case
* returning the information as specified above. Source for 'url' value
* is [1], source for 'format' value is [2]
*/

const moduleName = 'my-dual-package';

const mDir = rel(`node_modules/${moduleName}`);
const esSubDir = rel(`node_modules/${moduleName}/es`);
const cjsSubDir = rel(`node_modules/${moduleName}/lib`);
const pkg = rel(`node_modules/${moduleName}/package.json`);
const esmPkg = rel(`node_modules/${moduleName}/es/package.json`);
const esScript = rel(`node_modules/${moduleName}/es/index.js`);
const cjsScript = rel(`node_modules/${moduleName}/lib/index.js`);

createDir(nmDir);
createDir(mDir);
createDir(esSubDir);
createDir(cjsSubDir);

const mainPkgJsonContent = {
type: 'commonjs',
main: 'lib/index.js',
exports: {
'.': {
'require': './lib/index.js',
'import': './es/index.js'
},
'./package.json': './package.json',
}
};
const esmPkgJsonContent = {
type: 'module'
};
const moduleName = 'my-dual-package';

fs.writeFileSync(pkg, JSON.stringify(mainPkgJsonContent));
fs.writeFileSync(esmPkg, JSON.stringify(esmPkgJsonContent));
fs.writeFileSync(esScript,
'export function esm-resolve-tester() {return 42}');
fs.writeFileSync(cjsScript,
`module.exports = {
esm-resolve-tester: () => {return 42}}`
);
const mDir = rel(`node_modules/${moduleName}`);
const esSubDir = rel(`node_modules/${moduleName}/es`);
const cjsSubDir = rel(`node_modules/${moduleName}/lib`);
const pkg = rel(`node_modules/${moduleName}/package.json`);
const esmPkg = rel(`node_modules/${moduleName}/es/package.json`);
const esScript = rel(`node_modules/${moduleName}/es/index.js`);
const cjsScript = rel(`node_modules/${moduleName}/lib/index.js`);

createDir(nmDir);
createDir(mDir);
createDir(esSubDir);
createDir(cjsSubDir);

const mainPkgJsonContent = {
type: 'commonjs',
main: 'lib/index.js',
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I think node doesn't even consume main anymore (main is obsolete in node v12+, and v12- is end of life).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the proposal would be to renounce at the main all together? I did that as it does not impact the test but this example was built originally to simulate the situation found in highlight.js's package.json that being the main reason why this is here.

Copy link
Contributor

Choose a reason for hiding this comment

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

If we remove "main", let’s also remove "default" and "require", which are not used by this test either. Or let’s keep all of them so the test is closer to real world implementations 🤷‍♂️

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, true. My comment was just a nit, so no change needed. If it were me, I would KISS and remove the unused bits, but maybe not worth the effort.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Personally, I would go with test scenarios as close to real world scenarios as possible (would mean having main defined in package.json even if it is currently not used internally because it is used in real life and would be good to make sure those will not break in the future either). But I really have no strong opinion on this. Just let me know how you think this should be done.

exports: {
'.': {
'require': './lib/index.js',
'import': './es/index.js'
},
guybedford marked this conversation as resolved.
Show resolved Hide resolved
'./package.json': './package.json',
}
};
const esmPkgJsonContent = {
type: 'module'
};

fs.writeFileSync(pkg, JSON.stringify(mainPkgJsonContent));
fs.writeFileSync(esmPkg, JSON.stringify(esmPkgJsonContent));
fs.writeFileSync(esScript,
'export function esm-resolve-tester() {return 42}');
fs.writeFileSync(cjsScript,
`module.exports = {
esm-resolve-tester: () => {return 42}}`
);
dygabo marked this conversation as resolved.
Show resolved Hide resolved

// test the resolve
const resolveResult = resolve(`${moduleName}`);
assert.strictEqual(resolveResult.format, 'module');
assert.ok(resolveResult.url.includes('my-dual-package/es/index.js'));
}

function testDualPackageWithMjsMainScriptAndCJSType() {

// Additional test for following scenario
/**
* this creates following directory structure:
*
* ./node_modules:
* |-> dual-mjs-pjson
* |-> subdir
* |-> index.mjs [3]
* |-> package.json [2]
* |-> index.js
* |->package.json [1]
*
* [1] - main package.json of the package
* - it contains:
* - type: 'commonjs'
* - main: 'subdir/index.js'
* - conditional exports for 'require' (subdir/index.js) and
* 'import' (subdir/index.mjs)
* [2] - package.json add-on for the import case
* - it only contains:
* - type: 'commonjs'
* [3] - main script for the `import` case
*
* in case the package is consumed as an ESM by importing it:
* import * as my-package from 'dual-mjs-pjson'
* it will cause the resolve method to return:
* {
* url: '<base_path>/node_modules/dual-mjs-pjson/subdir/index.mjs',
* format: 'module'
* }
*
* following testcase ensures that resolve works correctly in this case
* returning the information as specified above. Source for 'url' value
* is [1], source for 'format' value is the file extension of [3]
*/
const moduleName = 'dual-mjs-pjson';

const mDir = rel(`node_modules/${moduleName}`);
const subDir = rel(`node_modules/${moduleName}/subdir`);
const pkg = rel(`node_modules/${moduleName}/package.json`);
const subdirPkg = rel(`node_modules/${moduleName}/subdir/package.json`);
const esScript = rel(`node_modules/${moduleName}/subdir/index.mjs`);
const cjsScript = rel(`node_modules/${moduleName}/subdir/index.js`);

createDir(nmDir);
createDir(mDir);
createDir(subDir);

const mainPkgJsonContent = {
type: 'commonjs',
main: 'lib/index.js',
exports: {
'.': {
'require': './subdir/index.js',
'import': './subdir/index.mjs'
},
'./package.json': './package.json',
}
};
const subdirPkgJsonContent = {
type: 'commonjs'
};

fs.writeFileSync(pkg, JSON.stringify(mainPkgJsonContent));
fs.writeFileSync(subdirPkg, JSON.stringify(subdirPkgJsonContent));
fs.writeFileSync(esScript,
'export function esm-resolve-tester() {return 42}');
fs.writeFileSync(cjsScript,
`module.exports = {
esm-resolve-tester: () => {return 42}}`
);
dygabo marked this conversation as resolved.
Show resolved Hide resolved

// test the resolve
const resolveResult = resolve(`${moduleName}`);
assert.strictEqual(resolveResult.format, 'module');
assert.ok(resolveResult.url.includes(`${moduleName}/subdir/index.mjs`));
}

testDualPackageWithJsMainScriptAndModuleType();
testDualPackageWithMjsMainScriptAndCJSType();

// test the resolve
const resolveResult = resolve(`${moduleName}`);
assert.strictEqual(resolveResult.format, 'module');
assert.ok(resolveResult.url.includes('my-dual-package/es/index.js'));
} finally {
process.chdir(previousCwd);
fs.rmSync(nmDir, { recursive: true, force: true });
Expand Down