"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Packager = void 0;
const child_process_1 = require("child_process");
const path_1 = require("path");
const fs_extra_1 = require("fs-extra");
const fs_1 = require("fs");
const archiver_1 = __importDefault(require("archiver"));
const chalk_1 = __importDefault(require("chalk"));
const glob_1 = require("glob");
class Packager {
    constructor(graph, services, logger) {
        this._services = Array.isArray(services) ? services : [services];
        this._tree = new Map();
        this._shaken = new Map();
        this._logger = logger.log('packagr');
        this._graph = graph;
    }
    getTree(serviceName) {
        return this._tree.get(serviceName);
    }
    setTree(serviceName, tree) {
        this._tree.set(serviceName, tree);
    }
    bundle(level = 4) {
        return __awaiter(this, void 0, void 0, function* () {
            yield Promise.all(this._services.map((service) => this.generateZip(service, level)));
        });
    }
    generateZip(service, level) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this._tree.has(service.getName())) {
                yield this.buildDependenciesTree(service);
            }
            if (!this._shaken.has(service.getName())) {
                this.shake(service);
            }
            this._logger.info(`${chalk_1.default.bold(service.getName())}: Dependency tree built and shaken: \n${this.print(service)}`);
            const packageDirectory = path_1.join(service.getLocation(), '.package');
            if (fs_extra_1.existsSync(packageDirectory)) {
                fs_extra_1.removeSync(packageDirectory);
            }
            fs_extra_1.mkdirSync(packageDirectory);
            return new Promise((resolve, reject) => {
                const zipName = 'bundle.zip';
                const output = fs_1.createWriteStream(path_1.join(packageDirectory, zipName));
                const archive = archiver_1.default('zip', {
                    zlib: { level },
                });
                output.on('close', () => {
                    const megabytes = Math.round(100 * (archive.pointer() / 1000000)) / 100;
                    this._logger.info(`${chalk_1.default.bold(service.getName())}: Zip files successfully created (${megabytes}MB)`);
                    return resolve(megabytes);
                });
                archive.on('warning', (err) => {
                    if (err.code === 'ENOENT') {
                        this._logger.warn(err);
                    }
                    else {
                        this._logger.error(err);
                        return reject(err);
                    }
                });
                archive.on('error', (err) => {
                    this._logger.error(err);
                    return reject(err);
                });
                archive.pipe(output);
                const toZip = new Map();
                const resolveDestination = (pkg, src, rootCursor, root) => {
                    const isLernaDescendent = service.getLocation() !== root;
                    if (!isLernaDescendent) {
                        return path_1.relative(service.getLocation(), src);
                    }
                    const relativePath = path_1.relative(root, src);
                    return path_1.join(rootCursor, relativePath);
                };
                const copyDependencies = (packages, cursor, root, rootCursor) => {
                    for (const pkg of packages) {
                        if (!pkg.duplicate) {
                            const newCursor = path_1.join(cursor, 'node_modules', pkg.name);
                            const isLerna = pkg.local;
                            const files = glob_1.sync(path_1.join(pkg.path, '**', '*')).filter((path) => !path_1.relative(pkg.path, path).includes('node_modules'));
                            const newRoot = !isLerna ? root : pkg.path;
                            const newRootCursor = !isLerna ? rootCursor : newCursor;
                            files.forEach((src) => toZip.set(resolveDestination(pkg, src, newRootCursor, newRoot), src));
                            if (pkg.children.length > 0) {
                                copyDependencies(pkg.children, newCursor, newRoot, newRootCursor);
                            }
                        }
                    }
                };
                const roots = this._tree.get(service.getName()).filter((p) => p.parent == null);
                copyDependencies(roots, '', service.getLocation(), '');
                const lib = path_1.join(service.getLocation(), 'lib');
                const compiledSources = glob_1.sync(path_1.join(lib, '**', '*.js'));
                compiledSources.forEach((js) => {
                    toZip.set(path_1.relative(lib, js), js);
                });
                toZip.forEach((from, dest) => {
                    if (fs_extra_1.statSync(from).isDirectory()) {
                        fs_extra_1.chmodSync(from, 0o755);
                    }
                    else if (fs_extra_1.statSync(from).isFile()) {
                        fs_extra_1.chmodSync(from, 0o644);
                        archive.file(from, {
                            name: dest,
                        });
                    }
                });
                archive.finalize();
            });
        });
    }
    static getDependenciesTreeFromNPM(cwd) {
        return __awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve) => {
                const process = child_process_1.spawn('npm', ['ls', '--json', '--long', '--prod'], {
                    cwd,
                });
                const chunks = [];
                process.stdout.on('data', (data) => chunks.push(data));
                process.on('close', () => {
                    resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
                });
            });
        });
    }
    buildDependenciesTree(service) {
        return __awaiter(this, void 0, void 0, function* () {
            const tree = [];
            const internals = yield this._graph.getNodes();
            const rawTree = yield Packager.getDependenciesTreeFromNPM(service.getLocation());
            const buildTree = (deps, parent) => __awaiter(this, void 0, void 0, function* () {
                if (!deps) {
                    return;
                }
                for (const name of Object.keys(deps)) {
                    const dep = deps[name];
                    let pkg;
                    if (dep.missing) {
                        const internal = internals.find((p) => p.getName() === name);
                        if (!internal) {
                            this._logger.error('Missing dependency', name);
                            throw Error(`Missing dependency ${name}`);
                        }
                        pkg = {
                            name: internal.getName(),
                            version: internal.getVersion(),
                            path: internal.getLocation(),
                            children: [],
                            parent,
                            local: true,
                        };
                    }
                    else {
                        pkg = {
                            name: name,
                            version: dep.version || 'missing',
                            path: dep.link || dep.path,
                            children: [],
                            parent,
                            local: dep.link != null,
                        };
                    }
                    tree.push(pkg);
                    if (parent) {
                        parent.children.push(pkg);
                    }
                    if (pkg.local) {
                        const linkedDeps = yield Packager.getDependenciesTreeFromNPM(pkg.path);
                        yield buildTree(linkedDeps.dependencies, pkg);
                    }
                    else {
                        yield buildTree(dep.dependencies, pkg);
                    }
                }
            });
            yield buildTree(rawTree.dependencies);
            this._tree.set(service.getName(), tree);
            return tree;
        });
    }
    shake(service) {
        const areEquals = (p1, p2) => {
            return p1.name === p2.name && p1.version === p2.version;
        };
        const roots = this._tree.get(service.getName()).filter((p) => p.parent == null);
        const leaves = this._tree.get(service.getName()).filter((p) => p.children.length == 0);
        const alreadyAnalyzed = new Set();
        const deduplicate = (deps) => {
            for (const dep of deps) {
                if (!alreadyAnalyzed.has(dep)) {
                    const isDuplicate = (pkg) => {
                        const getSiblings = () => {
                            if (pkg.parent) {
                                return pkg.parent.children;
                            }
                            return roots;
                        };
                        const siblings = getSiblings();
                        if (siblings.some((u) => areEquals(u, dep))) {
                            return true;
                        }
                        return pkg.parent ? isDuplicate(pkg.parent) : false;
                    };
                    dep.duplicate = dep.parent ? isDuplicate(dep.parent) : false;
                    const hasGrandpa = dep.parent && dep.parent.parent;
                    if (hasGrandpa) {
                        deduplicate(dep.parent.parent.children);
                    }
                    alreadyAnalyzed.add(dep);
                }
            }
        };
        deduplicate(leaves);
        const deduplicateChildren = (deps) => {
            for (const dep of deps) {
                if (dep.parent && dep.parent.duplicate) {
                    dep.duplicate = dep.parent.duplicate;
                }
                deduplicateChildren(dep.children);
            }
        };
        deduplicateChildren(roots);
        this._shaken.set(service.getName(), true);
        return this._tree.get(service.getName());
    }
    print(service, printDuplicates = false) {
        if (!this._tree) {
            this._logger.debug(this._tree);
        }
        let printable = '';
        const roots = this._tree.get(service.getName()).filter((p) => p.parent == null);
        const printLevel = (packages, depth = 0) => {
            for (const pkg of packages) {
                if (printDuplicates || !pkg.duplicate) {
                    printable += `${'-'.repeat(depth)}${pkg.name}@${pkg.version} [${pkg.local ? chalk_1.default.blue('local') : chalk_1.default.yellow('remote')}] ${pkg.duplicate ? chalk_1.default.red('dedup') : ''}\n`;
                }
                printLevel(pkg.children, depth + 1);
            }
        };
        printLevel(roots);
        return printable;
    }
}
exports.Packager = Packager;
//# sourceMappingURL=packagr.js.map