webstrate-package-browser-component/webstrate-package-browser.js

/**
 *  Package Browser
 *  Visual browser for installing WPM packages into a Webstrate
 *
 *  Copyright 2021 Janus B. Kristensen, CAVI,
 *  Center for Advanced Visualization and Interacion, Aarhus University
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0

 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
**/

/**
 * Visual browser for installing WPM packages into a Webstrate
 */
window.WPMPackageBrowser = class WPMPackageBrowser {
    constructor(autoOpen = true) {
        let self = this;
        self.topLevelComponent = document.documentElement;

        self.itemTemplate = WebstrateComponents.Tools.loadTemplate("#packageBrowserPackageItem");
        self.html = WebstrateComponents.Tools.loadTemplate("#packageBrowserBase");
        self.mainView = self.html.querySelector("#packageBrowserMain");

        wpm.require(["material-design-components", "material-design-icons", "ModalDialog", "MenuSystem", "MaterialMenu", "MaterialDesignOutlinedIcons"]).then(() => {
            mdc.autoInit(self.html);
            let tabs = self.html.querySelector(".mdc-tab-bar").MDCTabBar;

            tabs.listen("MDCTabBar:activated", (evt) => {
                // Switch tabs
                switch (evt.detail.index) {
                    case 0:
                        self.showRepositories();
                        break;
                    default:
                        self.showSystem();
                }
            });

            try {
                tabs.activateTab(0); //self.showRepositories();
            } catch (ex) {
                tabs.activateTab(1); //self.showSystem();
            }

            self.repositoryURLDialog = this.getURLDialog();
            self.repositoryDevURLDialog = this.getDevURLDialog();
        });

        if (autoOpen) this.openInBody();
    }

    openInBody() {
        let parent = document.createElement("transient");
        parent.appendChild(this.html);
        document.body.appendChild(parent);
    }

    setTopLevelComponent(component) {
        this.topLevelComponent = component;
    }

    getURLDialog() {
        let self = this;
        let repoAddTemplate = WebstrateComponents.Tools.loadTemplate("#packageBrowserRepositoryURL");
        let repositoryURLDialog = new WebstrateComponents.ModalDialog(
            repoAddTemplate,
            {
                "title": "Repository URL",
                "actions": {
                    "cancel": {},
                    "set": { primary: true, mdcIcon: "add_link" }
                }
            }
        );
        this.topLevelComponent.appendChild(repositoryURLDialog.html);

        repositoryURLDialog.setTarget = (target) => {
            repositoryURLDialog.target = target;

            let stepRepositoryRegistrations = WPMv2.getRegisteredRepositories(false);
            if (stepRepositoryRegistrations[target]) {
                repoAddTemplate.querySelector("#repourl").value = stepRepositoryRegistrations[target];
            }
        };

        EventSystem.registerEventCallback('ModalDialog.Closing', function (evt) {
            if (evt.detail.dialog === repositoryURLDialog && evt.detail.action === "set") {
                let bootConfig = self.getBootConfig();

                if (bootConfig.require && Array.isArray(bootConfig.require)) {
                    let repo = {};
                    repo[repositoryURLDialog.target] = repoAddTemplate.querySelector("#repourl").value;
                    if (bootConfig.require.length === 0) {
                        bootConfig.require = {
                            repositories: repo,
                            dependencies: []
                        };
                    } else {
                        let oldRepos = bootConfig.require[0].repositories;
                        if (!oldRepos) {
                            bootConfig.require[0].repositories = repo;
                        } else {
                            bootConfig.require[0].repositories = { ...bootConfig.require[0].repositories, ...repo };
                        }
                    }

                    // Effectuate immediately too
                    WPMv2.registerRepository(repositoryURLDialog.target, repo[repositoryURLDialog.target]);
                }

                self.setBootConfig(bootConfig);
                self.showRepositories();
            }
        });

        return repositoryURLDialog;
    }

    getDevURLDialog() {
        let self = this;
        let repoDevTemplate = WebstrateComponents.Tools.loadTemplate("#packageBrowserRepositoryDevURL");
        let repositoryDevURLDialog = new WebstrateComponents.ModalDialog(
            repoDevTemplate,
            {
                "title": "Repository Dev Override",
                "actions": {
                    "cancel": {},
                    "remove": {},
                    "set": { primary: true, mdcIcon: "add_link" }
                }
            }
        );
        this.topLevelComponent.appendChild(repositoryDevURLDialog.html);

        repositoryDevURLDialog.setTarget = (target) => {
            repositoryDevURLDialog.target = target;

            let stepRepositoryRegistrations = WPMv2.getRegisteredRepositories(true);
            if (stepRepositoryRegistrations[target]) {
                repoDevTemplate.querySelector("#repodevurl").value = stepRepositoryRegistrations[target];
            }
        };

        EventSystem.registerEventCallback('ModalDialog.Closing', function (evt) {
            if (evt.detail.dialog === repositoryDevURLDialog) {
                let value = repoDevTemplate.querySelector("#repodevurl").value;
                if (evt.detail.action === "remove" || (evt.detail.action === "set" && value.trim().length === 0)) {
                    WPMv2.unregisterRepository(repositoryDevURLDialog.target, true);
                } else if (evt.detail.action === "set") {
                    WPMv2.registerRepository(repositoryDevURLDialog.target, value, true);
                }
            }
            self.showRepositories();
        });

        return repositoryDevURLDialog;
    }


    async renderRepository(repositoryName, repositoryView) {
        let self = this;
        let repositoryPackages = await WPMv2.getPackagesFromRepository(repositoryName);
        let bootConfig = this.getBootConfig();

        let repositoryRepositoryTemplate = WebstrateComponents.Tools.loadTemplate("#packageBrowserRepositoryItem_repository");
        let repositoryBodyTemplate = WebstrateComponents.Tools.loadTemplate("#packageBrowserRepositoryItem_body");
        let repoListDiv = repositoryView.querySelector(".repository-list");
        repoListDiv.innerHTML = "";
        repoListDiv.appendChild(repositoryRepositoryTemplate);
        repoListDiv.appendChild(repositoryBodyTemplate);

        let renderBody = () => {
            let installedFromRepository = WPMv2.getCurrentlyInstalledPackages().filter(pkg => pkg.repository && pkg.repository == repositoryName);
            // TODO: Handle packages that are in the bootConfig requirestep for this repo but aren't in the repo
            let repositoryIsThisPage = WPMv2.getLocalRepositoryURL && (repositoryName == WPMv2.getLocalRepositoryURL());

            for (let packageInfo of repositoryPackages) {
                if (packageInfo.repository && packageInfo.repository != repositoryName) continue; // This package is not actually from here but somewhere else
                repositoryBodyTemplate.appendChild(self.renderPackageItem(packageInfo, bootConfig, { repositoryIsThisPage: repositoryIsThisPage }));
                installedFromRepository = installedFromRepository.filter(pkg => pkg.name != packageInfo.name);
            }

            // Render packages that claim to be in this repository, but aren't
            installedFromRepository.forEach((packageInfo) => {
                repositoryBodyTemplate.appendChild(self.renderPackageItem(packageInfo, bootConfig, { missingInRepository: true }));
            });
        };
        await renderBody();

        // Repository-wide override buttons
        repositoryRepositoryTemplate.querySelector(".repository-require input").checked = self.isConfigDirectlyRequiringRepository(bootConfig, repositoryName);
        repositoryRepositoryTemplate.querySelector(".repository-require input").addEventListener("click", async (evt) => {
            // Enabled repository-wide require option, remove any per-package settings from bootConfig
            for (let packageInfo of repositoryPackages) {
                await self.removePackageRequire(packageInfo);
            }

            if (evt.target.checked) {
                await self.addRepositoryRequire(repositoryName);
            } else {
                await self.removeRepositoryRequire(repositoryName);
            }

            bootConfig = self.getBootConfig();
            repositoryBodyTemplate.innerHTML = "";
            await renderBody();
        });

        repositoryRepositoryTemplate.querySelector(".repository-embed").addEventListener("click", async (evt) => {
            let embedMenu = MenuSystem.MenuManager.createMenu("PackageBrowser.RepositoryEmbed", {
                growDirection: MenuSystem.Menu.GrowDirection.DOWN
            });
            embedMenu.addItem({
                label: "Embed In-Use",
                order: 100,
                group: "Fetching",
                groupOrder: 200,
                icon: IconRegistry.createIcon("mdc:downloading"),
                onAction: async () => {
                    let packagesToEmbed = [];
                    for (let packageInfo of repositoryPackages) {
                        let inUse = self.getLocalPackageElement(packageInfo.name);
                        if (inUse && (self.isTransientPackageElement(inUse))) {
                            packagesToEmbed.push(packageInfo);
                        }
                    }
                    console.log(packagesToEmbed);
                    await self.embedPackages(packagesToEmbed);
                    self.renderRepository(repositoryName, repositoryView);
                }
            });
            embedMenu.addItem({
                label: "Embed All",
                icon: IconRegistry.createIcon("mdc:download_for_offline"),
                group: "Fetching",
                groupOrder: 200,
                order: 200,
                onAction: async () => {
                    if (confirm("This will bloat the page, the repository may not be designed to have ALL packages embedded and newly added packages will not be included")) {
                        let packagesToEmbed = []
                        for (let packageInfo of repositoryPackages) {
                            let localPackageElement = self.getLocalPackageElement(packageInfo.name);

                            if ((!localPackageElement) || self.isTransientPackageElement(localPackageElement)) {
                                packagesToEmbed.push(packageInfo);
                            }
                        }
                        await self.embedPackages(packagesToEmbed);
                        self.renderRepository(repositoryName, repositoryView);
                    }
                }
            });
            embedMenu.addItem({
                label: "Remove All Embedded",
                icon: IconRegistry.createIcon("mdc:delete"),
                group: "Destruction",
                groupOrder: 999,
                order: 999,
                onAction: async () => {
                    let packagesOnlyOnThisPage = 0;
                    let packagesToUnembed = [];
                    for (let packageInfo of repositoryPackages) {
                        let localPackageElement = self.getLocalPackageElement(packageInfo.name);
                        if (localPackageElement && !self.isTransientPackageElement(localPackageElement)) {
                            if (packageInfo.repository && WPMv2.getLocalRepositoryURL && packageInfo.repository == WPMv2.getLocalRepositoryURL()) {
                                packagesOnlyOnThisPage++;
                            }
                            packagesToUnembed.push(packageInfo);
                        }
                    }

                    if ((!packagesOnlyOnThisPage) || confirm("This will permanently delete " + packagesOnlyOnThisPage + " packages that ONLY exist in this page"))
                        for (let pkg of packagesToUnembed) {
                            await self.unembedPackage(pkg);
                        }

                    self.renderRepository(repositoryName, repositoryView);
                }
            });

            embedMenu.registerOnCloseCallback(() => {
                if (embedMenu.html.parentNode !== null) {
                    embedMenu.html.parentNode.removeChild(embedMenu.html);
                }
            });

            self.html.appendChild(embedMenu.html);

            embedMenu.open({
                x: evt.clientX,
                y: evt.clientY
            });
            evt.stopPropagation();
            evt.preventDefault();
        });
    };

    showRepositories() {
        let self = this;
        this.mainView.innerHTML = "";

        let repositoryView = WebstrateComponents.Tools.loadTemplate("#packageBrowserRepositoryList");

        // General actions for repositories
        let repoAddTemplate = WebstrateComponents.Tools.loadTemplate("#packageBrowserRepositoryAdder");
        let addRepositoryDialog = new WebstrateComponents.ModalDialog(
            repoAddTemplate,
            {
                "title": "Add Repository",
                "actions": {
                    "cancel": {},
                    "next": { primary: true, mdcIcon: "add_link" }
                }
            }
        );
        self.topLevelComponent.appendChild(addRepositoryDialog.html);
        EventSystem.registerEventCallback('ModalDialog.Closing', function (evt) {
            if (evt.detail.dialog === addRepositoryDialog && evt.detail.action === "next") {
                let bootConfig = self.getBootConfig();
                let value = repoAddTemplate.querySelector("#repoid").value.trim();
                if (value.length > 0) {
                    if ((!bootConfig.knownRepositories) || !Array.isArray(bootConfig.knownRepositories)) {
                        bootConfig.knownRepositories = []; // Destructive conformity
                    }
                    bootConfig.knownRepositories.push(repoAddTemplate.querySelector("#repoid").value);
                    self.setBootConfig(bootConfig);
                    self.showRepositories();

                    // TODO: .scrollIntoView() ?
                }
            }
        });

        // Gather a list of used repositories, both site-wide from WPM and previously added from this browser
        let knownRepositories = [];
        let installedPackages = WPMv2.getCurrentlyInstalledPackages();
        for (let packageInfo of installedPackages) {
            if (packageInfo.repository && !knownRepositories.includes(packageInfo.repository)) {
                knownRepositories.push(packageInfo.repository);
            }
        }
        let bootConfig = this.getBootConfig();
        if (bootConfig.knownRepositories && Array.isArray(bootConfig.knownRepositories)) {
            for (let repoName of bootConfig.knownRepositories) {
                if (!knownRepositories.includes(repoName)) {
                    knownRepositories.push(repoName);
                }
            }
        }

        // Sort it and show it
        let sortedRepositories = knownRepositories.sort();
        let overviewAddItemTemplate = WebstrateComponents.Tools.loadTemplate("#packageBrowserAddListItem");
        for (let repositoryName of sortedRepositories) {
            let overviewListItemTemplate = WebstrateComponents.Tools.loadTemplate("#packageBrowserRepositoryListItem");
            if (WPMv2.getLocalRepositoryURL && repositoryName == WPMv2.getLocalRepositoryURL()) {
                overviewListItemTemplate.querySelector(".repository-name").innerHTML = "⌂ This Page";
                overviewListItemTemplate.querySelector(".repository-url").innerText = repositoryName;
            } else {
                overviewListItemTemplate.querySelector(".repository-name").title = repositoryName;
                overviewListItemTemplate.querySelector(".repository-name").innerText = repositoryName.replace("-repos", "").replaceAll("_", " ").replaceAll("-", " ");
            }

            // Check if this is mapped somewhere with the bootloader
            // STUB: Currently this uses WPMv2 instead of the bootstep to resolve it
            let stepRepositoryRegistrations = WPMv2.getRegisteredRepositories(false);
            if (stepRepositoryRegistrations[repositoryName]) {
                overviewListItemTemplate.querySelector(".repository-url").innerText = stepRepositoryRegistrations[repositoryName];
                overviewListItemTemplate.querySelector(".repository-url").title = stepRepositoryRegistrations[repositoryName];
            }

            // Check if this is mapped somewhere with any overrides
            // STUB: Currently this uses WPMv2 instead of the bootstep to resolve it
            let overrideRepositoryRegistrations = WPMv2.getRegisteredRepositories(true);
            if (overrideRepositoryRegistrations[repositoryName]) {
                overviewListItemTemplate.querySelector(".repository-url").title = overviewListItemTemplate.querySelector(".repository-url").innerText + " overridden by site-wide developer setting in this browser";
                overviewListItemTemplate.querySelector(".repository-url").innerText = overrideRepositoryRegistrations[repositoryName];
                overviewListItemTemplate.querySelector(".repository-url").style.color = "red";
            }

            overviewListItemTemplate.querySelector(".repository-more").addEventListener("click", (evt) => {
                let moreMenu = MenuSystem.MenuManager.createMenu("PackageBrowser.RepositoryMore", {
                    growDirection: MenuSystem.Menu.GrowDirection.DOWN
                });
                moreMenu.addItem({
                    label: "Remove",
                    order: 10,
                    icon: IconRegistry.createIcon("mdc:delete_outline"),
                    onAction: () => {
                        let bootConfig = self.getBootConfig();
                        if ((!bootConfig.knownRepositories) || !Array.isArray(bootConfig.knownRepositories)) {
                            console.log("Couldn't understand bootConfig.knownRepositories", bootConfig.knownRepositories);
                            return;
                        } else {
                            bootConfig.knownRepositories = bootConfig.knownRepositories.filter(e => e !== repositoryName);
                            self.setBootConfig(bootConfig);
                            self.showRepositories();
                        }
                    }
                });
                moreMenu.addItem({
                    label: "Source URL...",
                    icon: IconRegistry.createIcon("mdc:link"),
                    order: 900,
                    onAction: () => {
                        self.repositoryURLDialog.setTarget(repositoryName);
                        self.repositoryURLDialog.open();
                    }
                });
                moreMenu.addItem({
                    label: "Dev Override...",
                    icon: IconRegistry.createIcon("mdc:assistant_direction"),
                    order: 999,
                    onAction: () => {
                        self.repositoryDevURLDialog.setTarget(repositoryName);
                        self.repositoryDevURLDialog.open();
                    }
                });

                moreMenu.registerOnCloseCallback(() => {
                    if (moreMenu.html.parentNode !== null) {
                        moreMenu.html.parentNode.removeChild(moreMenu.html);
                    }
                });

                self.html.appendChild(moreMenu.html);
                moreMenu.open({
                    x: evt.clientX,
                    y: evt.clientY
                });
                evt.stopPropagation();
                evt.preventDefault();
            });

            // Insert into overview of repositories
            repositoryView.querySelector(".repository-overview").appendChild(overviewListItemTemplate);
        }
        repositoryView.querySelector(".repository-overview").appendChild(overviewAddItemTemplate);
        repositoryView.querySelector(".add-repository").addEventListener("click", () => {
            addRepositoryDialog.html.querySelector("#repoid").value = "";
            addRepositoryDialog.open();
        });

        mdc.autoInit(repositoryView);
        let list = repositoryView.querySelector(".repository-overview").MDCList;
        list.singleSelection = true;
        list.listen("MDCList:action", async (evt) => {
            if (evt.detail.index > sortedRepositories.length) return;
            let repositoryName = sortedRepositories[evt.detail.index];
            self.renderRepository(repositoryName, repositoryView);
        });
        list.selectedIndex = 0;
        if (sortedRepositories.length > 0) self.renderRepository(sortedRepositories[0], repositoryView);

        this.mainView.appendChild(repositoryView);
    }

    showSystem() {
        let self = this;
        this.mainView.innerHTML = "";

        let systemView = WebstrateComponents.Tools.loadTemplate("#packageBrowserSystem");
        this.mainView.appendChild(systemView);
        let wpmInfo = systemView.querySelector("#wpm-info");
        let bootLoaderInfo = systemView.querySelector("#bootloader-info");
        let bootLoaderConfig = systemView.querySelector("#bootloader-config");

        // Package Manager
        if (WPMv2) {
            wpmInfo.querySelector(".name").innerText = "WPMv2";
            if (WPMv2.version) {
                wpmInfo.querySelector(".version").innerText = WPMv2.version;
                wpmInfo.querySelector(".wpm-version-warning").remove();
            }
            if (WPMv2.revision) {
                wpmInfo.querySelector(".revision").innerText = WPMv2.revision;
            }
        }

        // Config
        const bootconfigElement = document.querySelector("script[type='text/json+bootconfig']")
        const editor = bootLoaderConfig.querySelector(".editor");
        const saveButton = bootLoaderConfig.querySelector(".bootloader-save");
        const resetButton = bootLoaderConfig.querySelector(".bootloader-reset");

        const oldConfig = bootconfigElement.innerText;
        editor.value = oldConfig;

        saveButton.addEventListener("click", () => {
            bootconfigElement.innerHTML = editor.value;
        });

        resetButton.addEventListener("click", () => {
            editor.value = oldConfig;
        });
    }

    getBootConfig() {
        try {
            let element = document.querySelector("script[type='text/json+bootconfig']");
            if (!element) throw Error("No bootconfig defined");

            let j = JSON.parse(element.textContent);
            return j;
        } catch (ex) {
            console.log("PackageBrowser error", ex);
        }

        // We assume that we may create a new empty boot config if it doesn't exist
        let bootConfig = {
            creator: "WPMPackageManager",
            created: Date.now(),
            require: []
        };

        return bootConfig;
    }

    setBootConfig(config) {
        let element = document.querySelector("script[type='text/json+bootconfig']");
        if (!element) {
            element = document.createElement("script");
            element.setAttribute("type", "text/json+bootconfig");
            WPMv2.stripProtection(element);
            document.head.appendChild(element);
            console.warn("Installing new boot config");
        }

        config["updated"] = Date.now();
        element.textContent = JSON.stringify(config, null, 4);
    }

    isLive(packageElement) {
        return packageElement.getAttribute("transient-wpm-live") !== null;
    }

    isConfigDirectlyRequiringPackage(config, name) {
        if (!config) throw new Error("Boot config is undefined");
        if (!config.require) throw new Error("Boot config does not contain require");
        if (!Array.isArray(config.require)) throw new Error("Boot config require is not an array");

        for (let requireStep of config.require) {
            if (!requireStep.dependencies) continue;
            if (!Array.isArray(requireStep.dependencies)) continue;
            for (let dependency of requireStep.dependencies) {
                if (dependency.package && dependency.package === name) {
                    return true;
                }
            }
        }
        return false;
    }

    isConfigDirectlyRequiringRepository(config, name) {
        if (!config) throw new Error("Boot config is undefined");
        if (!config.require) throw new Error("Boot config does not contain require");
        if (!Array.isArray(config.require)) throw new Error("Boot config require is not an array");

        for (let requireStep of config.require) {
            if (!requireStep.dependencies) continue;
            if (!Array.isArray(requireStep.dependencies)) continue;
            for (let dependency of requireStep.dependencies) {
                if (dependency.repository && dependency.repository === name && !dependency.package) {
                    return true;
                }
            }
        }
        return false;
    }



    getLocalPackageElement(packageName) {
        return document.querySelector(".packages .package#" + packageName + ", wpm-package#" + packageName);
    }

    isTransientPackageElement(packageElement) {
        if (typeof webstrate !== "undefined") {
            // In webstrate mode webstrates defines what is transient
            if (webstrate.config.isTransientElement(packageElement)) return true;
        } else {
            // Otherwise we assume a transient-element tag is used to mark it
            console.log("Trying to match ", packageElement.tagName);
            if (packageElement.closest("[transient-wpmid]")) {
                return true;
            } else {
                return false;
            }
        }

        let parent = packageElement.parentElement;
        if (parent) {
            return this.isTransientPackageElement(parent);
        } else {
            return false;
        }
    }

    isForcedEmbeddedPackage(packageInfo) {
        if (packageInfo.descriptor) {
            if (packageInfo.descriptor.forceEmbedding) {
                return true;
            }
        }
        return false;
    }

    /**
     * Adds a package to the boot config and installs it
     *
     * @param {type} packageInfo
     * @param {type} step
     * @returns {undefined}
     */
    async addPackageRequire(packageInfo, step = -1) {
        let config = this.getBootConfig();

        if (step === -1 || step > config.require.length) {
            step = config.require.length - 1;
        }
        if (step === -1) {
            config.require.push({ dependencies: [], options: {} });
            step = 0;
        }

        // Add it and load it
        let packageRequire = { package: packageInfo.name, repository: packageInfo.repository };
        await WPMv2.require(packageRequire); // also install it right away
        config.require[step].dependencies.push(packageRequire);
        this.setBootConfig(config);
    }

    async removePackageRequire(packageInfo) {
        let config = this.getBootConfig();

        let removedIt = false;
        for (let requireStep of config.require) {
            if (!requireStep.dependencies) continue;
            if (!Array.isArray(requireStep.dependencies)) continue;

            let newDependencies = [];
            for (let dependency of requireStep.dependencies) {
                if (dependency.package && dependency.package === packageInfo.name) {
                    removedIt = true;
                } else {
                    newDependencies.push(dependency);
                }
            }
            requireStep.dependencies = newDependencies;
        }

        if (removedIt) {
            let localPackageElement = this.getLocalPackageElement(packageInfo.name);
            if (this.isTransientPackageElement(localPackageElement)) {
                localPackageElement.remove();
                document.querySelector("[transient-wpmid='" + packageInfo.name + "']")?.remove();
            }
        }

        this.setBootConfig(config);
    }

    async removeRepositoryRequire(repositoryURL) {
        let config = this.getBootConfig();

        let removedIt = false;
        for (let requireStep of config.require) {
            if (!requireStep.dependencies) continue;
            if (!Array.isArray(requireStep.dependencies)) continue;

            let newDependencies = [];
            for (let dependency of requireStep.dependencies) {
                if (dependency.repository && dependency.repository === repositoryURL && !dependency.package) {
                    removedIt = true;
                } else {
                    newDependencies.push(dependency);
                }
            }
            requireStep.dependencies = newDependencies;
        }

        if (removedIt) {
            // TODO: Figure out if something else also depends on it
            // If not, remove it from runtime too
        }

        this.setBootConfig(config);
    }

    /**
     * Adds a package to the boot config and installs it
     *
     * @param {type} repositoryURL
     * @param {type} step
     * @returns {undefined}
     */
    async addRepositoryRequire(repositoryURL, step = -1) {
        let config = this.getBootConfig();

        if (step === -1 || step > config.require.length) {
            step = config.require.length - 1;
        }
        if (step === -1) {
            config.require.push({ dependencies: [], options: {} });
            step = 0;
        }

        // Add it and load it
        let repositoryRequire = { repository: repositoryURL };
        await WPMv2.require(repositoryRequire); // also install it right away
        config.require[step].dependencies.push(repositoryRequire);
        this.setBootConfig(config);
    }

    async embedPackages(wpmPackages) {
        // Prepare for this package to be embedded
        let requiredPackages = [];
        wpmPackages.forEach((wpmPackage) => {
            let localPackageElement = this.getLocalPackageElement(wpmPackage.name);
            if (localPackageElement && this.isTransientPackageElement(localPackageElement)) {
                // Already installed transiently, remove first
                localPackageElement.remove();
                //Also remove wpm transient element if found
                document.querySelector("[transient-wpmid='" + wpmPackage.name + "']")?.remove();
            };
            requiredPackages.push({ package: wpmPackage.name, repository: wpmPackage.repository, appendTarget: "head" });
        });

        await WPMv2.require(requiredPackages, { "bootstrap": "false" }); // install it right away
    }

    async unembedPackage(wpmPackage) {
        let localPackageElement = this.getLocalPackageElement(wpmPackage.name); // update the package element
        localPackageElement.remove();

        if (this.isConfigDirectlyRequiringPackage(this.getBootConfig(), wpmPackage.name)) {
            // Re-install transiently if required
            await WPMv2.require(wpmPackage);
        };
    }

    renderPackageItem(wpmPackage, bootConfig, displayOptions = {}) {
        let self = this;

        let packageItemTemplate = WebstrateComponents.Tools.loadTemplate("#packageBrowserPackageItem");
        if (displayOptions.missingInRepository) {
            packageItemTemplate.classList.add("missing");
            packageItemTemplate.title = "This package could not be found in the repository";
        };

        if (!wpmPackage.name) {
            return WebstrateComponents.Tools.loadTemplate("#packageBrowserPackageItemError");
        }
        packageItemTemplate.querySelector(".package-name").innerText = wpmPackage.name;
        packageItemTemplate.querySelector(".package-name").title = wpmPackage.name;

        ["version", "description", "license"].forEach((packageProperty) => {
            if (wpmPackage[packageProperty]) {
                packageItemTemplate.querySelector(".package-" + packageProperty).innerText = wpmPackage[packageProperty];
                packageItemTemplate.querySelector(".package-" + packageProperty).title = wpmPackage[packageProperty];
            }
        });

        let localPackageElement = this.getLocalPackageElement(wpmPackage.name);
        if (localPackageElement) {
            // Check if required directly or because of dependency/system
            if (this.isConfigDirectlyRequiringPackage(bootConfig, wpmPackage.name)) {
                packageItemTemplate.querySelector(".package-required input").checked = true;
            } else {
                if (this.isLive(localPackageElement)) {
                    packageItemTemplate.querySelector(".package-required").setAttribute("data-indirect-requirement", "true");
                    packageItemTemplate.querySelector(".package-required .mdc-checkbox").setAttribute("title", "Indirectly required by another package at runtime");

                    try {
                        // Add which package pulled this in
                        let pulledInBy = [];
                        WPMv2.getCurrentlyInstalledPackages().forEach(function (pkg) {
                            pkg.dependencies.forEach(function (dep) {
                                if (typeof dep === "string" && dep.indexOf("#" + wpmPackage.name) != -1) {
                                    pulledInBy.push(pkg);
                                }
                            });
                        });
                        if (pulledInBy.length > 0) {
                            packageItemTemplate.querySelector(".package-required .mdc-checkbox").setAttribute("title", "Indirectly required by " + pulledInBy);
                        }
                    } catch (ex) {
                        console.warn(ex);
                    }
                }
            }

            // Embedding status
            packageItemTemplate.querySelector(".package-embedded input").checked = !this.isTransientPackageElement(localPackageElement);
            if (this.isForcedEmbeddedPackage(wpmPackage)) {
                packageItemTemplate.querySelector(".package-embedded input").setAttribute("title", "This package wants to be embedded when installed");
            }
        }


        // Click handlers
        packageItemTemplate.querySelector(".package-required input").addEventListener("click", async (box) => {
            if (box.target.checked) {
                // Require a package
                if (self.isForcedEmbeddedPackage(wpmPackage) && !packageItemTemplate.querySelector(".package-embedded input").checked) {
                    await self.embedPackages([wpmPackage]);
                    packageItemTemplate.querySelector(".package-embedded input").checked = true;

                }
                await self.addPackageRequire(wpmPackage);
            } else {
                if (self.isForcedEmbeddedPackage(wpmPackage) && packageItemTemplate.querySelector(".package-embedded input").checked) {
                    await self.unembedPackage(wpmPackage);
                    packageItemTemplate.querySelector(".package-embedded input").checked = false;
                }

                // Remove require for a package
                await self.removePackageRequire(wpmPackage);
            }
            localPackageElement = self.getLocalPackageElement(wpmPackage.name); // update the package element
        });

        packageItemTemplate.querySelector(".package-embedded input").addEventListener("click", async (box) => {
            if (box.target.checked) {
                await self.embedPackages([wpmPackage]);
                packageItemTemplate.querySelector(".package-embedded input").checked = true;
            } else {
                if (((!displayOptions.missingInRepository) || confirm("This package is missing in the repository, removing it will permanently delete it"))
                    &&
                    ((!displayOptions.repositoryIsThisPage) || confirm("The package only exists in this page, removing it will permanently delete it"))) {
                    await self.unembedPackage(wpmPackage);
                    packageItemTemplate.querySelector(".package-embedded input").checked = false;
                    if (self.isForcedEmbeddedPackage(wpmPackage) && packageItemTemplate.querySelector(".package-required input").checked) {
                        await self.removePackageRequire(wpmPackage);
                        packageItemTemplate.querySelector(".package-required input").checked = false;
                    }
                } else {
                    packageItemTemplate.querySelector(".package-embedded input").checked = true;
                }
            }
            localPackageElement = self.getLocalPackageElement(wpmPackage.name); // update the package element
        });


        // Check repository includes
        if (wpmPackage.repository && self.isConfigDirectlyRequiringRepository(bootConfig, wpmPackage.repository)) {
            packageItemTemplate.querySelector(".package-required input").disabled = true;
            packageItemTemplate.querySelector(".package-required input").checked = true;
            packageItemTemplate.querySelector(".package-embedded input").disabled = true;
        }

        return packageItemTemplate;
    }
};