"use strict";
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;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppRunner = void 0;
/* eslint-disable unicorn/no-process-exit */
const fs_1 = require("fs");
const componentsjs_1 = require("componentsjs");
const fs_extra_1 = require("fs-extra");
const yargs_1 = __importDefault(require("yargs"));
const LogLevel_1 = require("../logging/LogLevel");
const LogUtil_1 = require("../logging/LogUtil");
const ErrorUtil_1 = require("../util/errors/ErrorUtil");
const InternalServerError_1 = require("../util/errors/InternalServerError");
const PathUtil_1 = require("../util/PathUtil");
const SingleThreaded_1 = require("./cluster/SingleThreaded");
const DEFAULT_CONFIG = (0, PathUtil_1.resolveModulePath)('config/default.json');
const DEFAULT_CLI_RESOLVER = 'urn:solid-server-app-setup:default:CliResolver';
const DEFAULT_APP = 'urn:solid-server:default:App';
const CORE_CLI_PARAMETERS = {
    config: { type: 'array', alias: 'c', default: [DEFAULT_CONFIG], requiresArg: true },
    loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true, choices: LogLevel_1.LOG_LEVELS },
    mainModulePath: { type: 'string', alias: 'm', requiresArg: true },
};
const ENV_VAR_PREFIX = 'CSS';
/**
 * A class that can be used to instantiate and start a server based on a Component.js configuration.
 */
class AppRunner {
    constructor() {
        this.logger = (0, LogUtil_1.getLoggerFor)(this);
    }
    /**
     * Starts the server with a given config.
     * This method can be used to start the server from within another JavaScript application.
     *
     * Keys of the `variableBindings` object should be Components.js variables.
     * E.g.: `{ 'urn:solid-server:default:variable:rootFilePath': '.data' }`.
     *
     * `shorthand` are CLI argument names and their corresponding values.
     * E.g.: `{ rootFilePath: '.data' }`.
     * Abbreviated parameter names can not be used, so `{ f: '.data' }` would not work.
     *
     * The values in `variableBindings` take priority over those in `shorthand`.
     *
     * @param loaderProperties - Components.js loader properties.
     * @param configFile - Path to the server config file(s).
     * @param variableBindings - Bindings of Components.js variables.
     * @param shorthand - Shorthand values that need to be resolved.
     */
    async run(loaderProperties, configFile, variableBindings, shorthand) {
        const app = await this.create(loaderProperties, configFile, variableBindings, shorthand);
        await app.start();
    }
    /**
     * Returns an App object, created with the given config, that can start and stop the Solid server.
     *
     * Keys of the `variableBindings` object should be Components.js variables.
     * E.g.: `{ 'urn:solid-server:default:variable:rootFilePath': '.data' }`.
     *
     * `shorthand` are CLI argument names and their corresponding values.
     * E.g.: `{ rootFilePath: '.data' }`.
     * Abbreviated parameter names can not be used, so `{ f: '.data' }` would not work.
     *
     * The values in `variableBindings` take priority over those in `shorthand`.
     *
     * @param loaderProperties - Components.js loader properties.
     * @param configFile - Path to the server config file(s).
     * @param variableBindings - Bindings of Components.js variables.
     * @param shorthand - Shorthand values that need to be resolved.
     */
    async create(loaderProperties, configFile, variableBindings, shorthand) {
        const componentsManager = await this.createComponentsManager(loaderProperties, configFile);
        const cliResolver = await this.createCliResolver(componentsManager);
        const parsedVariables = await this.resolveShorthand(cliResolver.shorthandResolver, { ...shorthand });
        // Create the application using the translated variable values.
        // `variableBindings` override those resolved from the `shorthand` input.
        return this.createApp(componentsManager, { ...parsedVariables, ...variableBindings });
    }
    /**
     * Starts the server as a command-line application.
     * Will exit the process on failure.
     *
     * Made non-async to lower the risk of unhandled promise rejections.
     * This is only relevant when this is used to start as a Node.js application on its own,
     * if you use this as part of your code you probably want to use the async version.
     *
     * @param argv - Command line arguments.
     * @param stderr - Stream that should be used to output errors before the logger is enabled.
     */
    runCliSync({ argv, stderr = process.stderr }) {
        this.runCli(argv).catch((error) => {
            stderr.write((0, ErrorUtil_1.createErrorMessage)(error));
            process.exit(1);
        });
    }
    /**
     * Starts the server as a command-line application.
     * @param argv - Command line arguments.
     */
    async runCli(argv) {
        const app = await this.createCli(argv);
        try {
            await app.start();
        }
        catch (error) {
            this.logger.error(`Could not start the server: ${(0, ErrorUtil_1.createErrorMessage)(error)}`);
            this.resolveError('Could not start the server', error);
        }
    }
    /**
     * Returns an App object, created by parsing the Command line arguments, that can start and stop the Solid server.
     * Will exit the process on failure.
     *
     * @param argv - Command line arguments.
     */
    async createCli(argv = process.argv) {
        // Parse only the core CLI arguments needed to load the configuration
        let yargv = (0, yargs_1.default)(argv.slice(2))
            .usage('node ./bin/server.js [args]')
            .options(CORE_CLI_PARAMETERS)
            // We disable help here as it would only show the core parameters
            .help(false)
            // We also read from environment variables
            .env(ENV_VAR_PREFIX);
        const settings = await this.getPackageSettings();
        if (typeof settings !== 'undefined') {
            yargv = yargv.default(settings);
        }
        const params = await yargv.parse();
        const loaderProperties = {
            mainModulePath: (0, PathUtil_1.resolveAssetPath)(params.mainModulePath),
            dumpErrorState: true,
            logLevel: params.loggingLevel,
            typeChecking: false,
        };
        const configs = params.config.map(PathUtil_1.resolveAssetPath);
        // Create the Components.js manager used to build components from the provided config
        let componentsManager;
        try {
            componentsManager = await this.createComponentsManager(loaderProperties, configs);
        }
        catch (error) {
            // Print help of the expected core CLI parameters
            const help = await yargv.getHelp();
            this.resolveError(`${help}\n\nCould not build the config files from ${configs}`, error);
        }
        // Build the CLI components and use them to generate values for the Components.js variables
        const variables = await this.cliToVariables(componentsManager, argv, settings);
        // Build and start the actual server application using the generated variable values
        return await this.createApp(componentsManager, variables);
    }
    /**
     * Retrieves settings from package.json or configuration file when
     * part of an npm project.
     * @returns The settings defined in the configuration file
     */
    async getPackageSettings() {
        // Only try and retrieve config file settings if there is a package.json in the
        // scope of the current directory
        const packageJsonPath = (0, PathUtil_1.joinFilePath)(process.cwd(), 'package.json');
        if (!(0, fs_1.existsSync)(packageJsonPath)) {
            return;
        }
        // First see if there is a dedicated .json configuration file
        const cssConfigPath = (0, PathUtil_1.joinFilePath)(process.cwd(), '.community-solid-server.config.json');
        if ((0, fs_1.existsSync)(cssConfigPath)) {
            return (0, fs_extra_1.readJSON)(cssConfigPath);
        }
        // Next see if there is a dedicated .js file
        const cssConfigPathJs = (0, PathUtil_1.joinFilePath)(process.cwd(), '.community-solid-server.config.js');
        if ((0, fs_1.existsSync)(cssConfigPathJs)) {
            return Promise.resolve(`${cssConfigPathJs}`).then(s => __importStar(require(s)));
        }
        // Finally try and read from the config.community-solid-server
        // field in the root package.json
        const pkg = await (0, fs_extra_1.readJSON)(packageJsonPath);
        if (typeof pkg.config?.['community-solid-server'] === 'object') {
            return pkg.config['community-solid-server'];
        }
    }
    /**
     * Creates the Components Manager that will be used for instantiating.
     */
    async createComponentsManager(loaderProperties, configFile) {
        const componentsManager = await componentsjs_1.ComponentsManager.build(loaderProperties);
        for (const config of Array.isArray(configFile) ? configFile : [configFile]) {
            await componentsManager.configRegistry.register(config);
        }
        return componentsManager;
    }
    /**
     * Handles the first Components.js instantiation.
     * Uses it to extract the CLI shorthand values and use those to create variable bindings.
     */
    async cliToVariables(componentsManager, argv, settings) {
        const cliResolver = await this.createCliResolver(componentsManager);
        const shorthand = await this.extractShorthand(cliResolver.cliExtractor, argv);
        return await this.resolveShorthand(cliResolver.shorthandResolver, { ...settings, ...shorthand });
    }
    /**
     * Instantiates the {@link CliResolver}.
     */
    async createCliResolver(componentsManager) {
        try {
            // Create a CliResolver, which combines a CliExtractor and a VariableResolver
            return await componentsManager.instantiate(DEFAULT_CLI_RESOLVER, {});
        }
        catch (error) {
            this.resolveError(`Could not create the CLI resolver`, error);
        }
    }
    /**
     * Uses the {@link CliExtractor} to convert the CLI args to a {@link Shorthand} object.
     */
    async extractShorthand(cliExtractor, argv) {
        try {
            // Convert CLI args to CLI bindings
            return await cliExtractor.handleSafe(argv);
        }
        catch (error) {
            this.resolveError(`Could not parse the CLI parameters`, error);
        }
    }
    /**
     * Uses the {@link ShorthandResolver} to convert {@link Shorthand} to {@link VariableBindings} .
     */
    async resolveShorthand(shorthandResolver, shorthand) {
        try {
            // Convert CLI bindings into variable bindings
            return await shorthandResolver.handleSafe(shorthand);
        }
        catch (error) {
            this.resolveError(`Could not resolve the shorthand values`, error);
        }
    }
    /**
     * The second Components.js instantiation,
     * where the App is created and started using the variable mappings.
     */
    async createApp(componentsManager, variables) {
        let app;
        // Create the app
        try {
            app = await componentsManager.instantiate(DEFAULT_APP, { variables });
        }
        catch (error) {
            this.resolveError(`Could not create the server`, error);
        }
        // Ensure thread safety
        if (!app.clusterManager.isSingleThreaded()) {
            const violatingClasses = await (0, SingleThreaded_1.listSingleThreadedComponents)(componentsManager);
            if (violatingClasses.length > 0) {
                const verb = violatingClasses.length > 1 ? 'are' : 'is';
                const detailedError = new InternalServerError_1.InternalServerError(`[${violatingClasses.join(', ')}] ${verb} not threadsafe and should not be run in multithreaded setups!`);
                this.resolveError('Cannot run a singlethreaded-only component in a multithreaded setup!', detailedError);
            }
        }
        return app;
    }
    /**
     * Throws a new error that provides additional information through the extra message.
     * Also appends the stack trace to the message.
     * This is needed for errors that are thrown before the logger is created as we can't log those the standard way.
     */
    resolveError(message, error) {
        let errorMessage = `${message}\nCause: ${(0, ErrorUtil_1.createErrorMessage)(error)}\n`;
        if ((0, ErrorUtil_1.isError)(error)) {
            errorMessage += `${error.stack}\n`;
        }
        throw new Error(errorMessage);
    }
}
exports.AppRunner = AppRunner;
//# sourceMappingURL=AppRunner.js.map