merge from local

This commit is contained in:
SrGooglo 2025-03-13 21:56:37 +00:00
parent fb190878b2
commit d112e767a6
8 changed files with 318 additions and 53 deletions

View File

@ -97,7 +97,7 @@ export default class CoresManager {
return true return true
} }
getCoreContext = () => { getContext = () => {
return new Proxy(this.context, { return new Proxy(this.context, {
get: (target, key) => target[key], get: (target, key) => target[key],
set: () => { set: () => {

View File

@ -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

View File

@ -1,10 +1,13 @@
import InternalConsole from "../InternalConsole" import InternalConsole from "../InternalConsole"
import { isUrl } from "../../utils/url" import { isUrl } from "../../utils/url"
import * as Comlink from "comlink" import replaceRelativeImportWithUrl from "../../utils/replaceRelativeImportWithUrl"
import ExtensionsDB from "./db"
import ExtensionWorker from "../../workers/extension.js?worker"
export default class ExtensionManager { export default class ExtensionManager {
constructor(runtime) {
this.runtime = runtime
}
logger = new InternalConsole({ logger = new InternalConsole({
namespace: "ExtensionsManager", namespace: "ExtensionsManager",
bgColor: "bgMagenta", bgColor: "bgMagenta",
@ -12,39 +15,124 @@ export default class ExtensionManager {
extensions = new Map() extensions = new Map()
loadExtension = async (manifest) => { db = new ExtensionsDB()
throw new Error("Not implemented")
if (isUrl(manifest)) { context = Object()
manifest = await fetch(manifest)
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() manifest = await manifest.json()
} }
const worker = new ExtensionWorker() this.runtime.eventBus.emit("extension:installing", manifest)
this.logger.log(`Installing extension`, manifest)
worker.postMessage({ if (!manifest.main) {
event: "load", throw new Error("Extension manifest is missing main file")
manifest: manifest,
})
await new Promise((resolve) => {
worker.onmessage = ({ data }) => {
if (data.event === "loaded") {
resolve()
}
}
})
console.log(Comlink.wrap(worker))
// if (typeof main.events === "object") {
// Object.entries(main.events).forEach(([event, handler]) => {
// main.event.on(event, handler)
// })
// }
// this.extensions.set(manifest.registryId,main.public)
} }
installExtension = async () => {} manifest.id = manifest.name.replace("/", "-").replace("@", "")
manifest.url = manifestUrl
manifest.main = replaceRelativeImportWithUrl(manifest.main, manifestUrl)
this.db.manifest.put(manifest)
this.load(manifest.id)
this.runtime.eventBus.emit("extension:installed", manifest)
this.logger.log(`Extension installed`, manifest)
}
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)
}
}
} }

View File

@ -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()
}
}
}

78
src/extension/index.jsx Normal file
View File

@ -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 () => <RenderModule {...props} />
})
}
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,
},
},
)
}
}
}
}

55
src/patches.js Normal file
View File

@ -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()
}
})
}

View File

@ -1,6 +1,9 @@
import pkgJson from "../package.json" import pkgJson from "../package.json"
import "./patches"
import React from "react" import React from "react"
window.React = React
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import { createBrowserHistory } from "history" import { createBrowserHistory } from "history"
@ -116,8 +119,13 @@ export default class Runtime {
this.registerPublicField("isMobile", isMobile()) this.registerPublicField("isMobile", isMobile())
this.registerPublicField("__version", pkgJson.version) this.registerPublicField("__version", pkgJson.version)
window.app.cores = this.cores.getCoreContext() // create fake process
//window.app.extensions = new ExtensionManager() window.process = {
env: {},
}
window.app.cores = this.cores.getContext()
window.app.extensions = this.extensions
this.registerEventsToBus(this.internalEvents) this.registerEventsToBus(this.internalEvents)
@ -166,6 +174,9 @@ export default class Runtime {
this.eventBus.emit("runtime.initialize.finish") this.eventBus.emit("runtime.initialize.finish")
this.render(this.baseAppClass) this.render(this.baseAppClass)
// initialize extension manager
this.extensions.initialize()
if (!this.baseAppClass.splashAwaitEvent) { if (!this.baseAppClass.splashAwaitEvent) {
this.splash.detach() this.splash.detach()
} }

View File

@ -0,0 +1,7 @@
import { isUrl } from "../url"
function replaceRelativeImportWithUrl(relativePath, url) {
return isUrl(relativePath) ? relativePath : new URL(relativePath, url).href
}
export default replaceRelativeImportWithUrl