From e897de6814548daff44df49874171991a46505de Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Sun, 17 Nov 2024 20:35:30 +0000 Subject: [PATCH] merge from local --- package.json | 6 +- src/classes/ExtensionsManager/index.js | 50 ++++++++++ src/extension/index.js | 40 +++----- src/runtime.jsx | 10 +- src/utils/isJsxCode/index.js | 7 ++ src/utils/url/index.js | 20 ++++ src/workers/extension.js | 132 +++++++++++++++++++++++++ 7 files changed, 229 insertions(+), 36 deletions(-) create mode 100644 src/classes/ExtensionsManager/index.js create mode 100644 src/utils/isJsxCode/index.js create mode 100644 src/workers/extension.js diff --git a/package.json b/package.json index 13ff661..2dfe94e 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,14 @@ "dependencies": { "@loadable/component": "^5.16.4", "@vitejs/plugin-react": "^4.3.3", + "comlink": "^4.4.1", "history": "^5.3.0", "less": "^4.2.0", "million": "^3.1.11", "object-observer": "^6.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-helmet": "^6.1.0" + "react-helmet": "^6.1.0", + "sucrase": "^3.35.0" } -} \ No newline at end of file +} diff --git a/src/classes/ExtensionsManager/index.js b/src/classes/ExtensionsManager/index.js new file mode 100644 index 0000000..2e83407 --- /dev/null +++ b/src/classes/ExtensionsManager/index.js @@ -0,0 +1,50 @@ +import InternalConsole from "../InternalConsole" +import { isUrl } from "../../utils/url" +import * as Comlink from "comlink" + +import ExtensionWorker from "../../workers/extension.js?worker" + +export default class ExtensionManager { + logger = new InternalConsole({ + namespace: "ExtensionsManager", + bgColor: "bgMagenta", + }) + + extensions = new Map() + + loadExtension = async (manifest) => { + if (isUrl(manifest)) { + manifest = await fetch(manifest) + manifest = await manifest.json() + } + + const worker = new ExtensionWorker() + + worker.postMessage({ + event: "load", + 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 () => { + + } +} \ No newline at end of file diff --git a/src/extension/index.js b/src/extension/index.js index b3b009f..3eb94f4 100644 --- a/src/extension/index.js +++ b/src/extension/index.js @@ -1,34 +1,22 @@ -import { Observable } from "object-observer" +import InternalConsole from "../classes/InternalConsole" +import EventBus from "../classes/EventBus" export default class Extension { - constructor(appContext, mainContext) { - this.appContext = appContext - this.mainContext = mainContext - - this.promises = [] - - return this + constructor(params = {}) { + this.params = params } - __initializer() { - return new Promise(async (resolve, reject) => { - if (Array.isArray(this.depends)) { - this.depends.forEach((dependency) => { - const dependencyPromise = new Promise((resolve, reject) => { - Observable.observe(this.mainContext.ATTACHED_EXTENSIONS, (changes) => { - changes.forEach((change) => { - Array.from(change.object).includes(dependency) && resolve() - }) - }) - }) + eventBus = new EventBus({ + id: this.constructor.namespace ?? this.constructor.name, + }) - this.promises.push(dependencyPromise) - }) - } + console = new InternalConsole({ + namespace: this.constructor.namespace ?? this.constructor.name, + }) - await Promise.all(this.promises) - - return resolve() - }) + async _init() { + if (typeof this.onInitialize === "function") { + this.onInitialize() + } } } \ No newline at end of file diff --git a/src/runtime.jsx b/src/runtime.jsx index 70b5dd9..1afc869 100644 --- a/src/runtime.jsx +++ b/src/runtime.jsx @@ -11,6 +11,7 @@ import Extension from "./extension" import EventBus from "./classes/EventBus" import InternalConsole from "./classes/InternalConsole" +import ExtensionManager from "./classes/ExtensionsManager" import isMobile from "./utils/isMobile" @@ -81,14 +82,7 @@ export default class EviteRuntime { } }) - window.app.extensions = new Proxy(this.ExtensionsPublicContext, { - get: (target, key) => { - return target[key] - }, - set: (target, key, value) => { - throw new Error("You can't set a extension value") - } - }) + window.app.extensions = new ExtensionManager() this.registerPublicMethod({ key: "isMobile", locked: true }, isMobile()) this.registerPublicMethod({ key: "__version", locked: true }, pkgJson.version) diff --git a/src/utils/isJsxCode/index.js b/src/utils/isJsxCode/index.js new file mode 100644 index 0000000..6cc2dc8 --- /dev/null +++ b/src/utils/isJsxCode/index.js @@ -0,0 +1,7 @@ +function isJsxCode(code) { + const jsxPattern = /<([A-Za-z][A-Za-z0-9]*)(\s+[^>]*)?(\/?>).*<\/\1>|<([A-Za-z][A-Za-z0-9]*)(\s+[^>]*)?\/>/s; + + return jsxPattern.test(code) +} + +export default isJsxCode \ No newline at end of file diff --git a/src/utils/url/index.js b/src/utils/url/index.js index 980cbed..7a5c3b0 100644 --- a/src/utils/url/index.js +++ b/src/utils/url/index.js @@ -12,12 +12,32 @@ export function withSuffix(string, suffix) { return string.endsWith(suffix) ? string : string + suffix } +export function isUrl(string) { + return string.startsWith("http://") || string.startsWith("https://") +} + export function withoutSuffix(string, suffix) { return string.endsWith(suffix) ? string.slice(0, -1 * suffix.length) : string + suffix } +export function URLResolve(url, ...paths) { + const urlObj = new URL(url) + + paths.forEach((path) => { + if (typeof path !== "string") { + return path + } + + path = path.replace(/^\//, '') + + urlObj.pathname = urlObj.pathname.replace(/\/$/, '') + '/' + path + }) + + return urlObj.toString() +} + export function createUrl(urlLike) { if (urlLike instanceof URL) { return urlLike diff --git a/src/workers/extension.js b/src/workers/extension.js new file mode 100644 index 0000000..2c99a7e --- /dev/null +++ b/src/workers/extension.js @@ -0,0 +1,132 @@ +import * as Comlink from "comlink" +import { isUrl, URLResolve } from "../utils/url" +import classAggregation from "../utils/classAggregation" +import ExtensionClass from "../extension" + +let main = null +let manifest = null + +class Logger { + static log(...args) { + console.log(`[ExtensionWorker]`, ...args) + } + + static error(...args) { + console.error(`[ExtensionWorker]`, ...args) + } + + static warn(...args) { + console.warn(`[ExtensionWorker]`, ...args) + } +} + +async function replaceImportsWithRemoteURL(codeStr, assetsUrl) { + const importRegex = /from\s+["']([^"'\s]+)["']|import\s+["']([^"'\s]+)["']/g + + const matches = [...codeStr.matchAll(importRegex)] + + let replacements = matches.map(async ([match, fromImportStr, simpleImportStr]) => { + if (fromImportStr) { + if (fromImportStr.startsWith("./")) { + fromImportStr = await URLResolve(assetsUrl, fromImportStr) + fromImportStr = await createCodeResource(fromImportStr) + } + + return `from "${fromImportStr}"` + } + + if (simpleImportStr) { + if (simpleImportStr.startsWith("./")) { + simpleImportStr = await URLResolve(assetsUrl, simpleImportStr) + simpleImportStr = await createCodeResource(simpleImportStr) + } + + return `import "${simpleImportStr}"` + } + }) + + replacements = await Promise.all(replacements) + + matches.forEach((match, i) => { + codeStr = codeStr.replace(match[0], replacements[i]) + }) + + return codeStr +} + +async function createCodeResource(code) { + if (!code) { + return null + } + + if (!manifest || !manifest.srcUrl) { + throw new Error(`Manifest does not have srcUrl`) + } + + if (isUrl(code)) { + code = await fetch(code) + code = await code.text() + } + + code = await replaceImportsWithRemoteURL(code, manifest.srcUrl) + + const blob = new Blob([code], { type: "application/javascript" }) + return URL.createObjectURL(blob) +} + +onmessage = async ({ data }) => { + if (data.event === "load") { + manifest = data.manifest + + Logger.log(`Loading extension [${manifest.registryId}] >`, data.manifest) + + // try to fetch package.json + if (!manifest.packageUrl) { + throw new Error(`Extension ${manifest.registryId} is missing packageUrl`) + } + + // set packageJson + const packageJson = await fetch(manifest.packageUrl) + + manifest.packageJson = await packageJson.json() + + // try to fetch main file + if (!manifest.packageJson.main) { + throw new Error(`Extension ${manifest.registryId} is missing main`) + } + + // resolve url with the assetsUrl + manifest.packageJson.main = URLResolve(manifest.assetsUrl, manifest.packageJson.main) + + // load main file as a module + main = await createCodeResource(manifest.packageJson.main) + + try { + const module = await import(main) + + main = module.default + } catch (error) { + Logger.error(`Failed to import main module >`, error) + } + + try { + main = classAggregation(ExtensionClass, main) + + main = new main() + } catch (error) { + Logger.error(`Failed to instantiate main module >`, error) + } + + try { + await main._init() + } catch (error) { + Logger.error(`Failed to initialize main module >`, error) + } + + postMessage({ event: "loaded" }) + + Logger.log("Exposing main class >", main, main.public) + + Comlink.expose(main.public) + } +} \ No newline at end of file