"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("path");
const url = require("url");
const fs_1 = require("fs");
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 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 <support@typhonjs.io> (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 import to catch the error code.
            return isESM ? await import(url.pathToFileURL(filePath).href) : 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 import(url.pathToFileURL(filePath).href) : 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':
            case '.jsx':
            case '.ts':
            case '.tsx':
                return getPackageType.sync(filePath) === 'module';
            case '.mjs':
            case '.mts':
                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 {
            filePath = require.resolve(modulePath);
            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 ((0, fs_1.existsSync)(filePath)) {
                fileExists = true;
                try {
                    if ((0, fs_1.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 ((0, fs_1.existsSync)(testPath)) {
                return testPath;
            }
        }
        return null;
    }
}
exports.default = ModuleLoader;
