"use strict";
/*
 * @adonisjs/application
 *
 * (c) Harminder Virk <virk@adonisjs.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Application = void 0;
const path_1 = require("path");
const logger_1 = require("@adonisjs/logger");
const config_1 = require("@adonisjs/config");
const profiler_1 = require("@adonisjs/profiler");
const utils_1 = require("@poppinss/utils");
const fold_1 = require("@adonisjs/fold");
const env_1 = require("@adonisjs/env");
const helpers = __importStar(require("@poppinss/utils/build/helpers"));
const semver_1 = require("semver");
const rcParser_1 = require("./rcParser");
/**
 * Aliases for different environments
 */
const DEV_ENVS = ['dev', 'develop', 'development'];
const PROD_ENVS = ['prod', 'production'];
const TEST_ENVS = ['test', 'testing'];
/**
 * The main application instance to know about the environment, filesystem
 * in which your AdonisJs app is running
 */
class Application {
    /**
     * A boolean to know if application has bootstrapped successfully
     */
    get isReady() {
        return this.state === 'ready' && !this.isShuttingDown;
    }
    constructor(appRoot, environment, rcContents) {
        this.appRoot = appRoot;
        this.environment = environment;
        this.helpers = helpers;
        /**
         * An array of providers with ready and shutdown hooks.
         */
        this.providersWithReadyHook = [];
        this.providersWithShutdownHook = [];
        this.state = 'initiated';
        /**
         * Current working directory for the CLI and not the build directory
         * The `ADONIS_CLI_CWD` is set by the cli
         */
        this.cliCwd = process.env.ADONIS_ACE_CWD;
        /**
         * A boolean to know if app is preparing to shutdown
         */
        this.isShuttingDown = false;
        /**
         * It is unknown until the `setup` method is called
         */
        this.nodeEnvironment = 'unknown';
        /**
         * An array of files to be preloaded
         */
        this.preloads = [];
        /**
         * A map of pre-configured directories
         */
        this.directoriesMap = new Map();
        /**
         * A map of directories aliases
         */
        this.aliasesMap = new Map();
        /**
         * A map of namespaces that different parts of apps
         * can use
         */
        this.namespacesMap = new Map();
        /**
         * Reference to the IoC container for the application
         */
        this.container = new fold_1.Ioc();
        this.rcFile = (0, rcParser_1.parse)(rcContents || this.loadRcFile());
        this.typescript = this.rcFile.typescript;
        /**
         * Loads the package.json files to collect optional
         * info about the app.
         */
        const pkgFile = this.loadAppPackageJson();
        const corePkgFile = this.loadCorePackageJson();
        this.verifyNodeEngine(pkgFile.engines);
        /**
         * Fetching following info from the package file
         */
        this.appName = pkgFile.name;
        this.version = this.parseVersion(pkgFile.version);
        this.adonisVersion = corePkgFile.version ? this.parseVersion(corePkgFile.version) : null;
        /**
         * Fetching following info from the `.adonisrc.json` file.
         */
        this.preloads = this.rcFile.preloads;
        this.exceptionHandlerNamespace = this.rcFile.exceptionHandlerNamespace;
        this.assetsDriver = this.rcFile.assetsDriver;
        this.directoriesMap = new Map(Object.entries(this.rcFile.directories));
        this.aliasesMap = new Map(Object.entries(this.rcFile.aliases));
        this.namespacesMap = new Map(Object.entries(this.rcFile.namespaces));
        this.setEnvVars();
        this.setupGlobals();
        this.registerItselfToTheContainer();
        this.setupHelpers();
    }
    /**
     * Verify the node version when defined under "engines" object in
     * "packages.json" file.
     */
    verifyNodeEngine(engines) {
        const nodeEngine = engines?.node;
        if (!nodeEngine) {
            return;
        }
        if (!(0, semver_1.satisfies)(process.version, nodeEngine)) {
            throw new utils_1.Exception(`The installed Node.js version "${process.version}" does not satisfy the expected version "${nodeEngine}" defined inside package.json file`, 500);
        }
    }
    /**
     * Resolve a given module from the application root. The callback is invoked
     * when the module is missing
     */
    resolveModule(modulePath, onMissingCallback) {
        let filePath;
        try {
            filePath = helpers.resolveFrom(this.appRoot, modulePath);
            return require(filePath);
        }
        catch (error) {
            if (['ENOENT', 'MODULE_NOT_FOUND'].includes(error.code) &&
                (!filePath || filePath === error.path)) {
                return onMissingCallback(error);
            }
            else {
                throw error;
            }
        }
    }
    /**
     * Loads the rc file from the application root
     */
    loadRcFile() {
        return this.resolveModule('./.adonisrc.json', () => {
            throw new Error('AdonisJS expects ".adonisrc.json" file to exist in the application root');
        });
    }
    /**
     * Loads the package.json file from the application root. Swallows
     * the exception when file is missing
     */
    loadAppPackageJson() {
        const pkgFile = this.resolveModule('./package.json', () => {
            return {};
        });
        return {
            name: pkgFile.name || 'adonis-app',
            version: pkgFile.version || '0.0.0',
            engines: pkgFile.engines,
        };
    }
    /**
     * Loads the package.json file for the "@adonisjs/core" package. Swallows
     * the exception when file is missing
     */
    loadCorePackageJson() {
        const pkgFile = this.resolveModule('@adonisjs/core/package.json', () => {
            return {};
        });
        return {
            version: pkgFile.version,
        };
    }
    /**
     * Parses version string to an object.
     */
    parseVersion(version) {
        const parsed = (0, semver_1.parse)(version);
        if (!parsed) {
            return null;
        }
        return {
            major: parsed.major,
            minor: parsed.minor,
            patch: parsed.patch,
            prerelease: parsed.prerelease.map((release) => release),
            version: parsed.version,
            toString() {
                return this.version;
            },
        };
    }
    /**
     * Sets env variables based upon the provided application info.
     */
    setEnvVars() {
        process.env.APP_NAME = this.appName;
        if (this.version) {
            process.env.APP_VERSION = this.version.toString();
        }
        if (this.adonisVersion) {
            process.env.ADONIS_VERSION = this.adonisVersion.toString();
        }
    }
    /**
     * Setup container globals to easily resolve bindings
     */
    setupGlobals() {
        global[Symbol.for('ioc.use')] = this.container.use.bind(this.container);
        global[Symbol.for('ioc.make')] = this.container.make.bind(this.container);
        global[Symbol.for('ioc.call')] = this.container.call.bind(this.container);
    }
    /**
     * Registering itself to the container
     */
    registerItselfToTheContainer() {
        this.container.singleton('Adonis/Core/Application', () => this);
    }
    /**
     * Normalizes node env
     */
    normalizeNodeEnv(env) {
        if (!env || typeof env !== 'string') {
            return 'unknown';
        }
        env = env.toLowerCase();
        if (DEV_ENVS.includes(env)) {
            return 'development';
        }
        if (PROD_ENVS.includes(env)) {
            return 'production';
        }
        if (TEST_ENVS.includes(env)) {
            return 'test';
        }
        return env;
    }
    /**
     * Registering directory aliases
     */
    registerAliases() {
        this.aliasesMap.forEach((toPath, alias) => {
            this.container.alias((0, path_1.join)(this.appRoot, toPath), alias);
        });
    }
    /**
     * Loads the environment variables by reading and parsing the
     * `.env` and `.env.testing` files.
     */
    loadEnvironmentVariables() {
        /**
         * Load `.env` and `.env.testing` files from the application root. The
         * env loader handles the additional flags like
         *
         * ENV_SILENT = 'do not load the .env file'
         * ENV_PATH = 'load .env file from given path'
         * NODE_ENV = 'testing' will trigger optional loading of `.env.testing` file
         */
        const { envContents, testEnvContent } = (0, env_1.envLoader)(this.appRoot);
        /**
         * Create instance of the Env class
         */
        this.env = new env_1.Env([
            { values: new env_1.EnvParser(true).parse(envContents), overwriteExisting: false },
            { values: new env_1.EnvParser(false).parse(testEnvContent), overwriteExisting: true },
        ]);
        this.container.singleton('Adonis/Core/Env', () => this.env);
        /**
         * Attempt to load `env.(ts|js)` files to setup the validation rules
         */
        this.resolveModule('./env', () => { });
        /**
         * Process environment variables. This will trigger validations as well
         */
        this.env.process();
        /**
         * Update node environment
         */
        this.nodeEnvironment = this.normalizeNodeEnv(this.env.get('NODE_ENV'));
    }
    /**
     * Load config and define the container binding
     */
    loadConfig() {
        this.config = new config_1.Config(helpers.requireAll(this.configPath()));
        this.container.singleton('Adonis/Core/Config', () => this.config);
    }
    /**
     * Setup logger
     */
    setupLogger() {
        const config = this.container.resolveBinding('Adonis/Core/Config').get('app.logger', {});
        this.logger = new logger_1.Logger(config);
        this.container.singleton('Adonis/Core/Logger', () => this.logger);
    }
    /**
     * Setup profiler
     */
    setupProfiler() {
        const config = this.container.resolveBinding('Adonis/Core/Config').get('app.profiler', {});
        const logger = this.container.resolveBinding('Adonis/Core/Logger');
        this.profiler = new profiler_1.Profiler(this.appRoot, logger, config);
        this.container.singleton('Adonis/Core/Profiler', () => this.profiler);
    }
    /**
     * Setup helpers
     */
    setupHelpers() {
        this.container.bind('Adonis/Core/Helpers', () => helpers);
    }
    /**
     * Return true when `this.nodeEnvironment === 'production'`
     */
    get inProduction() {
        return this.nodeEnvironment === 'production';
    }
    /**
     * Opposite of [[this.isProduction]]
     */
    get inDev() {
        return !this.inProduction;
    }
    /**
     * Returns true when `this.nodeEnvironment === 'test'`
     */
    get inTest() {
        return this.nodeEnvironment === 'test';
    }
    /**
     * Returns path for a given namespace by replacing the base namespace
     * with the defined directories map inside the rc file.
     *
     * The method returns a relative path from the application root. You can
     * use join it with the [[this.appRoot]] to make the absolute path
     */
    resolveNamespaceDirectory(namespaceFor) {
        /**
         * Return null when rcfile doesn't have a special
         * entry for namespaces
         */
        if (!this.rcFile.namespaces[namespaceFor]) {
            return null;
        }
        let output = null;
        Object.keys(this.rcFile.aliases).forEach((baseNamespace) => {
            const autoloadPath = this.rcFile.aliases[baseNamespace];
            if (this.rcFile.namespaces[namespaceFor].startsWith(`${baseNamespace}/`) ||
                this.rcFile.namespaces[namespaceFor] === baseNamespace) {
                output = this.rcFile.namespaces[namespaceFor].replace(baseNamespace, autoloadPath);
            }
        });
        return output;
    }
    /**
     * Make path to a file or directory relative from
     * the application path
     */
    makePath(...paths) {
        return (0, path_1.join)(this.appRoot, ...paths);
    }
    /**
     * Makes the path to a directory from `cliCwd` vs the `appRoot`. This is
     * helpful when we want path inside the project root and not the
     * build directory
     * @deprecated Use `makePath` instead
     */
    makePathFromCwd(...paths) {
        process.emitWarning('DeprecationWarning', 'application.makePathFromCwd() is deprecated. Use application.makePath() instead');
        return (0, path_1.join)(this.cliCwd || this.appRoot, ...paths);
    }
    /**
     * Make path to a file or directory relative from
     * the config directory
     */
    configPath(...paths) {
        return this.makePath(this.directoriesMap.get('config'), ...paths);
    }
    /**
     * Make path to a file or directory relative from
     * the public path
     */
    publicPath(...paths) {
        return this.makePath(this.directoriesMap.get('public'), ...paths);
    }
    /**
     * Make path to a file or directory relative from
     * the providers path
     */
    providersPath(...paths) {
        return this.makePath(this.directoriesMap.get('providers'), ...paths);
    }
    /**
     * Make path to a file or directory relative from
     * the database path
     */
    databasePath(...paths) {
        return this.makePath(this.directoriesMap.get('database'), ...paths);
    }
    /**
     * Make path to a file or directory relative from
     * the migrations path
     */
    migrationsPath(...paths) {
        return this.makePath(this.directoriesMap.get('migrations'), ...paths);
    }
    /**
     * Make path to a file or directory relative from
     * the seeds path
     */
    seedsPath(...paths) {
        return this.makePath(this.directoriesMap.get('seeds'), ...paths);
    }
    /**
     * Make path to a file or directory relative from
     * the resources path
     */
    resourcesPath(...paths) {
        return this.makePath(this.directoriesMap.get('resources'), ...paths);
    }
    /**
     * Make path to a file or directory relative from
     * the views path
     */
    viewsPath(...paths) {
        return this.makePath(this.directoriesMap.get('views'), ...paths);
    }
    /**
     * Makes path to the start directory
     */
    startPath(...paths) {
        return this.makePath(this.directoriesMap.get('start'), ...paths);
    }
    /**
     * Makes path to the tests directory
     */
    testsPath(...paths) {
        return this.makePath(this.directoriesMap.get('tests'), ...paths);
    }
    /**
     * Makes path to the tmp directory. Since the tmp path is used for
     * writing at the runtime, we use `cwd` path to the write to the
     * source and not the build directory.
     */
    tmpPath(...paths) {
        return this.makePath(this.directoriesMap.get('tmp'), ...paths);
    }
    /**
     * Serialized output
     */
    toJSON() {
        return {
            isReady: this.isReady,
            isShuttingDown: this.isShuttingDown,
            environment: this.environment,
            nodeEnvironment: this.nodeEnvironment,
            appName: this.appName,
            version: this.version ? this.version.toString() : null,
            adonisVersion: this.adonisVersion ? this.adonisVersion.toString() : null,
        };
    }
    /**
     * Switch application environment. Only allowed before the setup
     * is called
     */
    switchEnvironment(environment) {
        if (this.state !== 'initiated') {
            throw new Error(`Cannot switch application environment in "${this.state}" state`);
        }
        this.environment = environment;
        return this;
    }
    /**
     * Performs the initial setup. This is the time, when we configure the
     * app to be able to boot itself. For example:
     *
     * - Loading environment variables
     * - Loading config
     * - Setting up the logger
     * - Registering directory aliases
     *
     * Apart from the providers, most of the app including the container
     * is ready at this stage
     */
    async setup() {
        if (this.state !== 'initiated') {
            return;
        }
        this.state = 'setup';
        this.registerAliases();
        this.loadEnvironmentVariables();
        this.loadConfig();
        this.setupLogger();
        this.setupProfiler();
    }
    /**
     * Register providers
     */
    async registerProviders() {
        if (this.state !== 'setup') {
            return;
        }
        this.state = 'registered';
        await this.profiler.profile('providers:register', {}, async () => {
            const providers = this.rcFile.providers
                .concat(this.rcFile.aceProviders)
                .concat(this.inTest ? this.rcFile.testProviders : []);
            this.logger.trace('registering providers', providers);
            this.registrar = new fold_1.Registrar([this], this.appRoot);
            const registeredProviders = await this.registrar
                .useProviders(providers, (provider) => {
                return new provider(this);
            })
                .register();
            /**
             * Keep reference of providers that are using ready or shutdown hooks
             */
            registeredProviders.forEach((provider) => {
                if (typeof provider.shutdown === 'function') {
                    this.providersWithShutdownHook.push(provider);
                }
                if (typeof provider.ready === 'function') {
                    this.providersWithReadyHook.push(provider);
                }
            });
        });
    }
    /**
     * Booted providers
     */
    async bootProviders() {
        if (this.state !== 'registered') {
            return;
        }
        this.state = 'booted';
        await this.profiler.profileAsync('providers:boot', {}, async () => {
            this.logger.trace('booting providers');
            await this.registrar.boot();
        });
    }
    /**
     * Require files registered as preloads inside `.adonisrc.json` file
     */
    async requirePreloads() {
        this.preloads
            .filter((node) => {
            if (!node.environment || this.environment === 'unknown') {
                return true;
            }
            return node.environment.indexOf(this.environment) > -1;
        })
            .forEach((node) => {
            this.profiler.profile('file:preload', node, () => {
                this.logger.trace(node, 'file:preload');
                this.resolveModule(node.file, (error) => {
                    if (!node.optional) {
                        throw error;
                    }
                });
            });
        });
    }
    /**
     * Start the application. At this time we execute the provider's
     * ready hooks
     */
    async start() {
        if (this.state !== 'booted') {
            return;
        }
        this.state = 'ready';
        await this.profiler.profileAsync('providers:ready', {}, async () => {
            this.logger.trace('executing providers ready hook');
            await Promise.all(this.providersWithReadyHook.map((provider) => provider.ready()));
        });
        this.providersWithReadyHook = [];
    }
    /**
     * Prepare the application for shutdown. At this time we execute the
     * provider's shutdown hooks
     */
    async shutdown() {
        if (['initiated', 'setup'].includes(this.state)) {
            return;
        }
        this.isShuttingDown = true;
        this.state = 'shutdown';
        await this.profiler.profileAsync('providers:shutdown', {}, async () => {
            this.logger.trace('executing providers shutdown hook');
            await Promise.all(this.providersWithShutdownHook.map((provider) => provider.shutdown()));
        });
        this.providersWithShutdownHook = [];
    }
}
exports.Application = Application;
