"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Extension = void 0;
const ajv_1 = __importDefault(require("ajv"));
const fs_1 = require("fs");
const glob_1 = __importDefault(require("glob"));
const inversify_1 = require("inversify");
const path_1 = __importDefault(require("path"));
const semver_1 = __importDefault(require("semver"));
const tar_1 = __importDefault(require("tar"));
const util_1 = require("util");
const exception_js_1 = require("../exception.js");
const index_js_1 = require("./model/index.js");
const inspection_js_1 = require("./model/inspection.js");
const globAsync = (0, util_1.promisify)(glob_1.default);
const ajv = new ajv_1.default({ messages: true });
class Extension {
    static async load(dir) {
        const spec = await this.loadExtensionSpec(dir);
        return new Extension(dir, spec);
    }
    constructor(dir, spec) {
        this.dir = dir;
        this.spec = spec;
        this.actionClasses = [];
        this.pipeClasses = [];
        this.inspectionClasses = [];
        this._container = null;
    }
    isVersionWithinRange(range) {
        return semver_1.default.satisfies(this.spec.version, range);
    }
    init(engine) {
        this._container = new inversify_1.Container({ skipBaseClassChecks: true });
        this._container.parent = engine.container;
        this.loadModulesSync();
        if (this.spec.entrypoint) {
            const entrypointPath = path_1.default.join(this.dir, this.spec.entrypoint);
            const { init } = require(entrypointPath);
            init(this._container, engine);
        }
    }
    destroy() {
        this.unloadModulesSync();
    }
    isInitialized() {
        return !!this._container;
    }
    get container() {
        if (this._container) {
            return this._container;
        }
        throw new exception_js_1.Exception({
            name: 'ExtensionNotInitialized',
            message: 'Extension needs to be initialized before accessing its container',
            retry: false,
        });
    }
    loadModulesSync() {
        this.actionClasses = [];
        this.pipeClasses = [];
        this.inspectionClasses = [];
        const files = Extension.expandFilePatternsSync(this.dir, this.spec.modules);
        const modules = files.map(f => {
            const fullPath = path_1.default.join(this.dir, f);
            return require(fullPath);
        });
        for (const module of modules) {
            for (const value of Object.values(module)) {
                if (this.isActionClass(value)) {
                    this.actionClasses.push(value);
                }
                if (this.isPipeClass(value)) {
                    this.pipeClasses.push(value);
                }
                if (this.isInspectionClass(value)) {
                    this.inspectionClasses.push(value);
                }
            }
        }
    }
    unloadModulesSync() {
        const modulePaths = Object.keys(require.cache);
        for (const path of modulePaths) {
            if (path.startsWith(this.dir)) {
                delete require.cache[path];
            }
        }
    }
    isActionClass(val) {
        return (typeof val === 'function' &&
            index_js_1.Action.isPrototypeOf(val) &&
            typeof val.$type === 'string' &&
            typeof val.prototype.exec === 'function');
    }
    isPipeClass(val) {
        return (typeof val === 'function' &&
            index_js_1.Pipe.isPrototypeOf(val) &&
            typeof val.$type === 'string' &&
            typeof val.prototype.apply === 'function');
    }
    isInspectionClass(val) {
        return (typeof val === 'function' &&
            inspection_js_1.Inspection.isPrototypeOf(val) &&
            typeof val.prototype.inspect === 'function');
    }
    static async loadExtensionSpec(dir) {
        const pkg = await this.loadPackageJson(dir);
        const valid = Extension.packageJsonValidator(pkg);
        if (!valid) {
            const messages = Extension.packageJsonValidator.errors?.map(_ => _.message ?? '').join('\n') || '';
            throw new exception_js_1.Exception({
                name: 'ExtensionLoadFailed',
                message: `Extension package.json invalid:\n\n${messages}`
            });
        }
        const { name, version, } = pkg;
        const title = pkg.extension?.title ?? pkg.title ?? name;
        const description = pkg.extension?.description ?? pkg.description ?? name;
        const category = pkg.extension?.category ?? 'extension';
        const modules = pkg.extension?.modules ?? pkg.modules ?? [];
        const entrypoint = pkg.extension?.entrypoint;
        const isPrivate = pkg.extension?.private ?? true;
        const tags = pkg.extension?.tags ?? [];
        return {
            name,
            version,
            title,
            description,
            category,
            modules,
            entrypoint,
            private: isPrivate,
            tags,
        };
    }
    static async loadPackageJson(dir) {
        try {
            const packageJsonFile = path_1.default.join(dir, 'package.json');
            const pkg = await fs_1.promises.readFile(packageJsonFile, 'utf-8');
            return JSON.parse(pkg);
        }
        catch (error) {
            const reason = error instanceof SyntaxError ? 'package.json is malformed' :
                error.code === 'ENOENT' ? 'package.json not found' : error.message;
            throw new exception_js_1.Exception({
                name: 'ExtensionLoadFailed',
                message: `Cannot load extension: ${reason}`,
                details: {
                    dir,
                    error,
                }
            });
        }
    }
    static async packExtensionBundle(dir, tarballFile) {
        const pkg = await this.loadPackageJson(dir);
        const files = pkg.files || [];
        const modules = (pkg.extension?.modules ?? pkg.modules ?? []);
        const fileList = [
            ...new Set([
                'package.json',
                ...(await this.expandFilePatterns(dir, files)),
                ...(await this.expandFilePatterns(dir, modules)),
            ])
        ];
        const parentDir = path_1.default.dirname(tarballFile);
        await fs_1.promises.mkdir(parentDir, { recursive: true });
        await tar_1.default.create({
            file: tarballFile,
            cwd: dir,
        }, fileList);
    }
    static async expandFilePatterns(cwd, patterns) {
        const result = [];
        for (const pattern of patterns) {
            const files = await globAsync(pattern, { cwd });
            result.push(...files);
        }
        return result;
    }
    static expandFilePatternsSync(cwd, patterns) {
        const result = [];
        for (const pattern of patterns) {
            const files = glob_1.default.sync(pattern, { cwd });
            result.push(...files);
        }
        return result;
    }
}
exports.Extension = Extension;
Extension.packageJsonSchema = {
    type: 'object',
    required: ['name', 'version'],
    properties: {
        name: { type: 'string', minLength: 1 },
        version: { type: 'string', minLength: 1 },
        title: { type: 'string' },
        description: { type: 'string' },
        extension: {
            type: 'object',
            required: [],
            properties: {
                modules: {
                    type: 'array',
                    items: { type: 'string' },
                },
                entrypoint: { type: 'string', minLength: 1 }
            }
        },
        modules: {
            type: 'array',
            items: { type: 'string' },
        },
    }
};
Extension.packageJsonValidator = ajv.compile(Extension.packageJsonSchema);
//# sourceMappingURL=extension.js.map