Skip to content

Commit

Permalink
loader: return package format from defaultResolve if known
Browse files Browse the repository at this point in the history
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 <bradley.meck@gmail.com>
Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
  • Loading branch information
dygabo committed Jan 29, 2022
1 parent ab28dc5 commit c75d953
Show file tree
Hide file tree
Showing 7 changed files with 368 additions and 46 deletions.
154 changes: 108 additions & 46 deletions lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,23 @@ const patternRegEx = /\*/g;

function resolvePackageTargetString(
target, subpath, match, packageJSONUrl, base, pattern, internal, conditions) {

const composeResult = (resolved) => {
let format;
try {
format = getPackageType(resolved);
} catch (err) {
if (err.code === 'ERR_INVALID_FILE_URL_PATH') {
const invalidModuleErr = new ERR_INVALID_MODULE_SPECIFIER(
resolved, 'must not include encoded "/" or "\\" characters', base);
invalidModuleErr.cause = err;
throw invalidModuleErr;
}
throw err;
}
return { resolved, ...(format !== 'none') && { format } };
};

if (subpath !== '' && !pattern && target[target.length - 1] !== '/')
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);

Expand All @@ -488,7 +505,8 @@ function resolvePackageTargetString(
const exportTarget = pattern ?
RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) :
target + subpath;
return packageResolve(exportTarget, packageJSONUrl, conditions);
return packageResolve(
exportTarget, packageJSONUrl, conditions);
}
}
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
Expand All @@ -504,15 +522,18 @@ function resolvePackageTargetString(
if (!StringPrototypeStartsWith(resolvedPath, packagePath))
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);

if (subpath === '') return resolved;
if (subpath === '') return composeResult(resolved);

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

if (pattern)
return new URL(RegExpPrototypeSymbolReplace(patternRegEx, resolved.href,
() => subpath));
return new URL(subpath, resolved);
if (pattern) {
return composeResult(new URL(RegExpPrototypeSymbolReplace(patternRegEx,
resolved.href,
() => subpath)));
}

return composeResult(new URL(subpath, resolved));
}

/**
Expand All @@ -538,9 +559,9 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
let lastException;
for (let i = 0; i < target.length; i++) {
const targetItem = target[i];
let resolved;
let resolveResult;
try {
resolved = resolvePackageTarget(
resolveResult = resolvePackageTarget(
packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern,
internal, conditions);
} catch (e) {
Expand All @@ -549,13 +570,13 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
continue;
throw e;
}
if (resolved === undefined)
if (resolveResult === undefined)
continue;
if (resolved === null) {
if (resolveResult === null) {
lastException = null;
continue;
}
return resolved;
return resolveResult;
}
if (lastException === undefined || lastException === null)
return lastException;
Expand All @@ -574,12 +595,12 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
const key = keys[i];
if (key === 'default' || conditions.has(key)) {
const conditionalTarget = target[key];
const resolved = resolvePackageTarget(
const resolveResult = resolvePackageTarget(
packageJSONUrl, conditionalTarget, subpath, packageSubpath, base,
pattern, internal, conditions);
if (resolved === undefined)
if (resolveResult === undefined)
continue;
return resolved;
return resolveResult;
}
}
return undefined;
Expand Down Expand Up @@ -638,12 +659,15 @@ function packageExportsResolve(
!StringPrototypeIncludes(packageSubpath, '*') &&
!StringPrototypeEndsWith(packageSubpath, '/')) {
const target = exports[packageSubpath];
const resolved = resolvePackageTarget(
const resolveResult = resolvePackageTarget(
packageJSONUrl, target, '', packageSubpath, base, false, false, conditions
);
if (resolved === null || resolved === undefined)

if (resolveResult == null) {
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
return { resolved, exact: true };
}

return { ...resolveResult, exact: true };
}

let bestMatch = '';
Expand Down Expand Up @@ -679,14 +703,25 @@ function packageExportsResolve(
if (bestMatch) {
const target = exports[bestMatch];
const pattern = StringPrototypeIncludes(bestMatch, '*');
const resolved = resolvePackageTarget(packageJSONUrl, target,
bestMatchSubpath, bestMatch, base,
pattern, false, conditions);
if (resolved === null || resolved === undefined)
const resolveResult = resolvePackageTarget(
packageJSONUrl,
target,
bestMatchSubpath,
bestMatch,
base,
pattern,
false,
conditions);

if (resolveResult == null) {
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
if (!pattern)
}

if (!pattern) {
emitFolderMapDeprecation(bestMatch, packageJSONUrl, true, base);
return { resolved, exact: pattern };
}

return { ...resolveResult, exact: pattern };
}

throwExportsNotFound(packageSubpath, packageJSONUrl, base);
Expand Down Expand Up @@ -726,11 +761,12 @@ function packageImportsResolve(name, base, conditions) {
if (ObjectPrototypeHasOwnProperty(imports, name) &&
!StringPrototypeIncludes(name, '*') &&
!StringPrototypeEndsWith(name, '/')) {
const resolved = resolvePackageTarget(
const resolveResult = resolvePackageTarget(
packageJSONUrl, imports[name], '', name, base, false, true, conditions
);
if (resolved !== null)
return { resolved, exact: true };
if (resolveResult != null) {
return { resolved: resolveResult.resolved, exact: true };
}
} else {
let bestMatch = '';
let bestMatchSubpath;
Expand Down Expand Up @@ -762,14 +798,15 @@ function packageImportsResolve(name, base, conditions) {
if (bestMatch) {
const target = imports[bestMatch];
const pattern = StringPrototypeIncludes(bestMatch, '*');
const resolved = resolvePackageTarget(packageJSONUrl, target,
bestMatchSubpath, bestMatch,
base, pattern, true,
conditions);
if (resolved !== null) {
const resolveResult = resolvePackageTarget(
packageJSONUrl, target,
bestMatchSubpath, bestMatch,
base, pattern, true,
conditions);
if (resolveResult !== null) {
if (!pattern)
emitFolderMapDeprecation(bestMatch, packageJSONUrl, false, base);
return { resolved, exact: pattern };
return { resolved: resolveResult.resolved, exact: pattern };
}
}
}
Expand Down Expand Up @@ -833,7 +870,7 @@ function parsePackageName(specifier, base) {
* @param {string} specifier
* @param {string | URL | undefined} base
* @param {Set<string>} conditions
* @returns {URL}
* @returns {resolved: URL, format? : string}
*/
function packageResolve(specifier, base, conditions) {
const { packageName, packageSubpath, isScoped } =
Expand All @@ -846,8 +883,7 @@ function packageResolve(specifier, base, conditions) {
if (packageConfig.name === packageName &&
packageConfig.exports !== undefined && packageConfig.exports !== null) {
return packageExportsResolve(
packageJSONUrl, packageSubpath, packageConfig, base, conditions
).resolved;
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
}
}

Expand All @@ -869,13 +905,26 @@ function packageResolve(specifier, base, conditions) {

// Package match.
const packageConfig = getPackageConfig(packageJSONPath, specifier, base);
if (packageConfig.exports !== undefined && packageConfig.exports !== null)
if (packageConfig.exports !== undefined && packageConfig.exports !== null) {
return packageExportsResolve(
packageJSONUrl, packageSubpath, packageConfig, base, conditions
).resolved;
if (packageSubpath === '.')
return legacyMainResolve(packageJSONUrl, packageConfig, base);
return new URL(packageSubpath, packageJSONUrl);
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
}

if (packageSubpath === '.') {
return {
resolved: legacyMainResolve(
packageJSONUrl,
packageConfig,
base),
...(packageConfig.type !== 'none') && { format: packageConfig.type }
};
}

return {
resolved: new URL(packageSubpath, packageJSONUrl),
...(packageConfig.type !== 'none') && { format: packageConfig.type }
};

// Cross-platform root check.
} while (packageJSONPath.length !== lastPath.length);

Expand Down Expand Up @@ -912,12 +961,13 @@ function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) {
* @param {string} specifier
* @param {string | URL | undefined} base
* @param {Set<string>} conditions
* @returns {URL}
* @returns {url: URL, format?: string}
*/
function moduleResolve(specifier, base, conditions) {
// Order swapped from spec for minor perf gain.
// Ok since relative URLs cannot parse as URLs.
let resolved;
let format;
if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) {
resolved = new URL(specifier, base);
} else if (specifier[0] === '#') {
Expand All @@ -926,10 +976,13 @@ function moduleResolve(specifier, base, conditions) {
try {
resolved = new URL(specifier);
} catch {
resolved = packageResolve(specifier, base, conditions);
({ resolved, format } = packageResolve(specifier, base, conditions));
}
}
return finalizeResolution(resolved, base);
return {
url: finalizeResolution(resolved, base),
...(format != null) && { format }
};
}

/**
Expand Down Expand Up @@ -1040,8 +1093,14 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) {

conditions = getConditionsSet(conditions);
let url;
let format;
try {
url = moduleResolve(specifier, parentURL, conditions);
({ url, format } =
moduleResolve(
specifier,
parentURL,
conditions
));
} catch (error) {
// Try to give the user a hint of what would have been the
// resolved CommonJS module
Expand Down Expand Up @@ -1077,7 +1136,10 @@ function defaultResolve(specifier, context = {}, defaultResolveUnused) {
url.hash = old.hash;
}

return { url: `${url}` };
return {
url: `${url}`,
...(format != null) && { format }
};
}

module.exports = {
Expand Down
41 changes: 41 additions & 0 deletions test/es-module/test-esm-loader-resolve-type.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Flags: --loader ./test/fixtures/es-module-loaders/hook-resolve-type.mjs
import { allowGlobals } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { strict as assert } from 'assert';
import * as fs from 'fs';

allowGlobals(global.getModuleTypeStats);

const basePath =
new URL('./node_modules/', import.meta.url);

const rel = (file) => new URL(file, basePath);
const createDir = (path) => {
if (!fs.existsSync(path)) {
fs.mkdirSync(path);
}
};

const moduleName = 'module-counter-by-type';

const moduleDir = rel(`${moduleName}`);
createDir(basePath);
createDir(moduleDir);
fs.cpSync(
fixtures.path('es-modules', moduleName),
moduleDir,
{ recursive: true }
);

const { importedESM: importedESMBefore,
importedCJS: importedCJSBefore } = global.getModuleTypeStats();

import(`${moduleName}`).finally(() => {
fs.rmSync(basePath, { recursive: true, force: true });
});

const { importedESM: importedESMAfter,
importedCJS: importedCJSAfter } = global.getModuleTypeStats();

assert.strictEqual(importedESMBefore + 1, importedESMAfter);
assert.strictEqual(importedCJSBefore, importedCJSAfter);
Loading

0 comments on commit c75d953

Please sign in to comment.