"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const cxapi = require("@aws-cdk/cx-api");
const jsBase64 = require("js-base64");
const stack_1 = require("./cloudformation/stack");
const construct_1 = require("./core/construct");
const tokens_1 = require("./core/tokens");
/**
 * Cloud Executable interface version.
 */
const CX_VERSION = 'CloudExecutable/1.0';
/**
 * Represents a CDK program.
 */
class App extends construct_1.Root {
    constructor(argv) {
        super();
        argv = argv || [];
        if (argv.length >= 1) {
            // if the first argument ends with "/node" or "node.exe", skip it (this is argv[0] in node programs).
            if (/[\/\\]node(?:\.exe)?$/.test(argv[0])) {
                argv = argv.slice(1);
            }
            this.progname = argv[0].split('/').pop();
        }
        if (argv.length > 1) {
            try {
                this.request = this.parseRequest(argv[1]);
            }
            catch (e) {
                throw new Error(`Cannot parse request '${argv[1]}': ${e.message}`);
            }
            this.loadContext();
        }
    }
    get stacks() {
        const out = {};
        for (const child of this.children) {
            if (!(child instanceof stack_1.Stack)) {
                throw new Error(`The child ${child.toString()} of Program must be a Stack`);
            }
            out[child.id] = child;
        }
        return out;
    }
    /**
     * Runs the program
     * @returns STDOUT
     */
    run() {
        // no arguments - print usage and exit successfully.
        if (!this.request || !this.request.type) {
            return this.usage;
        }
        const result = this.runCommand();
        return JSON.stringify(result, undefined, 2);
    }
    /**
     * @deprecated Use app.run().
     */
    async exec() {
        return this.run();
    }
    /**
     * Lists all stacks in this app.
     */
    listStacks() {
        return Object.keys(this.stacks).map(name => {
            const stack = this.stacks[name];
            const region = stack.env.region;
            const account = stack.env.account;
            let environment;
            if (account && region) {
                environment = { name: `${account}/${region}`, account, region };
            }
            return { name, environment };
        });
    }
    /**
     * Synthesize and validate a single stack
     * @param stackName The name of the stack to synthesize
     */
    synthesizeStack(stackName) {
        const stack = this.getStack(stackName);
        // first, validate this stack and stop if there are errors.
        const errors = stack.validateTree();
        if (errors.length > 0) {
            const errorList = errors.map(e => `[${e.source.path}] ${e.message}`).join('\n    ');
            throw new Error(`Stack validation failed with the following errors:\n    ${errorList}`);
        }
        let environment;
        if (stack.env.account && stack.env.region) {
            environment = {
                name: `${stack.env.account}/${stack.env.region}`,
                account: stack.env.account,
                region: stack.env.region
            };
        }
        return {
            name: stack.id,
            environment,
            missing: Object.keys(stack.missingContext).length ? stack.missingContext : undefined,
            template: stack.toCloudFormation(),
            metadata: this.collectMetadata(stack)
        };
    }
    /**
     * Synthesizes multiple stacks
     */
    synthesizeStacks(stackNames) {
        const ret = [];
        for (const stackName of stackNames) {
            ret.push(this.synthesizeStack(stackName));
        }
        return ret;
    }
    /**
     * Returns metadata for all constructs in the stack.
     */
    collectMetadata(stack) {
        const output = {};
        visit(stack);
        // add app-level metadata under "."
        if (this.metadata.length > 0) {
            output[construct_1.PATH_SEP] = this.metadata;
        }
        return output;
        function visit(node) {
            if (node.metadata.length > 0) {
                // Make the path absolute
                output[construct_1.PATH_SEP + node.path] = node.metadata.map(md => tokens_1.resolve(md));
            }
            for (const child of node.children) {
                visit(child);
            }
        }
    }
    collectRuntimeInformation() {
        const libraries = {};
        for (const fileName of Object.keys(require.cache)) {
            const pkg = findNpmPackage(fileName);
            if (pkg && !pkg.private) {
                libraries[pkg.name] = pkg.version;
            }
        }
        return { libraries };
    }
    getStack(stackname) {
        if (stackname == null) {
            throw new Error('Stack name must be defined');
        }
        const stack = this.stacks[stackname];
        if (!stack) {
            throw new Error(`Cannot find stack ${stackname}`);
        }
        return stack;
    }
    runCommand() {
        switch (this.request.type) {
            case 'list':
                return {
                    stacks: this.listStacks()
                };
            case 'synth':
                return {
                    stacks: this.synthesizeStacks(this.request.stacks),
                    runtime: this.collectRuntimeInformation()
                };
            default:
                throw new Error(`Invalid command: ${this.request.type}`);
        }
    }
    get usage() {
        const progname = this.progname ? this.progname + ' ' : '';
        return `${CX_VERSION}

Usage:
  ${progname}REQUEST

REQUEST is a JSON-encoded request object.
`;
    }
    loadContext() {
        const context = (this.request && this.request.context) || {};
        for (const key of Object.keys(context)) {
            this.setContext(key, context[key]);
        }
    }
    parseRequest(req) {
        // allow toolkit to send request in base64 if they begin with "base64:"
        // this is in order to avoid shell escaping issues when defining "--app"
        // in the toolkit.
        if (req.startsWith(cxapi.BASE64_REQ_PREFIX)) {
            req = jsBase64.Base64.fromBase64(req.slice(cxapi.BASE64_REQ_PREFIX.length));
        }
        // parse as JSON
        return JSON.parse(req);
    }
}
exports.App = App;
class Program extends App {
    constructor(argv) {
        super(argv);
        this.addWarning('"Program" is deprecated in favor of "App"');
    }
}
exports.Program = Program;
/**
 * Determines which NPM module a given loaded javascript file is from.
 *
 * The only infromation that is available locally is a list of Javascript files,
 * and every source file is associated with a search path to resolve the further
 * ``require`` calls made from there, which includes its own directory on disk,
 * and parent directories - for example:
 *
 * [ '...repo/packages/aws-cdk-resources/lib/cfn/node_modules',
 *   '...repo/packages/aws-cdk-resources/lib/node_modules',
 *   '...repo/packages/aws-cdk-resources/node_modules',
 *   '...repo/packages/node_modules',
 *   // etc...
 * ]
 *
 * We are looking for ``package.json`` that is anywhere in the tree, except it's
 * in the parent directory, not in the ``node_modules`` directory. For this
 * reason, we strip the ``/node_modules`` suffix off each path and use regular
 * module resolution to obtain a reference to ``package.json``.
 *
 * @param fileName a javascript file name.
 * @returns the NPM module infos (aka ``package.json`` contents), or
 *          ``undefined`` if the lookup was unsuccessful.
 */
function findNpmPackage(fileName) {
    const mod = require.cache[fileName];
    const paths = mod.paths.map(stripNodeModules);
    try {
        const path = require.resolve('package.json', { paths });
        return require(path);
    }
    catch (e) {
        return undefined;
    }
    /**
     * @param s a path.
     * @returns ``s`` with any terminating ``/node_modules``
     *          (or ``\\node_modules``) stripped off.)
     */
    function stripNodeModules(s) {
        if (s.endsWith('/node_modules') || s.endsWith('\\node_modules')) {
            // /node_modules is 13 characters
            return s.substr(0, s.length - 13);
        }
        return s;
    }
}
//# sourceMappingURL=data:application/json;base64, 