Home Reference Source

cables_dev/cables_electron/src_client/standalone.js

import { Logger } from "cables-shared-client";
import ElectronEditor from "./electron_editor.js";
import electronCommands from "./cmd_electron.js";

/**
 * frontend class for cables standalone
 * initializes the ui, starts the editor and adds functions custom to this platform
 */
export default class CablesStandalone
{
    constructor()
    {
        this._electron = window.nodeRequire("electron");
        this._importSync = window.nodeRequire("import-sync");

        window.ipcRenderer = this._electron.ipcRenderer; // needed to have ipcRenderer in electron_editor.js
        this._settings = this._electron.ipcRenderer.sendSync("platformSettings") || {};
        this._usersettings = this._settings.userSettings;
        delete this._settings.userSettings;
        this._config = this._electron.ipcRenderer.sendSync("cablesConfig") || {};
        this.editorIframe = null;

        this._startUpLogItems = this._electron.ipcRenderer.sendSync("getStartupLog") || [];

        if (!this._config.isPackaged) window.ELECTRON_DISABLE_SECURITY_WARNINGS = true;

        this._loadedModules = {};
    }

    /**
     * the `gui` object of the current editor, if initialized
     *
     * @type {Gui|null}
     */
    get gui()
    {
        return this.editorWindow ? this.editorWindow.gui : null;
    }

    /**
     * the current editor window, if initialized
     *
     * @type {{}|null}
     */
    get editorWindow()
    {
        return this.editorIframe.contentWindow;
    }

    /**
     * the CABLES core instance of the current editor window, if initialized
     *
     * @type {{}|null}
     */
    get CABLES()
    {
        return this.editorWindow ? this.editorWindow.CABLES : null;
    }

    /**
     * initialize the editor, wait for core and ui to be ready, add
     * custom functionality
     */
    init()
    {
        this.editorIframe = document.getElementById("editorIframe");
        let src = this._config.uiIndexHtml + window.location.search;
        if (window.location.hash)
        {
            src += window.location.hash;
        }
        this.editorIframe.src = src;
        this.editorIframe.onload = () =>
        {
            if (this.editorWindow)
            {
                const waitForAce = this.editorWindow.waitForAce;
                this.editorWindow.waitForAce = () =>
                {
                    this._log.info("loading", this._settings.patchFile);

                    this._incrementStartup();
                    this._logStartup("checking/installing op dependencies...");
                    this._electron.ipcRenderer.invoke("talkerMessage", "installProjectDependencies").then((npmResult) =>
                    {
                        this.editorWindow.CABLESUILOADER.cfg.patchConfig.onError = (...args) =>
                        {
                            // npm runtime error...
                            if (args && args[0] === "core_patch" && args[2] && args[2].message && args[2].message.includes("was compiled against a different Node.js version"))
                            {
                                const dirParts = args[2].message.split("/");
                                const opNameIndex = dirParts.findIndex((part) => { return part.startsWith("Ops."); });
                                const opName = dirParts[opNameIndex];
                                const packageName = dirParts[opNameIndex + 2];
                                const onClick = "CABLES.CMD.STANDALONE.openOpDir('', '" + opName + "');";

                                const msg = "try running this <a onclick=\"" + onClick + "\" > in the op dir</a>:";
                                this._log.error(msg);
                                this._log.error("`npm --prefix ./ install " + packageName + "`");
                                this._log.error("`npx \"@electron/rebuild\" -v " + process.versions.electron);
                            }
                        };
                        waitForAce();

                        if (npmResult.error && npmResult.data && npmResult.msg !== "UNSAVED_PROJECT")
                        {
                            npmResult.data.forEach((msg) =>
                            {
                                const opName = msg.opName ? " for " + msg.opName : "";
                                this._log.error("failed dependency" + opName + ": " + msg.stderr);
                            });
                        }
                        else if (npmResult.msg !== "EMPTY" && npmResult.msg !== "UNSAVED_PROJECT")
                        {
                            npmResult.data.forEach((result) =>
                            {
                                const npmText = result.stderr || result.stdout;
                                this._logStartup(result.opName + ": " + npmText);
                            });
                        }


                        if (this.gui)
                        {
                            this.gui.on("uiloaded", () =>
                            {
                                if (this.editor && this.editor.config && !this.editor.config.patchFile) this.gui.setStateUnsaved();
                            });
                        }
                    });
                };
                if (this._settings.uiLoadStart) this.editorWindow.CABLESUILOADER.uiLoadStart -= this._settings.uiLoadStart;
                this._startUpLogItems.forEach((logEntry) =>
                {
                    this._logStartup(logEntry.title);
                });
                if (this.editorWindow.loadjs)
                {
                    this.editorWindow.loadjs.ready("cables_core", this._coreReady.bind(this));
                    this.editorWindow.loadjs.ready("cablesuinew", this._uiReady.bind(this));
                }
            }
        };

        window.addEventListener("message", (event) =>
        {
            if (event.data && event.data.type === "hashchange")
            {
                window.location.hash = event.data.data;
            }
        }, false);

        window.addEventListener("hashchange", () =>
        {
            if (this.editorWindow)
            {
                this.editorWindow.postMessage({ "type": "hashchange", "data": window.location.hash }, "*");
            }
        }, false);

        this.editor = new ElectronEditor({
            "config": {
                ...this._settings,
                "isTrustedPatch": true,
                "platformClass": "PlatformStandalone",
                "urlCables": "cables://",
                "urlSandbox": "cables://",
                "communityUrl": this._config.communityUrl,
                "user": this._settings.currentUser,
                "usersettings": { "settings": this._usersettings },
                "isDevEnv": !this._config.isPackaged,
                "env": this._config.env,
                "patchId": this._settings.patchId,
                "patchVersion": "",
                "socketcluster": {},
                "remoteClient": false,
                "buildInfo": this._settings.buildInfo,
                "patchConfig": {
                    "allowEdit": true,
                    "prefixAssetPath": this._settings.currentPatchDir,
                    "assetPath": this._settings.paths.assetPath,
                    "paths": this._settings.paths
                },
            }
        });
    }

    _coreReady()
    {
        if (this.CABLES)
        {
            if (this.CABLES.Op)
            {
                const standAlone = this;
                this.CABLES.Op.prototype.require = function (moduleName)
                {
                    return standAlone._opRequire(moduleName, this, standAlone);
                };
            }
            if (this.CABLES.Patch)
            {
                Object.defineProperty(this.CABLES.Patch.prototype, "patchDir", { "get": this._patchDir.bind(this) });
            }
        }
    }

    _uiReady()
    {
        this.CABLES.UI.standaloneLogger = () =>
        {
            CABLES.UI = this.CABLES.UI;
            return new Logger("standalone");
        };
        this._log = this.CABLES.UI.standaloneLogger();
        if (this.CABLES)
        {
            const getOpsForFilename = this.CABLES.UI.getOpsForFilename;
            this.CABLES.UI.getOpsForFilename = (filename) =>
            {
                let defaultOps = getOpsForFilename(filename);
                if (defaultOps.length === 0)
                {
                    defaultOps.push(this.CABLES.UI.DEFAULTOPNAMES.HttpRequest);
                    const addOpCb = this.gui.corePatch().on("onOpAdd", (newOp) =>
                    {
                        const contentPort = newOp.getPortByName("Content", false);
                        if (contentPort) contentPort.set("String");
                        this.gui.corePatch().off(addOpCb);
                    });
                }
                return defaultOps;
            };
            this.CABLES.CMD.STANDALONE = electronCommands.functions;
            this.CABLES.CMD.commands = this.CABLES.CMD.commands.concat(electronCommands.commands);
            Object.assign(this.CABLES.CMD.PATCH, electronCommands.functionOverrides.PATCH);
            Object.assign(this.CABLES.CMD.RENDERER, electronCommands.functionOverrides.RENDERER);
            const commandOverrides = electronCommands.commandOverrides;
            this.CABLES.CMD.commands.forEach((command) =>
            {
                const commandOverride = commandOverrides.find((override) => { return override.cmd === command.cmd; });
                if (commandOverride)
                {
                    Object.assign(command, commandOverride);
                }
            });
        }
    }

    _opRequire(moduleName, op, thisClass)
    {
        if (op) op.setUiError("oprequire", null);
        if (moduleName === "electron") return thisClass._electron;
        if (this._loadedModules[moduleName]) return this._loadedModules[moduleName];

        try
        {
            // load module by directory name
            const modulePath = window.ipcRenderer.sendSync("getOpModuleDir", { "opName": op.objName || op._name, "opId": op.opId, "moduleName": moduleName });
            this._loadedModules[moduleName] = window.nodeRequire(modulePath);
            return this._loadedModules[moduleName];
        }
        catch (ePath)
        {
            try
            {
                // load module by resolved filename from package.json
                const moduleFile = window.ipcRenderer.sendSync("getOpModuleLocation", { "opName": op.objName || op._name, "opId": op.opId, "moduleName": moduleName });
                this._loadedModules[moduleName] = window.nodeRequire(moduleFile);
                return this._loadedModules[moduleName];
            }
            catch (eFile)
            {
                try
                {
                    // load module by module name
                    this._loadedModules[moduleName] = window.nodeRequire(moduleName);
                    return this._loadedModules[moduleName];
                }
                catch (eName)
                {
                    try
                    {
                        const moduleFile = window.ipcRenderer.sendSync("getOpModuleLocation", { "opName": op.objName || op._name, "opId": op.opId, "moduleName": moduleName, });
                        this._loadedModules[moduleName] = this._importSync(moduleFile);
                        return this._loadedModules[moduleName];
                    }
                    catch (eImport)
                    {
                        let errorMessage = "failed to load node module: " + moduleName + "\n\n";
                        errorMessage += "require by import:\n" + eImport + "\n\n";
                        errorMessage += "require by name:\n" + eName + "\n\n";
                        errorMessage += "require by file:\n" + eFile + "\n\n";
                        errorMessage += "require by path:\n" + ePath;
                        if (op) op.setUiError("oprequire", errorMessage);
                        this._log.error(errorMessage, eName, eFile, ePath);
                        return { };
                    }
                }
            }
        }
    }

    _patchDir(...args)
    {
        return this._settings.currentPatchDir;
    }

    _logStartup(title)
    {
        if (this.editorWindow && this.editorWindow.logStartup) this.editorWindow.logStartup(title);
    }

    _incrementStartup()
    {
        if (this.editorWindow && this.editorWindow.logStartup) this.editorWindow.incrementStartup();
    }
}