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
|
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: () => {
|
||||||
|
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 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) => {
|
manifest.id = manifest.name.replace("/", "-").replace("@", "")
|
||||||
worker.onmessage = ({ data }) => {
|
manifest.url = manifestUrl
|
||||||
if (data.event === "loaded") {
|
manifest.main = replaceRelativeImportWithUrl(manifest.main, manifestUrl)
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(Comlink.wrap(worker))
|
this.db.manifest.put(manifest)
|
||||||
|
this.load(manifest.id)
|
||||||
|
|
||||||
// if (typeof main.events === "object") {
|
this.runtime.eventBus.emit("extension:installed", manifest)
|
||||||
// Object.entries(main.events).forEach(([event, handler]) => {
|
this.logger.log(`Extension installed`, manifest)
|
||||||
// main.event.on(event, handler)
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// this.extensions.set(manifest.registryId,main.public)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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()
|
||||||
}
|
}
|
||||||
|
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