diff --git a/lib/module-loader.js b/lib/module-loader.js index e28f1254473d53c5f0b21c390f19ced16dc1d27b..4907936a8252e02f59145bd3297512d93302d4d8 100644 --- a/lib/module-loader.js +++ b/lib/module-loader.js @@ -127,7 +127,21 @@ class ModuleLoader { return config.type !== undefined; }; try { - filePath = require.resolve(modulePath); + try { + // We wrap it with try/catch and fallback to custom path if it fails to make it compatible with Hive. + // Due to some weird behavior in tsup/esbuild, it fails to resolve the path to the module. + filePath = require.resolve(modulePath); + } catch (error) { + const customPath = process.env.OCLIF_CLI_CUSTOM_PATH; + if (typeof customPath !== "string") { + throw error; + } + modulePath = modulePath.replace('/src/', '/dist/').replace('\\src\\', '\\dist\\'); + filePath = require.resolve( + path.resolve(customPath, modulePath) + ".js" + ); + } + isESM = ModuleLoader.isPathModule(filePath); } catch { diff --git a/lib/module-loader.modified.js b/lib/module-loader.modified.js new file mode 100644 index 0000000000000000000000000000000000000000..bd3bf6cda2c0537590ee13860bbe36fbfb3c24d3 --- /dev/null +++ b/lib/module-loader.modified.js @@ -0,0 +1,208 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const path = require("path"); +const url = require("url"); +const fs = require("fs-extra"); +const errors_1 = require("./errors"); +const Config = require("./config"); +const getPackageType = require("get-package-type"); +/** + * Defines file extension resolution when source files do not have an extension. + */ +// eslint-disable-next-line camelcase +const s_EXTENSIONS = [".ts", ".js", ".mjs", ".cjs"]; +/** + * Provides a mechanism to use dynamic import / import() with tsconfig -> module: commonJS as otherwise import() gets + * transpiled to require(). + */ +const _importDynamic = new Function("modulePath", "return import(modulePath)"); // eslint-disable-line no-new-func +/** + * Provides a static class with several utility methods to work with Oclif config / plugin to load ESM or CJS Node + * modules and source files. + * + * @author Michael Leahy (https://github.com/typhonrt) + */ +// eslint-disable-next-line unicorn/no-static-only-class +class ModuleLoader { + /** + * Loads and returns a module. + * + * Uses `getPackageType` to determine if `type` is set to 'module. If so loads '.js' files as ESM otherwise uses + * a bare require to load as CJS. Also loads '.mjs' files as ESM. + * + * Uses dynamic import to load ESM source or require for CommonJS. + * + * A unique error, ModuleLoadError, combines both CJS and ESM loader module not found errors into a single error that + * provides a consistent stack trace and info. + * + * @param {IConfig|IPlugin} config - Oclif config or plugin config. + * @param {string} modulePath - NPM module name or file path to load. + * + * @returns {Promise<*>} The entire ESM module from dynamic import or CJS module by require. + */ + static async load(config, modulePath) { + let filePath; + let isESM; + try { + ({ isESM, filePath } = ModuleLoader.resolvePath(config, modulePath)); + // It is important to await on _importDynamic to catch the error code. + return isESM + ? await _importDynamic(url.pathToFileURL(filePath)) + : require(filePath); + } catch (error) { + if ( + error.code === "MODULE_NOT_FOUND" || + error.code === "ERR_MODULE_NOT_FOUND" + ) { + throw new errors_1.ModuleLoadError( + `${isESM ? "import()" : "require"} failed to load ${ + filePath || modulePath + }` + ); + } + throw error; + } + } + /** + * Loads a module and returns an object with the module and data about the module. + * + * Uses `getPackageType` to determine if `type` is set to `module`. If so loads '.js' files as ESM otherwise uses + * a bare require to load as CJS. Also loads '.mjs' files as ESM. + * + * Uses dynamic import to load ESM source or require for CommonJS. + * + * A unique error, ModuleLoadError, combines both CJS and ESM loader module not found errors into a single error that + * provides a consistent stack trace and info. + * + * @param {IConfig|IPlugin} config - Oclif config or plugin config. + * @param {string} modulePath - NPM module name or file path to load. + * + * @returns {Promise<{isESM: boolean, module: *, filePath: string}>} An object with the loaded module & data including + * file path and whether the module is ESM. + */ + static async loadWithData(config, modulePath) { + let filePath; + let isESM; + try { + ({ isESM, filePath } = ModuleLoader.resolvePath(config, modulePath)); + const module = isESM + ? await _importDynamic(url.pathToFileURL(filePath)) + : require(filePath); + return { isESM, module, filePath }; + } catch (error) { + if ( + error.code === "MODULE_NOT_FOUND" || + error.code === "ERR_MODULE_NOT_FOUND" + ) { + throw new errors_1.ModuleLoadError( + `${isESM ? "import()" : "require"} failed to load ${ + filePath || modulePath + }: ${error.message}` + ); + } + throw error; + } + } + /** + * For `.js` files uses `getPackageType` to determine if `type` is set to `module` in associated `package.json`. If + * the `modulePath` provided ends in `.mjs` it is assumed to be ESM. + * + * @param {string} filePath - File path to test. + * + * @returns {boolean} The modulePath is an ES Module. + * @see https://www.npmjs.com/package/get-package-type + */ + static isPathModule(filePath) { + const extension = path.extname(filePath).toLowerCase(); + switch (extension) { + case ".js": + return getPackageType.sync(filePath) === "module"; + case ".ts": + return getPackageType.sync(filePath) === "module"; + case ".mjs": + return true; + default: + return false; + } + } + /** + * Resolves a modulePath first by `require.resolve` to allow Node to resolve an actual module. If this fails then + * the `modulePath` is resolved from the root of the provided config. `Config.tsPath` is used for initial resolution. + * If this file path does not exist then several extensions are tried from `s_EXTENSIONS` in order: '.js', '.mjs', + * '.cjs'. After a file path has been selected `isPathModule` is used to determine if the file is an ES Module. + * + * @param {IConfig|IPlugin} config - Oclif config or plugin config. + * @param {string} modulePath - File path to load. + * + * @returns {{isESM: boolean, filePath: string}} An object including file path and whether the module is ESM. + */ + static resolvePath(config, modulePath) { + let isESM; + let filePath; + const isPlugin = (config) => { + return config.type !== undefined; + }; + try { + try { + // We wrap it with try/catch and fallback to custom path if it fails to make it compatible with Hive. + // Due to some weird behavior in tsup/esbuild, it fails to resolve the path to the module. + filePath = require.resolve(modulePath); + } catch (error) { + const customPath = process.env.OCLIF_CLI_CUSTOM_PATH; + if (typeof customPath !== "string") { + throw error; + } + filePath = require.resolve( + path.resolve(customPath, modulePath) + ".js" + ); + } + isESM = ModuleLoader.isPathModule(filePath); + } catch { + filePath = isPlugin(config) + ? Config.tsPath(config.root, modulePath, config.type) + : Config.tsPath(config.root, modulePath); + let fileExists = false; + let isDirectory = false; + if (fs.existsSync(filePath)) { + fileExists = true; + try { + if (fs.lstatSync(filePath)?.isDirectory?.()) { + fileExists = false; + isDirectory = true; + } + } catch {} + } + if (!fileExists) { + // Try all supported extensions. + let foundPath = ModuleLoader.findFile(filePath); + if (!foundPath && isDirectory) { + // Since filePath is a directory, try looking for index file. + foundPath = ModuleLoader.findFile(path.join(filePath, "index")); + } + if (foundPath) { + filePath = foundPath; + } + } + isESM = ModuleLoader.isPathModule(filePath); + } + return { isESM, filePath }; + } + /** + * Try adding the different extensions from `s_EXTENSIONS` to find the file. + * + * @param {string} filePath - File path to load. + * + * @returns {string | null} Modified file path including extension or null if file is not found. + */ + static findFile(filePath) { + // eslint-disable-next-line camelcase + for (const extension of s_EXTENSIONS) { + const testPath = `${filePath}${extension}`; + if (fs.existsSync(testPath)) { + return testPath; + } + } + return null; + } +} +exports.default = ModuleLoader;