Skip to content

Commit 07fa545

Browse files
committed
loader: return package format from defaultResolve if known
This is a proposed modification of defaultResolve to return the package format in case it has been found during package resolution. The format will be returned as described in the documentation: https://nodejs.org/api/esm.html#resolvespecifier-context-defaultresolve There is one new unit test as well: test/es-module/test-esm-resolve-type.js PR-URL: nodejs#40980 Reviewed-By: Bradley Farias <[email protected]> Reviewed-By: Gerhard Stöbich <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> Backport-PR-URL: nodejs#41752
1 parent 5607d3f commit 07fa545

File tree

7 files changed

+368
-46
lines changed

7 files changed

+368
-46
lines changed

lib/internal/modules/esm/resolve.js

+108-46
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,23 @@ const patternRegEx = /\*/g;
487487

488488
function resolvePackageTargetString(
489489
target, subpath, match, packageJSONUrl, base, pattern, internal, conditions) {
490+
491+
const composeResult = (resolved) => {
492+
let format;
493+
try {
494+
format = getPackageType(resolved);
495+
} catch (err) {
496+
if (err.code === 'ERR_INVALID_FILE_URL_PATH') {
497+
const invalidModuleErr = new ERR_INVALID_MODULE_SPECIFIER(
498+
resolved, 'must not include encoded "/" or "\\" characters', base);
499+
invalidModuleErr.cause = err;
500+
throw invalidModuleErr;
501+
}
502+
throw err;
503+
}
504+
return { resolved, ...(format !== 'none') && { format } };
505+
};
506+
490507
if (subpath !== '' && !pattern && target[target.length - 1] !== '/')
491508
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
492509

@@ -502,7 +519,8 @@ function resolvePackageTargetString(
502519
const exportTarget = pattern ?
503520
RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) :
504521
target + subpath;
505-
return packageResolve(exportTarget, packageJSONUrl, conditions);
522+
return packageResolve(
523+
exportTarget, packageJSONUrl, conditions);
506524
}
507525
}
508526
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
@@ -518,15 +536,18 @@ function resolvePackageTargetString(
518536
if (!StringPrototypeStartsWith(resolvedPath, packagePath))
519537
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
520538

521-
if (subpath === '') return resolved;
539+
if (subpath === '') return composeResult(resolved);
522540

523541
if (RegExpPrototypeTest(invalidSegmentRegEx, subpath))
524542
throwInvalidSubpath(match + subpath, packageJSONUrl, internal, base);
525543

526-
if (pattern)
527-
return new URL(RegExpPrototypeSymbolReplace(patternRegEx, resolved.href,
528-
() => subpath));
529-
return new URL(subpath, resolved);
544+
if (pattern) {
545+
return composeResult(new URL(RegExpPrototypeSymbolReplace(patternRegEx,
546+
resolved.href,
547+
() => subpath)));
548+
}
549+
550+
return composeResult(new URL(subpath, resolved));
530551
}
531552

532553
/**
@@ -552,9 +573,9 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
552573
let lastException;
553574
for (let i = 0; i < target.length; i++) {
554575
const targetItem = target[i];
555-
let resolved;
576+
let resolveResult;
556577
try {
557-
resolved = resolvePackageTarget(
578+
resolveResult = resolvePackageTarget(
558579
packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern,
559580
internal, conditions);
560581
} catch (e) {
@@ -563,13 +584,13 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
563584
continue;
564585
throw e;
565586
}
566-
if (resolved === undefined)
587+
if (resolveResult === undefined)
567588
continue;
568-
if (resolved === null) {
589+
if (resolveResult === null) {
569590
lastException = null;
570591
continue;
571592
}
572-
return resolved;
593+
return resolveResult;
573594
}
574595
if (lastException === undefined || lastException === null)
575596
return lastException;
@@ -588,12 +609,12 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
588609
const key = keys[i];
589610
if (key === 'default' || conditions.has(key)) {
590611
const conditionalTarget = target[key];
591-
const resolved = resolvePackageTarget(
612+
const resolveResult = resolvePackageTarget(
592613
packageJSONUrl, conditionalTarget, subpath, packageSubpath, base,
593614
pattern, internal, conditions);
594-
if (resolved === undefined)
615+
if (resolveResult === undefined)
595616
continue;
596-
return resolved;
617+
return resolveResult;
597618
}
598619
}
599620
return undefined;
@@ -652,12 +673,15 @@ function packageExportsResolve(
652673
!StringPrototypeIncludes(packageSubpath, '*') &&
653674
!StringPrototypeEndsWith(packageSubpath, '/')) {
654675
const target = exports[packageSubpath];
655-
const resolved = resolvePackageTarget(
676+
const resolveResult = resolvePackageTarget(
656677
packageJSONUrl, target, '', packageSubpath, base, false, false, conditions
657678
);
658-
if (resolved === null || resolved === undefined)
679+
680+
if (resolveResult == null) {
659681
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
660-
return { resolved, exact: true };
682+
}
683+
684+
return { ...resolveResult, exact: true };
661685
}
662686

663687
let bestMatch = '';
@@ -693,14 +717,25 @@ function packageExportsResolve(
693717
if (bestMatch) {
694718
const target = exports[bestMatch];
695719
const pattern = StringPrototypeIncludes(bestMatch, '*');
696-
const resolved = resolvePackageTarget(packageJSONUrl, target,
697-
bestMatchSubpath, bestMatch, base,
698-
pattern, false, conditions);
699-
if (resolved === null || resolved === undefined)
720+
const resolveResult = resolvePackageTarget(
721+
packageJSONUrl,
722+
target,
723+
bestMatchSubpath,
724+
bestMatch,
725+
base,
726+
pattern,
727+
false,
728+
conditions);
729+
730+
if (resolveResult == null) {
700731
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
701-
if (!pattern)
732+
}
733+
734+
if (!pattern) {
702735
emitFolderMapDeprecation(bestMatch, packageJSONUrl, true, base);
703-
return { resolved, exact: pattern };
736+
}
737+
738+
return { ...resolveResult, exact: pattern };
704739
}
705740

706741
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
@@ -740,11 +775,12 @@ function packageImportsResolve(name, base, conditions) {
740775
if (ObjectPrototypeHasOwnProperty(imports, name) &&
741776
!StringPrototypeIncludes(name, '*') &&
742777
!StringPrototypeEndsWith(name, '/')) {
743-
const resolved = resolvePackageTarget(
778+
const resolveResult = resolvePackageTarget(
744779
packageJSONUrl, imports[name], '', name, base, false, true, conditions
745780
);
746-
if (resolved !== null)
747-
return { resolved, exact: true };
781+
if (resolveResult != null) {
782+
return { resolved: resolveResult.resolved, exact: true };
783+
}
748784
} else {
749785
let bestMatch = '';
750786
let bestMatchSubpath;
@@ -776,14 +812,15 @@ function packageImportsResolve(name, base, conditions) {
776812
if (bestMatch) {
777813
const target = imports[bestMatch];
778814
const pattern = StringPrototypeIncludes(bestMatch, '*');
779-
const resolved = resolvePackageTarget(packageJSONUrl, target,
780-
bestMatchSubpath, bestMatch,
781-
base, pattern, true,
782-
conditions);
783-
if (resolved !== null) {
815+
const resolveResult = resolvePackageTarget(
816+
packageJSONUrl, target,
817+
bestMatchSubpath, bestMatch,
818+
base, pattern, true,
819+
conditions);
820+
if (resolveResult !== null) {
784821
if (!pattern)
785822
emitFolderMapDeprecation(bestMatch, packageJSONUrl, false, base);
786-
return { resolved, exact: pattern };
823+
return { resolved: resolveResult.resolved, exact: pattern };
787824
}
788825
}
789826
}
@@ -843,7 +880,7 @@ function parsePackageName(specifier, base) {
843880
* @param {string} specifier
844881
* @param {string | URL | undefined} base
845882
* @param {Set<string>} conditions
846-
* @returns {URL}
883+
* @returns {resolved: URL, format? : string}
847884
*/
848885
function packageResolve(specifier, base, conditions) {
849886
if (NativeModule.canBeRequiredByUsers(specifier))
@@ -859,8 +896,7 @@ function packageResolve(specifier, base, conditions) {
859896
if (packageConfig.name === packageName &&
860897
packageConfig.exports !== undefined && packageConfig.exports !== null) {
861898
return packageExportsResolve(
862-
packageJSONUrl, packageSubpath, packageConfig, base, conditions
863-
).resolved;
899+
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
864900
}
865901
}
866902

@@ -882,13 +918,26 @@ function packageResolve(specifier, base, conditions) {
882918

883919
// Package match.
884920
const packageConfig = getPackageConfig(packageJSONPath, specifier, base);
885-
if (packageConfig.exports !== undefined && packageConfig.exports !== null)
921+
if (packageConfig.exports !== undefined && packageConfig.exports !== null) {
886922
return packageExportsResolve(
887-
packageJSONUrl, packageSubpath, packageConfig, base, conditions
888-
).resolved;
889-
if (packageSubpath === '.')
890-
return legacyMainResolve(packageJSONUrl, packageConfig, base);
891-
return new URL(packageSubpath, packageJSONUrl);
923+
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
924+
}
925+
926+
if (packageSubpath === '.') {
927+
return {
928+
resolved: legacyMainResolve(
929+
packageJSONUrl,
930+
packageConfig,
931+
base),
932+
...(packageConfig.type !== 'none') && { format: packageConfig.type }
933+
};
934+
}
935+
936+
return {
937+
resolved: new URL(packageSubpath, packageJSONUrl),
938+
...(packageConfig.type !== 'none') && { format: packageConfig.type }
939+
};
940+
892941
// Cross-platform root check.
893942
} while (packageJSONPath.length !== lastPath.length);
894943

@@ -932,6 +981,7 @@ function moduleResolve(specifier, base, conditions, preserveSymlinks) {
932981
// Order swapped from spec for minor perf gain.
933982
// Ok since relative URLs cannot parse as URLs.
934983
let resolved;
984+
let format;
935985
if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) {
936986
resolved = new URL(specifier, base);
937987
} else if (specifier[0] === '#') {
@@ -940,12 +990,15 @@ function moduleResolve(specifier, base, conditions, preserveSymlinks) {
940990
try {
941991
resolved = new URL(specifier);
942992
} catch {
943-
resolved = packageResolve(specifier, base, conditions);
993+
({ resolved, format } = packageResolve(specifier, base, conditions));
944994
}
945995
}
946996
if (resolved.protocol !== 'file:')
947997
return resolved;
948-
return finalizeResolution(resolved, base, preserveSymlinks);
998+
return {
999+
url: finalizeResolution(resolved, base, preserveSymlinks),
1000+
...(format != null) && { format }
1001+
};
9491002
}
9501003

9511004
/**
@@ -1034,9 +1087,15 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) {
10341087

10351088
conditions = getConditionsSet(conditions);
10361089
let url;
1090+
let format;
10371091
try {
1038-
url = moduleResolve(specifier, parentURL, conditions,
1039-
isMain ? preserveSymlinksMain : preserveSymlinks);
1092+
({ url, format } =
1093+
moduleResolve(
1094+
specifier,
1095+
parentURL,
1096+
conditions,
1097+
isMain ? preserveSymlinksMain : preserveSymlinks
1098+
));
10401099
} catch (error) {
10411100
// Try to give the user a hint of what would have been the
10421101
// resolved CommonJS module
@@ -1064,7 +1123,10 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) {
10641123
url.protocol !== 'node:')
10651124
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(url);
10661125

1067-
return { url: `${url}` };
1126+
return {
1127+
url: `${url}`,
1128+
...(format != null) && { format }
1129+
};
10681130
}
10691131

10701132
module.exports = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Flags: --loader ./test/fixtures/es-module-loaders/hook-resolve-type.mjs
2+
import { allowGlobals } from '../common/index.mjs';
3+
import * as fixtures from '../common/fixtures.mjs';
4+
import { strict as assert } from 'assert';
5+
import * as fs from 'fs';
6+
7+
allowGlobals(global.getModuleTypeStats);
8+
9+
const basePath =
10+
new URL('./node_modules/', import.meta.url);
11+
12+
const rel = (file) => new URL(file, basePath);
13+
const createDir = (path) => {
14+
if (!fs.existsSync(path)) {
15+
fs.mkdirSync(path);
16+
}
17+
};
18+
19+
const moduleName = 'module-counter-by-type';
20+
21+
const moduleDir = rel(`${moduleName}`);
22+
createDir(basePath);
23+
createDir(moduleDir);
24+
fs.cpSync(
25+
fixtures.path('es-modules', moduleName),
26+
moduleDir,
27+
{ recursive: true }
28+
);
29+
30+
const { importedESM: importedESMBefore,
31+
importedCJS: importedCJSBefore } = global.getModuleTypeStats();
32+
33+
import(`${moduleName}`).finally(() => {
34+
fs.rmSync(basePath, { recursive: true, force: true });
35+
});
36+
37+
const { importedESM: importedESMAfter,
38+
importedCJS: importedCJSAfter } = global.getModuleTypeStats();
39+
40+
assert.strictEqual(importedESMBefore + 1, importedESMAfter);
41+
assert.strictEqual(importedCJSBefore, importedCJSAfter);

0 commit comments

Comments
 (0)