optional-require
Version:
NodeJS Require that let you handle module not found error without try/catch
203 lines • 7.8 kB
JavaScript
import assert from "assert";
import requireAt from "require-at";
import { URL } from "node:url";
/* eslint-disable max-params, complexity */
let appPath = process.cwd();
export function setAppPathAtTopNodeModules(appPath) {
const nmIdx = appPath.indexOf("/node_modules/");
if (nmIdx !== -1) {
appPath = appPath.slice(0, nmIdx);
}
return appPath;
}
export function setAppPath(path) {
appPath = path;
}
// Copied from https://github.com/yarnpkg/berry/blob/d5454007c9c76becfa97b36a92de299a3694afd5/packages/yarnpkg-pnp/sources/loader/makeApi.ts#L27
// Splits a require request into its components, or return null if the request is a file path
const pnpDependencyNameRegExp = /^(?![a-zA-Z]:[\\/]|\\\\|\.{0,2}(?:\/|$))((?:node:)?(?:@[^/]+\/)?[^/]+)\/*(.*|)$/;
/**
* Change a module name request into a Yarn Berry PnP dependency name,
* since the dependency name is what will be included in the error message.
* For example, `optionalRequire('my-package/package.json')` will print a message like
* `Your application tried to access my-package,` without the `/package.json` at the end of it.
* This function grabs the dependency name only, or returns `null` if it can't find it.
* @param {string} name Requested name
* @returns {string} Dependency name
*/
function getPnpDependencyName(name) {
const dependencyNameMatch = name.match(pnpDependencyNameRegExp);
if (!dependencyNameMatch)
return null;
return dependencyNameMatch[1];
}
/**
* Check if an error from require is really due to the module not found,
* and not because the module itself trying to require another module
* that's not found.
*
* @param err - the error
* @param name - name of the module being required
* @returns true or false
*/
function findModuleNotFound(err, name) {
// Check the first line of the error message
const msg = err.message.split("\n")[0];
/* c8 ignore start */
if (!msg) {
return false;
}
/* c8 ignore stop */
// Check for "Cannot find module 'foo'"
if (msg.includes(`'${name}'`)) {
return true;
}
const pnpDependencyName = getPnpDependencyName(name);
if (pnpDependencyName) {
return (
// Check for "Your application tried to access foo (a peer dependency) ..." (Yarn Berry PnP)
// https://github.com/yarnpkg/berry/blob/e81dc0d29bb2f41818d9c5c1c74bab1406fb979b/packages/yarnpkg-pnp/sources/loader/makeApi.ts#L680
msg.includes(` ${pnpDependencyName} `) ||
// Check for "Your application tried to access foo. While ..." (Yarn Berry PnP)
// https://github.com/yarnpkg/berry/blob/e81dc0d29bb2f41818d9c5c1c74bab1406fb979b/packages/yarnpkg-pnp/sources/loader/makeApi.ts#L704
msg.includes(` ${pnpDependencyName}. `) ||
// Check for "Your application tried to access foo, but ..." (Yarn Berry PnP)
// https://github.com/yarnpkg/berry/blob/e81dc0d29bb2f41818d9c5c1c74bab1406fb979b/packages/yarnpkg-pnp/sources/loader/makeApi.ts#L718
msg.includes(` ${pnpDependencyName}, `));
}
return false;
}
/**
* Default log function
*
* @param message - message to log
* @param path - path of the module to require
*/
function defaultLog(message, path) {
console.log(`Just FYI: ${message}; Path "${path}"`);
}
let __defaultLog = defaultLog;
export function setDefaultLog(log) {
__defaultLog = log;
}
/**
* Get the require function from the caller's context
* @param callerRequire - the require function from the caller's context
* @returns the require function
*/
export function _getRequire(callerRequire, fallbackPath) {
if (!callerRequire) {
return requireAt(fallbackPath || process.cwd());
}
else if (typeof callerRequire === "string") {
try {
return requireAt(new URL(callerRequire).pathname);
}
catch {
return requireAt(callerRequire);
}
}
else {
return callerRequire;
}
}
function _getOptions(optsOrMsg, requireFunction, log) {
if (typeof optsOrMsg === "object") {
const opts = { require: requireFunction, log, ...optsOrMsg };
assert(!(opts.hasOwnProperty("notFound") && opts.hasOwnProperty("default")), "optionalRequire: options set with both `notFound` and `default`");
return opts;
}
else {
return { require: requireFunction, log, message: optsOrMsg };
}
}
/**
* Internal optional require implementation
*
* @param path - path to module to require
* @param optsOrMsg - options or message to log in case module not found
* @returns require or resolve result
*/
function _optionalRequire(path, opts) {
try {
return opts.resolve ? opts.require.resolve(path) : opts.require(path);
}
catch (e) {
// exception that's not due to the module itself not found
if (e.code !== "MODULE_NOT_FOUND" || !findModuleNotFound(e, path)) {
// if the module we are requiring fail because it try to require a
// module that's not found, then we have to report this as failed.
if (typeof opts.fail === "function") {
return opts.fail(e);
}
throw e;
}
if (opts.message) {
const message = opts.message === true ? "" : `${opts.message} - `;
const r = opts.resolve ? "resolved" : "found";
opts.log(`${message}optional module not ${r}`, path);
}
if (typeof opts.notFound === "function") {
return opts.notFound(e);
}
return opts.default;
}
}
/**
* try to require a module with optional handling in case it's not found or fail to require
*
* @param callerRequire - `require` from caller context
* @param path - path to module to require
* @param optsOrMsg - options or message to log in case of exceptions
* @returns require result
*/
export function tryRequire(callerRequire, path, optsOrMsg) {
const opts = _getOptions(optsOrMsg, _getRequire(callerRequire), __defaultLog);
opts.resolve = false;
return _optionalRequire(path, opts);
}
/**
* try to resolve a module with optional handling in case it's not found or fail to require
*
* @param callerRequire - `require` from caller context
* @param path - path to module to require
* @param optsOrMsg - options or message to log in case of exceptions
* @returns resolve result
*/
export function tryResolve(callerRequire, path, optsOrMsg) {
const opts = _getOptions(optsOrMsg, _getRequire(callerRequire), __defaultLog);
opts.resolve = true;
return _optionalRequire(path, opts);
}
/**
* Make an optional require function, using the `require` from caller's context.
*
* @param callerRequire - `require` from caller's context
* @param log - function to log if module is not found
* @returns required module
*/
export function makeOptionalRequire(callerRequire, log) {
let _require = _getRequire(callerRequire);
const x = (path, optsOrMsg) => {
const opts = _getOptions(optsOrMsg, _require, x.log);
return _optionalRequire(path, opts);
};
x.resolve = (path, optsOrMsg) => {
const opts = _getOptions(optsOrMsg, _require, x.log);
opts.resolve = true;
return _optionalRequire(path, opts);
};
x.log = log || __defaultLog;
return x;
}
/**
* An optionalRequire function using `require` or `import` from CWD context
*
* @remarks because `require` is from CWD context, if you do `optionalRequireCwd("./my-module")`
* then it will look for `my-module` in CWD.
*
* @remarks You can still override the `require` using `options.require` with the one from your
* calling context.
*/
export const optionalRequireCwd = makeOptionalRequire(process.cwd());
//# sourceMappingURL=optional-require.js.map