diff --git a/astro.config.ts b/astro.config.ts index c523f7c..7c4e0b7 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -79,6 +79,11 @@ export default defineConfig({ ], server: { proxy: { + "/api/catalog-stats": { + target: "http://localhost:8080/api/catalog-stats", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api\/catalog-stats/, "") + }, "/api/catalog-assets": { target: "http://localhost:8080/api/catalog-assets", changeOrigin: true, diff --git a/public/sw.js b/public/sw.js index 32ea8cb..50fa3a3 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,7 +1,9 @@ importScripts("/uv/uv.bundle.js"); importScripts("/uv/uv.config.js"); +importScripts("/workerware/workerware.js"); importScripts(__uv$config.sw || "/uv/uv.sw.js"); const uv = new UVServiceWorker(); +const ww = new WorkerWare({ debug: false }); //where we handle our plugins!!! self.addEventListener("message", function(event) { @@ -10,32 +12,51 @@ self.addEventListener("message", function(event) { //loop over the required data (we don't verify here as types will take care of us :D) event.data.forEach((data) => { if (data.remove) { - const idx = uv.config.inject.indexOf(data.host); - uv.config.inject.splice(idx, 1); + if (data.type === "page") { + const idx = uv.config.inject.indexOf(data.host); + uv.config.inject.splice(idx, 1); + } + else if (data.type === "serviceWorker") { + ww.deleteByName(data.name); + } } else { - uv.config.inject.push({ - host: data.host, - html: data.html, - injectTo: data.injectTo - }); + if (data.type === "page") { + uv.config.inject.push({ + host: data.host, + html: data.html, + injectTo: data.injectTo + }); + } + else if (data.type === "serviceWorker") { + const wwFunction = eval(data.function); + ww.use({ + function: wwFunction ? wwFunction : new Function(data.function), + name: data.name, + events: data.events + }) + } + else { + console.error('NO type exists for that. Only serviceWorker & page exist.'); + return; + } } }); - console.log(uv.config.inject); }); self.addEventListener("fetch", function (event) { - if (event.request.url.startsWith(location.origin + __uv$config.prefix)) { - event.respondWith( - (async function () { + event.respondWith( + (async () => { + const wwRes = await ww.run(event)(); + if (wwRes.includes(null)) { + return; + } + if (event.request.url.startsWith(location.origin + __uv$config.prefix)) { return await uv.fetch(event); - })() - ); - } else { - event.respondWith( - (async function () { + } + else { return await fetch(event.request); - })() - ); - } + } + })() + ); }); diff --git a/public/workerware/workerware.js b/public/workerware/workerware.js new file mode 100644 index 0000000..1fe7bf4 --- /dev/null +++ b/public/workerware/workerware.js @@ -0,0 +1,168 @@ +importScripts("/workerware/WWError.js"); +const dbg = console.log.bind(console, "[WorkerWare]"); +const time = console.time.bind(console, "[WorkerWare]"); +const timeEnd = console.timeEnd.bind(console, "[WorkerWare]"); + +/* + OPTS: + debug - Enables debug logging. + randomNames - Generate random names for middlewares. + timing - Logs timing for each middleware. +*/ + +const defaultOpt = { + debug: false, + randomNames: false, + timing: false, +}; + +const validEvents = [ + "abortpayment", + "activate", + "backgroundfetchabort", + "backgroundfetchclick", + "backgroundfetchfail", + "backgroundfetchsuccess", + "canmakepayment", + "contentdelete", + "cookiechange", + "fetch", + "install", + "message", + "messageerror", + "notificationclick", + "notificationclose", + "paymentrequest", + "periodicsync", + "push", + "pushsubscriptionchange", + "sync", +]; + +class WorkerWare { + constructor(opt) { + this._opt = Object.assign({}, defaultOpt, opt); + this._middlewares = []; + } + info() { + return { + version: "0.1.0", + middlewares: this._middlewares, + options: this._opt, + }; + } + use(middleware) { + let validateMW = this.validateMiddleware(middleware); + if (validateMW.error) throw new WWError(validateMW.error); + // This means the middleware is an anonymous function, or the user is silly and named their function "function" + if (middleware.function.name == "function") middleware.name = crypto.randomUUID(); + if (!middleware.name) middleware.name = middleware.function.name; + if (this._opt.randomNames) middleware.name = crypto.randomUUID(); + if (this._opt.debug) dbg("Adding middleware:", middleware.name); + this._middlewares.push(middleware); + } + // Run all middlewares for the event type passed in. + run(event) { + const middlewares = this._middlewares; + const returnList = []; + let fn = async () => { + for (let i = 0; i < middlewares.length; i++) { + if (middlewares[i].events.includes(event.type)) { + if (this._opt.timing) console.time(middlewares[i].name); + // Add the configuration to the event object. + event.workerware = { + config: middlewares[i].configuration || {}, + }; + if (!middlewares[i].explicitCall) { + let res = await middlewares[i].function(event); + if (this._opt.timing) console.timeEnd(middlewares[i].name); + returnList.push(res); + } + } + } + return returnList; + }; + return fn; + } + deleteByName(middlewareID) { + if (this._opt.debug) dbg("Deleting middleware:", middlewareID); + this._middlewares = this._middlewares.filter((mw) => mw.name !== middlewareID); + } + deleteByEvent(middlewareEvent) { + if (this._opt.debug) dbg("Deleting middleware by event:", middlewareEvent); + this._middlewares = this._middlewares.filter((mw) => !mw.events.includes(middlewareEvent)); + } + get() { + return this._middlewares; + } + /* + Run a single middleware by ID. + This assumes that the user knows what they're doing, and is running the middleware on an event that it's supposed to run on. + */ + runMW(name, event) { + const middlewares = this._middlewares; + if (this._opt.debug) dbg("Running middleware:", name); + // if (middlewares.includes(name)) { + // return middlewares[name](event); + // } else { + // throw new WWError("Middleware not found!"); + // } + let didCall = false; + for (let i = 0; i < middlewares.length; i++) { + if (middlewares[i].name == name) { + didCall = true; + event.workerware = { + config: middlewares[i].configuration || {}, + } + if (this._opt.timing) console.time(middlewares[i].name); + let call = middlewares[i].function(event); + if (this._opt.timing) console.timeEnd(middlewares[i].name); + return call; + } + } + if (!didCall) { + throw new WWError("Middleware not found!"); + } + } + // type middlewareManifest = { + // function: Function, + // name?: string, + // events: string[], // Should be a union of validEvents. + // configuration?: Object // Optional configuration for the middleware. + // } + validateMiddleware(middleware) { + if (!middleware.function) + return { + error: "middleware.function is required", + }; + if (typeof middleware.function !== "function") + return { + error: "middleware.function must be typeof function", + }; + if (typeof middleware.configuration !== "object" && middleware.configuration !== undefined) { + return { + error: "middleware.configuration must be typeof object", + }; + } + if (!middleware.events) + return { + error: "middleware.events is required", + }; + if (!Array.isArray(middleware.events)) + return { + error: "middleware.events must be an array", + }; + if (middleware.events.some((ev) => !validEvents.includes(ev))) + return { + error: "Invalid event type! Must be one of the following: " + validEvents.join(", "), + }; + if (middleware.explicitCall && typeof middleware.explicitCall !== "boolean") { + return { + error: "middleware.explicitCall must be typeof boolean", + }; + } + return { + error: undefined, + }; + } +} diff --git a/server/marketplace.ts b/server/marketplace.ts index b309b36..85477b6 100644 --- a/server/marketplace.ts +++ b/server/marketplace.ts @@ -49,8 +49,12 @@ const catalogAssets = db.define("catalog_assets", { }); function marketplaceAPI(app: FastifyInstance) { - app.get("/api", (request, reply) => { - reply.send({ Server: "Active" }); + app.get("/api/catalog-stats/", (request, reply) => { + reply.send({ + version: '1.0.0', + spec: 'Nebula Services', + enabled: true + }); }); // This API returns a list of the assets in the database (SW plugins and themes). diff --git a/server/server.ts b/server/server.ts index 290eb47..ce88932 100644 --- a/server/server.ts +++ b/server/server.ts @@ -10,7 +10,6 @@ import fastifyStatic from "@fastify/static"; import chalk from "chalk"; import Fastify, { FastifyReply, FastifyRequest } from "fastify"; import gradient from "gradient-string"; -import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize"; //@ts-ignore WHY would I want this typechecked AT ALL import { handler as ssrHandler } from "../dist/server/entry.mjs"; import { parsedDoc } from "./config.js"; diff --git a/src/pages/[lang]/index.astro b/src/pages/[lang]/index.astro index c0adbce..f8fdbc0 100644 --- a/src/pages/[lang]/index.astro +++ b/src/pages/[lang]/index.astro @@ -195,5 +195,5 @@ import { VERSION } from "astro:env/client"; } } }); - }); + }, true); diff --git a/src/utils/settings/marketplace.ts b/src/utils/settings/marketplace.ts index 0ec476f..00b0526 100644 --- a/src/utils/settings/marketplace.ts +++ b/src/utils/settings/marketplace.ts @@ -1,6 +1,6 @@ //marketplace code & handlers import { Settings } from "."; -import { type Package, type PackageType, type Plugin, type PluginType, type SWPlugin } from "./types"; +import { type Package, type PackageType, type Plugin, type PluginType, type SWPagePlugin, type SWPlugin } from "./types"; const AppearanceSettings = { themes: "nebula||themes", themeName: "nebula||themeName", @@ -53,8 +53,8 @@ const marketPlaceSettings = { if (p === "theme") { let items = localStorage.getItem(AppearanceSettings.themes) as any; items ? (items = JSON.parse(items)) : (items = []); - if (items.find((theme: any) => theme === packageName)) { - const idx = items.indexOf(packageName); + if (items.find((theme: any) => theme === packageName.toLowerCase())) { + const idx = items.indexOf(packageName.toLowerCase()); items.splice(idx, 1); localStorage.setItem(AppearanceSettings.themes, JSON.stringify(items)); this.changeTheme(true); @@ -65,7 +65,7 @@ const marketPlaceSettings = { let plugins = localStorage.getItem(PluginSettings.plugins) as any; plugins ? (plugins = JSON.parse(plugins)) : (plugins = []); //@ts-ignore - const plugin = plugins.find(({name}) => name === packageName); + const plugin = plugins.find(({name}) => name === packageName.toLowerCase()); if (plugin) { plugin.remove = true; localStorage.setItem(PluginSettings.plugins, JSON.stringify(plugins)); @@ -77,6 +77,7 @@ const marketPlaceSettings = { handlePlugins: function(worker: never | ServiceWorkerRegistration) { return new Promise((resolve) => { const plugins = JSON.parse(localStorage.getItem(Settings.PluginSettings.plugins) as string) || []; + const swPagePlugins: SWPagePlugin[] = []; const swPlugins: SWPlugin[] = []; if (plugins.length === 0) { console.log('Plugin length is not greater then 0. Resolving.'); @@ -84,22 +85,37 @@ const marketPlaceSettings = { } plugins.forEach(async (plugin: Plugin) => { if (plugin.type === "page") { - const pluginScript = await fetch(`/packages/${plugin.name}/${plugin.src}`).then((res) => res.text()); + const pluginScript = await fetch(`/packages/${plugin.name.toLowerCase()}/${plugin.src}`).then((res) => res.text()); const script = eval(pluginScript); - const inject = await script() as unknown as SWPlugin; + const inject = await script() as unknown as SWPagePlugin; if (plugin.remove) { //@ts-ignore freaking types BRO - const plug = plugins.filter(({ name }) => name !== plugin.name); - swPlugins.push({remove: true, host: inject.host, html: inject.html, injectTo: inject.injectTo}); + const plug = plugins.filter(({ name }) => name !== plugin.name.toLowerCase()); + swPagePlugins.push({remove: true, host: inject.host, html: inject.html, injectTo: inject.injectTo, type: "page"}); localStorage.setItem(Settings.PluginSettings.plugins, JSON.stringify(plug)); } else { - swPlugins.push({host: inject.host, html: inject.html, injectTo: inject.injectTo}); + swPagePlugins.push({host: inject.host, html: inject.html, injectTo: inject.injectTo, type: "page"}); } //only resolve AFTER we have postMessaged to the SW. - worker.active?.postMessage(swPlugins); - resolve(); + worker.active?.postMessage(swPagePlugins); } + else if (plugin.type === "serviceWorker") { + const pluginScript = await fetch(`/packages/${plugin.name.toLowerCase()}/${plugin.src}`).then((res) => res.text()); + const script = eval(pluginScript); + const inject = await script() as unknown as SWPlugin; + if (plugin.remove) { + //@ts-ignore + const plug = plugins.filter(({ name }) => name !== plugin.name.toLowerCase()); + swPlugins.push({remove: true, function: inject.function.toString(), name: plugin.name, events: inject.events, type: "serviceWorker"}); + localStorage.setItem(Settings.PluginSettings.plugins, JSON.stringify(plug)); + } + else { + swPlugins.push({function: inject.function.toString(), name: plugin.name, events: inject.events, type: "serviceWorker"}); + } + worker.active?.postMessage(swPlugins); + } + resolve(); }); }); }, diff --git a/src/utils/settings/types.ts b/src/utils/settings/types.ts index 8a2f0b0..d9eb9b5 100644 --- a/src/utils/settings/types.ts +++ b/src/utils/settings/types.ts @@ -23,12 +23,17 @@ interface Plugin { type: PluginType; remove?: boolean; } -interface SWPlugin { +interface SWPagePlugin extends Omit { host: string; html: string; injectTo: "head" | "body"; - remove?: boolean; } + +interface SWPlugin extends Omit { + function: any; + events: []; +} + interface Package { theme?: { payload: string; @@ -48,6 +53,7 @@ export { type Package, type PluginType, type Plugin, + type SWPagePlugin, type SWPlugin, SearchEngines, type SearchEngine,