From d112e767a6dd32aec9eb8152b0700acbfd8a8b15 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Thu, 13 Mar 2025 21:56:37 +0000 Subject: [PATCH] merge from local --- src/classes/CoresManager/index.js | 2 +- src/classes/ExtensionsManager/db.js | 48 ++++++ src/classes/ExtensionsManager/index.js | 144 ++++++++++++++---- src/extension/index.js | 22 --- src/extension/index.jsx | 78 ++++++++++ src/patches.js | 55 +++++++ src/runtime.jsx | 15 +- .../replaceRelativeImportWithUrl/index.js | 7 + 8 files changed, 318 insertions(+), 53 deletions(-) create mode 100644 src/classes/ExtensionsManager/db.js delete mode 100644 src/extension/index.js create mode 100644 src/extension/index.jsx create mode 100644 src/patches.js create mode 100644 src/utils/replaceRelativeImportWithUrl/index.js diff --git a/src/classes/CoresManager/index.js b/src/classes/CoresManager/index.js index e80eba7..77f9397 100644 --- a/src/classes/CoresManager/index.js +++ b/src/classes/CoresManager/index.js @@ -97,7 +97,7 @@ export default class CoresManager { return true } - getCoreContext = () => { + getContext = () => { return new Proxy(this.context, { get: (target, key) => target[key], set: () => { diff --git a/src/classes/ExtensionsManager/db.js b/src/classes/ExtensionsManager/db.js new file mode 100644 index 0000000..0db3a98 --- /dev/null +++ b/src/classes/ExtensionsManager/db.js @@ -0,0 +1,48 @@ +import localforage from "localforage" + +class ExtensionsDB { + static dbName = "extensions" + + db = null + + async initialize() { + this.db = await localforage.createInstance({ + name: ExtensionsDB.dbName, + storeName: ExtensionsDB.dbName, + driver: localforage.INDEXEDDB, + }) + + if (!(await this.db.getItem("manifests"))) { + await this.db.setItem("manifests", {}) + } + + if (!(await this.db.getItem("data"))) { + await this.db.setItem("data", []) + } + + return this.db + } + + manifest = { + put: async (manifest) => { + const manifests = await this.db.getItem("manifests") + manifests[manifest.id] = manifest + await this.db.setItem("manifests", manifests) + }, + get: async (id) => { + const manifests = await this.db.getItem("manifests") + return manifests[id] + }, + getAll: async () => { + const manifests = await this.db.getItem("manifests") + return Object.values(manifests) + }, + delete: async (id) => { + const manifests = await this.db.getItem("manifests") + delete manifests[id] + await this.db.setItem("manifests", manifests) + }, + } +} + +export default ExtensionsDB diff --git a/src/classes/ExtensionsManager/index.js b/src/classes/ExtensionsManager/index.js index 8796f13..092dede 100644 --- a/src/classes/ExtensionsManager/index.js +++ b/src/classes/ExtensionsManager/index.js @@ -1,10 +1,13 @@ import InternalConsole from "../InternalConsole" import { isUrl } from "../../utils/url" -import * as Comlink from "comlink" - -import ExtensionWorker from "../../workers/extension.js?worker" +import replaceRelativeImportWithUrl from "../../utils/replaceRelativeImportWithUrl" +import ExtensionsDB from "./db" export default class ExtensionManager { + constructor(runtime) { + this.runtime = runtime + } + logger = new InternalConsole({ namespace: "ExtensionsManager", bgColor: "bgMagenta", @@ -12,39 +15,124 @@ export default class ExtensionManager { extensions = new Map() - loadExtension = async (manifest) => { - throw new Error("Not implemented") + db = new ExtensionsDB() - if (isUrl(manifest)) { - manifest = await fetch(manifest) + context = Object() + + load = async (id) => { + let manifest = await this.db.manifest.get(id) + + if (!manifest) { + throw new Error(`Extension ${id} not found`) + } + + this.runtime.eventBus.emit("extension:loading", manifest) + this.logger.log(`Loading extension`, manifest) + + if (!manifest.main) { + throw new Error("Extension manifest is missing main file") + } + + // load main file + let mainClass = await import( + /* @vite-ignore */ + manifest.main + ) + + // inject dependencies + mainClass = mainClass.default + + // initializate + let main = new mainClass(this.runtime, this, manifest) + + await main._init() + + // set extension in map + this.extensions.set(manifest.id, { + manifest: manifest, + main: main, + worker: null, + }) + + this.runtime.eventBus.emit("extension:loaded", manifest) + this.logger.log(`Extension loaded`, manifest) + } + + unload = async (id) => { + let extension = this.extensions.get(id) + + if (!extension) { + throw new Error(`Extension ${id} not found`) + } + + await extension.main._unload() + + this.extensions.delete(id) + + this.runtime.eventBus.emit("extension:unloaded", extension.manifest) + this.logger.log(`Extension unloaded`, extension.manifest) + } + + install = async (manifestUrl) => { + let manifest = null + + if (isUrl(manifestUrl)) { + manifest = await fetch(manifestUrl) manifest = await manifest.json() } - const worker = new ExtensionWorker() + this.runtime.eventBus.emit("extension:installing", manifest) + this.logger.log(`Installing extension`, manifest) - worker.postMessage({ - event: "load", - manifest: manifest, - }) + if (!manifest.main) { + throw new Error("Extension manifest is missing main file") + } - await new Promise((resolve) => { - worker.onmessage = ({ data }) => { - if (data.event === "loaded") { - resolve() - } - } - }) + manifest.id = manifest.name.replace("/", "-").replace("@", "") + manifest.url = manifestUrl + manifest.main = replaceRelativeImportWithUrl(manifest.main, manifestUrl) - console.log(Comlink.wrap(worker)) + this.db.manifest.put(manifest) + this.load(manifest.id) - // if (typeof main.events === "object") { - // Object.entries(main.events).forEach(([event, handler]) => { - // main.event.on(event, handler) - // }) - // } - - // this.extensions.set(manifest.registryId,main.public) + this.runtime.eventBus.emit("extension:installed", manifest) + this.logger.log(`Extension installed`, manifest) } - installExtension = async () => {} + uninstall = async (id) => { + let extension = this.extensions.get(id) + + if (!extension) { + throw new Error(`Extension ${id} not found`) + } + + this.runtime.eventBus.emit("extension:uninstalling", extension.manifest) + this.logger.log(`Uninstalling extension`, extension.manifest) + + await this.unload(extension.manifest.id) + await this.db.manifest.delete(extension.manifest.id) + + this.runtime.eventBus.emit("extension:uninstalled", extension.manifest) + this.logger.log(`Extension uninstalled`, extension.manifest) + } + + registerContext(id, value) { + return (this.context[id] = value) + } + + unregisterContext(id) { + delete this.context[id] + } + + async initialize() { + await this.db.initialize() + + // load all extensions + // TODO: load this to late app initializer to avoid waiting for extensions to load the app + let extensions = await this.db.manifest.getAll() + + for (let extension of extensions) { + await this.load(extension.id) + } + } } diff --git a/src/extension/index.js b/src/extension/index.js deleted file mode 100644 index 3eb94f4..0000000 --- a/src/extension/index.js +++ /dev/null @@ -1,22 +0,0 @@ -import InternalConsole from "../classes/InternalConsole" -import EventBus from "../classes/EventBus" - -export default class Extension { - constructor(params = {}) { - this.params = params - } - - eventBus = new EventBus({ - id: this.constructor.namespace ?? this.constructor.name, - }) - - console = new InternalConsole({ - namespace: this.constructor.namespace ?? this.constructor.name, - }) - - async _init() { - if (typeof this.onInitialize === "function") { - this.onInitialize() - } - } -} \ No newline at end of file diff --git a/src/extension/index.jsx b/src/extension/index.jsx new file mode 100644 index 0000000..034ff75 --- /dev/null +++ b/src/extension/index.jsx @@ -0,0 +1,78 @@ +import InternalConsole from "../classes/InternalConsole" +import EventBus from "../classes/EventBus" +import loadable from "@loadable/component" + +import replaceRelativeImportWithUrl from "../utils/replaceRelativeImportWithUrl" + +function buildAppRender(renderURL, props) { + return loadable(async () => { + /* @vite-ignore */ + let RenderModule = await import(/* @vite-ignore */ renderURL) + + RenderModule = RenderModule.default + + return () => + }) +} + +export default class Extension { + constructor(runtime, manager, manifest) { + this.runtime = runtime + this.manager = manager + this.manifest = manifest + + this.originUrl = this.manifest.url.split("/").slice(0, -1).join("/") + + this.eventBus = new EventBus({ + id: this.constructor.namespace ?? this.constructor.name, + }) + this.console = new InternalConsole({ + namespace: this.constructor.namespace ?? this.constructor.name, + }) + } + + async _unload() { + if (typeof this.onUnload === "function") { + await this.onUnload() + } + + if (typeof this.public === "object") { + this.manager.unregisterContext(this.manifest.id) + } + + if (typeof this.app === "object") { + if (typeof this.app.render === "string") { + this.app.renderComponent = null + } + } + } + + async _init() { + if (typeof this.onInitialize === "function") { + this.onInitialize() + } + + if (typeof this.public === "object") { + this.manager.registerContext(this.manifest.id, this.public) + } + + if (typeof this.app === "object") { + if (typeof this.app.render === "string") { + this.app.render = await replaceRelativeImportWithUrl( + this.app.render, + this.manifest.url, + ) + + this.app.renderComponent = await buildAppRender( + this.app.render, + { + extension: { + main: this, + manifest: this.manifest, + }, + }, + ) + } + } + } +} diff --git a/src/patches.js b/src/patches.js new file mode 100644 index 0000000..d15598c --- /dev/null +++ b/src/patches.js @@ -0,0 +1,55 @@ +// Patch global prototypes +import { Buffer } from "buffer" + +globalThis.IS_MOBILE_HOST = window.navigator.userAgent === "capacitor" + +window.Buffer = Buffer + +Array.prototype.findAndUpdateObject = function (discriminator, obj) { + let index = this.findIndex( + (item) => item[discriminator] === obj[discriminator], + ) + if (index !== -1) { + this[index] = obj + } + + return index +} + +Array.prototype.move = function (from, to) { + this.splice(to, 0, this.splice(from, 1)[0]) + return this +} + +String.prototype.toTitleCase = function () { + return this.replace(/\w\S*/g, function (txt) { + return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() + }) +} + +String.prototype.toBoolean = function () { + return this === "true" +} + +Promise.tasked = function (promises) { + return new Promise(async (resolve, reject) => { + let rejected = false + + for await (let promise of promises) { + if (rejected) { + return + } + + try { + await promise() + } catch (error) { + rejected = true + return reject(error) + } + } + + if (!rejected) { + return resolve() + } + }) +} diff --git a/src/runtime.jsx b/src/runtime.jsx index 6239cb6..bb35f40 100644 --- a/src/runtime.jsx +++ b/src/runtime.jsx @@ -1,6 +1,9 @@ import pkgJson from "../package.json" +import "./patches" + import React from "react" +window.React = React import { createRoot } from "react-dom/client" import { createBrowserHistory } from "history" @@ -116,8 +119,13 @@ export default class Runtime { this.registerPublicField("isMobile", isMobile()) this.registerPublicField("__version", pkgJson.version) - window.app.cores = this.cores.getCoreContext() - //window.app.extensions = new ExtensionManager() + // create fake process + window.process = { + env: {}, + } + + window.app.cores = this.cores.getContext() + window.app.extensions = this.extensions this.registerEventsToBus(this.internalEvents) @@ -166,6 +174,9 @@ export default class Runtime { this.eventBus.emit("runtime.initialize.finish") this.render(this.baseAppClass) + // initialize extension manager + this.extensions.initialize() + if (!this.baseAppClass.splashAwaitEvent) { this.splash.detach() } diff --git a/src/utils/replaceRelativeImportWithUrl/index.js b/src/utils/replaceRelativeImportWithUrl/index.js new file mode 100644 index 0000000..3c761c6 --- /dev/null +++ b/src/utils/replaceRelativeImportWithUrl/index.js @@ -0,0 +1,7 @@ +import { isUrl } from "../url" + +function replaceRelativeImportWithUrl(relativePath, url) { + return isUrl(relativePath) ? relativePath : new URL(relativePath, url).href +} + +export default replaceRelativeImportWithUrl