mirror of
https://github.com/ragestudio/vessel.git
synced 2025-06-09 02:24:17 +00:00
merge from local
This commit is contained in:
parent
fb190878b2
commit
d112e767a6
@ -97,7 +97,7 @@ export default class CoresManager {
|
||||
return true
|
||||
}
|
||||
|
||||
getCoreContext = () => {
|
||||
getContext = () => {
|
||||
return new Proxy(this.context, {
|
||||
get: (target, key) => target[key],
|
||||
set: () => {
|
||||
|
48
src/classes/ExtensionsManager/db.js
Normal file
48
src/classes/ExtensionsManager/db.js
Normal 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
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
78
src/extension/index.jsx
Normal 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
55
src/patches.js
Normal 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()
|
||||
}
|
||||
})
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
7
src/utils/replaceRelativeImportWithUrl/index.js
Normal file
7
src/utils/replaceRelativeImportWithUrl/index.js
Normal 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
|
Loading…
x
Reference in New Issue
Block a user