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

Package.json exports #1

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
253 changes: 210 additions & 43 deletions install.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ makeInstaller = function (options) {
// might make sense to support the object version, a la browserify.
(options.browser ? ["browser", "main"] : ["main"]);

// List of "conditions" to use for the package.json exports field.
var conditions = options.conditions || ['default'];

var hasOwn = {}.hasOwnProperty;
function strictHasOwn(obj, key) {
return isObject(obj) && isString(key) && hasOwn.call(obj, key);
Expand Down Expand Up @@ -291,17 +294,6 @@ makeInstaller = function (options) {

function fileEvaluate(file, parentModule) {
var module = file.module;
const runSettersAndReturn = () => {
// The module.runModuleSetters method will be deprecated in favor of
// just module.runSetters: https://github.com/benjamn/reify/pull/160
var runSetters = module.runSetters || module.runModuleSetters;
if (isFunction(runSetters)) {
runSetters.call(module);
}

return module.exports;
}

if (! strictHasOwn(module, "exports")) {
var contents = file.contents;
if (! contents) {
Expand All @@ -327,23 +319,6 @@ makeInstaller = function (options) {
}
}

// We don't need to do anything fancy. When importing an async module, we need to wait for it
// to evaluate, and then evaluate the rest like usual.
if (contents.constructor.name === "AsyncFunction") {
return contents(
makeRequire(file),
// If the file had a .stub, reuse the same object for exports.
module.exports = file.stub || {},
module,
file.module.id,
file.parent.module.id
).then(() => {
module.loaded = true;
return runSettersAndReturn();
})

}

contents(
makeRequire(file),
// If the file had a .stub, reuse the same object for exports.
Expand All @@ -356,7 +331,14 @@ makeInstaller = function (options) {
module.loaded = true;
}

return runSettersAndReturn();
// The module.runModuleSetters method will be deprecated in favor of
// just module.runSetters: https://github.com/benjamn/reify/pull/160
var runSetters = module.runSetters || module.runModuleSetters;
if (isFunction(runSetters)) {
runSetters.call(module);
}

return module.exports;
}

function fileIsDirectory(file) {
Expand Down Expand Up @@ -485,9 +467,107 @@ makeInstaller = function (options) {
}
}

function resolvePackageJsonExports(subPath, pkg) {
var exports = pkg.exports;

if (subPath === '.') {
if (typeof exports === 'string') {
return resolveTarget(subPath, exports);
} else if (exports && exports['.']) {
return resolveTarget('.', exports['.']);
} else if (Object.keys(exports).every(function (key) {return !key.startsWith('.')})) {
return resolveTarget('.', exports);
}

return null;
}

if (typeof exports === 'object' && exports !== null) {
if (subPath in exports) {
return resolveTarget(subPath, exports[subPath]);
}

var expansionKeys = Object.keys(exports).filter(function (key) {
return key.includes('*');
}).sort(function (keyA, keyB) {
var baseLengthA = keyA.indexOf('*');
var baseLengthB = keyB.indexOf('*');

if (baseLengthA !== baseLengthB) {
return baseLengthA > baseLengthB ? -1 : 1;
}

if (keyA.length !== keyB.length) {
return keyA.length > keyB.length ? -1 : 1;
}

return 0;
});

var result;
expansionKeys.some(function (expansionKey) {
var patternBase = expansionKey.substring(0, expansionKey.indexOf('*'));
if (subPath !== patternBase && subPath.indexOf(patternBase) === 0) {
var patternTrailer = expansionKey.substring(patternBase.length + 1);

if (
patternTrailer.length === 0 ||
subPath.endsWith(patternTrailer) && subPath.length >= expansionKey.length
) {
var patternMatch = subPath.substring(patternBase.length, subPath.length - patternTrailer.length);
return result = resolveTarget(expansionKey, exports[expansionKey], patternMatch);
}
}
});

return result;
}

return null;

function resolveTarget(key, value, patternMatch) {
if (typeof value === 'string') {
if (value.indexOf('./') !== 0) {
throw new Error('Invalid Package Target');
}
if (patternMatch === undefined) {
return value;
}

var resolved = value.replaceAll('*', patternMatch);
return resolved;
}

if (Array.isArray(value)) {
var result;
value.some(function (targetItem) {
try {
return result = resolveTarget(key, targetItem, patternMatch);
} catch (err) {
if (err.message === 'Invalid Package Target') return result = undefined;
throw err;
}
});
return result;
}

if (typeof value === 'object' && value !== null) {
var result;
Object.keys(value).find(function (prop) {
if (conditions.indexOf(prop) > -1) {
return result = resolveTarget(key, value[prop], patternMatch)
}
});
return result || null;
}
}
}

function fileResolve(file, id, parentModule, seenDirFiles) {
var parentModule = parentModule || file.module;
var extensions = fileGetExtensions(file);
var packageName = extractPackageName(id);
var packageSubpath = '.' + id.substring(packageName.length);

file =
// Absolute module identifiers (i.e. those that begin with a `/`
Expand All @@ -500,7 +580,7 @@ makeInstaller = function (options) {
id.charAt(0) === "." ? fileAppendId(file, id, extensions) :
// Top-level module identifiers are interpreted as referring to
// packages in `node_modules` directories.
nodeModulesLookup(file, id, extensions);
nodeModulesLookup(file, id, extensions, parentModule);

// If the identifier resolves to a directory, we use the same logic as
// Node to find an `index.js` or `package.json` file to evaluate.
Expand All @@ -518,18 +598,48 @@ makeInstaller = function (options) {

var pkgJsonFile = fileAppendIdPart(file, "package.json");
var pkg = pkgJsonFile && fileEvaluate(pkgJsonFile, parentModule);
var mainFile, resolved = pkg && mainFields.some(function (name) {
var main = pkg[name];
if (isString(main)) {
// The "main" field of package.json does not have to begin
// with ./ to be considered relative, so first we try
// simply appending it to the directory path before
// falling back to a full fileResolve, which might return
// a package from a node_modules directory.
return mainFile = fileAppendId(file, main, extensions) ||
fileResolve(file, main, parentModule, seenDirFiles);
var mainFile, resolved;

if (pkg && pkg.exports) {
// We might need to re-check the folder again if there are aliases
seenDirFiles.pop();

var exportPath = resolvePackageJsonExports(packageSubpath, pkg);

if (!exportPath) {
var err = new Error(
'[ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath "' + packageSubpath +
'" is not defined by "exports" in ' + pkgJsonFile.module.id
);
err.code = 'ERR_PACKAGE_PATH_NOT_EXPORTED';
throw err;
}
});

resolved = mainFile = fileAppendId(file, exportPath, extensions) ||
fileResolve(file, exportPath, parentModule, seenDirFiles);
} else if (pkg && (!packageName || packageSubpath === '.')) {
resolved = mainFields.some(function (name) {
var main = pkg[name];
if (isString(main)) {
// The "main" field of package.json does not have to begin
// with ./ to be considered relative, so first we try
// simply appending it to the directory path before
// falling back to a full fileResolve, which might return
// a package from a node_modules directory.
return mainFile = fileAppendId(file, main, extensions) ||
fileResolve(file, main, parentModule, seenDirFiles);
}
});
} else if (packageSubpath.indexOf('./') === 0) {
resolved = mainFile = fileAppendId(file, packageSubpath, extensions);

// We might need to re-check the folder again if there are aliases
// If we resolved to the same file, then we want to exclude this folder
// to avoid an infinite loop
if (resolved !== file) {
seenDirFiles.pop();
}
}

if (resolved && mainFile) {
file = mainFile;
Expand Down Expand Up @@ -561,14 +671,71 @@ makeInstaller = function (options) {
return file;
};

function nodeModulesLookup(file, id, extensions) {
function nodeModulesLookup(file, id, extensions, parentModule) {
var pkgJsonFile = findPackageJson(file, parentModule);
var packageName = extractPackageName(id);

if (pkgJsonFile) {
var pkg = fileEvaluate(pkgJsonFile, parentModule);

if (pkg && pkg.name === packageName && pkg.exports) {
return pkgJsonFile.parent;
}
}

if (packageName.length < id.length) {
// Add a trailing slash to indicate we want a folder, in case there is
// also a file with the same name
packageName = packageName + '/';
}

for (var resolved; file && ! resolved; file = file.parent) {
resolved = fileIsDirectory(file) &&
fileAppendId(file, "node_modules/" + id, extensions);
fileAppendId(file, "node_modules/" + packageName, extensions);

if (resolved && fileIsDirectory(resolved)) {
var pkgJsonFile = fileAppendIdPart(resolved, "package.json");
var pkg = pkgJsonFile && fileEvaluate(pkgJsonFile, parentModule);

if (pkg && pkg.exports || id === packageName) {
break;
}

// commonjs checks if the exact id exists, or continues to the parent
if (!fileAppendId(file, "node_modules/" + id, extensions)) {
resolved = null;
}
}
}
return resolved;
}

function extractPackageName(id) {
if ('./'.indexOf(id.charAt(0)) > -1) {
// Not an id for a package
return '';
} else if (id.indexOf('/') === -1) {
return id;
} else if (id.charAt(0) === '@') {
// everything before second "/"
return id.substring(0, id.indexOf('/', id.indexOf('/') + 1));
}

return id.substring(0, id.indexOf('/'));
}

function findPackageJson(file) {
file = file.parent;
while(file && fileIsDirectory(file) && file.module.id !== 'node_modules') {
var pkgJsonFile = fileAppendIdPart(file, "package.json");
if (pkgJsonFile) {
return pkgJsonFile
}

file = file.parent;
}
}

return install;
};

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"email": "[email protected]"
},
"name": "install",
"version": "0.14.0",
"version": "0.13.0",
"description": "Minimal JavaScript module loader",
"keywords": [
"modules",
Expand Down Expand Up @@ -34,6 +34,7 @@
"reify": "^0.18.1",
"terser": "^3.16.0"
},
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
Expand Down
Loading