From 08e6c3bad8b584e6d1a8bac30c1ff8cb1f6142b3 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Fri, 25 Mar 2022 12:03:06 +0100 Subject: [PATCH] Refactoring --- common/manifest.ts | 4 +- package.json | 7 +- plugbox/environment/iframe_sandbox.ts | 6 +- plugbox/environment/node_sandbox.ts | 6 +- plugbox/environment/webworker_sandbox.ts | 6 +- plugbox/feature/endpoint.test.ts | 2 +- plugbox/feature/event.ts | 43 ++++++++ plugbox/plug.ts | 39 +++---- plugbox/plug_loader.ts | 6 +- plugbox/runtime.test.ts | 45 +++++++- plugbox/sandbox.ts | 12 ++- plugbox/syscall/fetch.node.ts | 15 +++ .../shell.ts => plugbox/syscall/shell.node.ts | 9 +- plugbox/syscall/store.dexie_browser.test.ts | 50 +++++++++ plugbox/syscall/store.dexie_browser.ts | 66 ++++++++++++ plugbox/syscall/store.knex_node.test.ts | 50 +++++++++ plugbox/syscall/store.knex_node.ts | 84 +++++++++++++++ plugbox/syscall/transport.ts | 16 +++ plugbox/system.ts | 73 ++++++++----- plugbox/types.ts | 7 +- plugs/core/core.plug.json | 10 -- plugs/core/dates.ts | 2 +- plugs/core/git.ts | 12 --- plugs/core/markup.ts | 2 +- plugs/core/navigate.ts | 36 ++----- plugs/core/page.ts | 13 +-- plugs/core/server.ts | 1 + plugs/core/task.ts | 2 +- plugs/core/word_count_command.ts | 2 +- plugs/git/git.plug.json | 37 +++++++ plugs/git/git.ts | 42 ++++++++ plugs/{core => }/lib/syscall.ts | 0 server/api_server.ts | 20 +++- server/index_api.ts | 17 +-- server/page_api.ts | 6 +- server/server.ts | 8 +- server/syscalls/page_index.ts | 25 ++--- tsconfig.json | 9 +- webapp/collab.ts | 3 +- webapp/components/top_bar.tsx | 15 +-- webapp/editor.tsx | 34 +++--- webapp/space.ts | 38 +------ webapp/styles/editor.scss | 1 - webapp/syscalls/db.localstorage.ts | 8 -- .../syscalls/{editor.browser.ts => editor.ts} | 33 +++--- webapp/syscalls/indexer.native.ts | 22 ---- webapp/syscalls/indexer.ts | 17 +++ webapp/syscalls/{space.native.ts => space.ts} | 14 +-- webapp/syscalls/system.ts | 13 +++ webapp/syscalls/ui.browser.ts | 20 ---- webapp/tsconfig.json | 12 --- yarn.lock | 102 +++++++++++++++++- 52 files changed, 796 insertions(+), 326 deletions(-) create mode 100644 plugbox/feature/event.ts create mode 100644 plugbox/syscall/fetch.node.ts rename server/syscalls/shell.ts => plugbox/syscall/shell.node.ts (54%) create mode 100644 plugbox/syscall/store.dexie_browser.test.ts create mode 100644 plugbox/syscall/store.dexie_browser.ts create mode 100644 plugbox/syscall/store.knex_node.test.ts create mode 100644 plugbox/syscall/store.knex_node.ts create mode 100644 plugbox/syscall/transport.ts delete mode 100644 plugs/core/git.ts create mode 100644 plugs/git/git.plug.json create mode 100644 plugs/git/git.ts rename plugs/{core => }/lib/syscall.ts (100%) delete mode 100644 webapp/syscalls/db.localstorage.ts rename webapp/syscalls/{editor.browser.ts => editor.ts} (78%) delete mode 100644 webapp/syscalls/indexer.native.ts create mode 100644 webapp/syscalls/indexer.ts rename webapp/syscalls/{space.native.ts => space.ts} (64%) create mode 100644 webapp/syscalls/system.ts delete mode 100644 webapp/syscalls/ui.browser.ts delete mode 100644 webapp/tsconfig.json diff --git a/common/manifest.ts b/common/manifest.ts index 1ba822c3..06187b26 100644 --- a/common/manifest.ts +++ b/common/manifest.ts @@ -1,6 +1,7 @@ import * as plugbox from "../plugbox/types"; import { EndpointHook } from "../plugbox/feature/endpoint"; import { CronHook } from "../plugbox/feature/node_cron"; +import { EventHook } from "../plugbox/feature/event"; export type CommandDef = { // Function name to invoke @@ -20,6 +21,7 @@ export type SilverBulletHooks = { [key: string]: CommandDef; }; } & EndpointHook & - CronHook; + CronHook & + EventHook; export type Manifest = plugbox.Manifest; diff --git a/package.json b/package.json index 35e7c1af..2b39a167 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "watch": "rm -rf .parcel-cache && parcel watch", "build": "parcel build", "clean": "rm -rf dist", - "plugs": "node dist/bundler/plugbox-bundle.js plugs/core/core.plug.json plugs/dist/core.plug.json", + "plugs": "node dist/bundler/plugbox-bundle.js plugs/core/core.plug.json plugs/dist/core.plug.json && node dist/bundler/plugbox-bundle.js plugs/git/git.plug.json plugs/dist/git.plug.json", "server": "nodemon -w dist/server dist/server/server.js pages", "test": "jest" }, @@ -41,6 +41,8 @@ "source": [ "plugbox/runtime.test.ts", "plugbox/feature/endpoint.test.ts", + "plugbox/syscall/store.knex_node.test.ts", + "plugbox/syscall/store.dexie_browser.test.ts", "server/api.test.ts" ], "outputFormat": "commonjs", @@ -72,11 +74,13 @@ "dexie": "^3.2.1", "esbuild": "^0.14.27", "express": "^4.17.3", + "fake-indexeddb": "^3.1.7", "idb": "^7.0.0", "jest": "^27.5.1", "knex": "^1.0.4", "lodash": "^4.17.21", "node-cron": "^3.0.0", + "node-fetch": "^3.2.3", "nodemon": "^2.0.15", "parcel": "^2.3.2", "react": "^17.0.2", @@ -99,6 +103,7 @@ "@types/jest": "^27.4.1", "@types/node": "^17.0.21", "@types/node-cron": "^3.0.1", + "@types/node-fetch": "^2.6.1", "@types/react": "^17.0.39", "@types/react-dom": "^17.0.11", "@types/supertest": "^2.0.11", diff --git a/plugbox/environment/iframe_sandbox.ts b/plugbox/environment/iframe_sandbox.ts index a4448c23..81e901e5 100644 --- a/plugbox/environment/iframe_sandbox.ts +++ b/plugbox/environment/iframe_sandbox.ts @@ -3,8 +3,8 @@ import { safeRun } from "../util"; // @ts-ignore import sandboxHtml from "bundle-text:./iframe_sandbox.html"; import { Sandbox } from "../sandbox"; -import { System } from "../system"; import { WorkerLike } from "./worker"; +import { Plug } from "../plug"; class IFrameWrapper implements WorkerLike { private iframe: HTMLIFrameElement; @@ -49,6 +49,6 @@ class IFrameWrapper implements WorkerLike { } } -export function createSandbox(system: System) { - return new Sandbox(system, new IFrameWrapper()); +export function createSandbox(plug: Plug) { + return new Sandbox(plug, new IFrameWrapper()); } diff --git a/plugbox/environment/node_sandbox.ts b/plugbox/environment/node_sandbox.ts index 7de76e50..f51b99a3 100644 --- a/plugbox/environment/node_sandbox.ts +++ b/plugbox/environment/node_sandbox.ts @@ -4,8 +4,8 @@ import { safeRun } from "../util"; // @ts-ignore import workerCode from "bundle-text:./node_worker.ts"; import { Sandbox } from "../sandbox"; -import { System } from "../system"; import { WorkerLike } from "./worker"; +import { Plug } from "../plug"; class NodeWorkerWrapper implements WorkerLike { onMessage?: (message: any) => Promise; @@ -33,12 +33,12 @@ class NodeWorkerWrapper implements WorkerLike { } } -export function createSandbox(system: System) { +export function createSandbox(plug: Plug) { let worker = new Worker(workerCode, { eval: true, }); return new Sandbox( - system, + plug, new NodeWorkerWrapper( new Worker(workerCode, { eval: true, diff --git a/plugbox/environment/webworker_sandbox.ts b/plugbox/environment/webworker_sandbox.ts index e7be30b0..307ae791 100644 --- a/plugbox/environment/webworker_sandbox.ts +++ b/plugbox/environment/webworker_sandbox.ts @@ -1,7 +1,7 @@ import { safeRun } from "../util"; import { Sandbox } from "../sandbox"; -import { System } from "../system"; import { WorkerLike } from "./worker"; +import { Plug } from "../plug"; class WebWorkerWrapper implements WorkerLike { private worker: Worker; @@ -28,10 +28,10 @@ class WebWorkerWrapper implements WorkerLike { } } -export function createSandbox(system: System) { +export function createSandbox(plug: Plug) { // ParcelJS will build this file into a worker. let worker = new Worker(new URL("sandbox_worker.ts", import.meta.url), { type: "module", }); - return new Sandbox(system, new WebWorkerWrapper(worker)); + return new Sandbox(plug, new WebWorkerWrapper(worker)); } diff --git a/plugbox/feature/endpoint.test.ts b/plugbox/feature/endpoint.test.ts index 58bfbdcd..86016ee2 100644 --- a/plugbox/feature/endpoint.test.ts +++ b/plugbox/feature/endpoint.test.ts @@ -27,7 +27,7 @@ test("Run a plugbox endpoint server", async () => { endpoints: [{ method: "GET", path: "/", handler: "testhandler" }], }, } as Manifest, - createSandbox(system) + createSandbox ); const app = express(); diff --git a/plugbox/feature/event.ts b/plugbox/feature/event.ts new file mode 100644 index 00000000..54cc0409 --- /dev/null +++ b/plugbox/feature/event.ts @@ -0,0 +1,43 @@ +import { Feature, Manifest } from "../types"; +import { System } from "../system"; + +export type EventHook = { + events?: { [key: string]: string[] }; +}; + +export class EventFeature implements Feature { + private system?: System; + + async dispatchEvent(name: string, data?: any): Promise { + if (!this.system) { + throw new Error("EventFeature is not initialized"); + } + let promises: Promise[] = []; + for (const plug of this.system.loadedPlugs.values()) { + if (!plug.manifest!.hooks?.events) { + continue; + } + let functionsToSpawn = plug.manifest!.hooks.events[name]; + if (functionsToSpawn) { + functionsToSpawn.forEach((functionToSpawn) => { + // Only dispatch functions on events when they're allowed to be invoked in this environment + if (plug.canInvoke(functionToSpawn)) { + promises.push(plug.invoke(functionToSpawn, [data])); + } + }); + } + } + return Promise.all(promises); + } + + apply(system: System): void { + this.system = system; + system.on({ + plugLoaded: (name, plug) => {}, + }); + } + + validateManifest(manifest: Manifest): string[] { + return []; + } +} diff --git a/plugbox/plug.ts b/plugbox/plug.ts index f8688d91..c15c1c13 100644 --- a/plugbox/plug.ts +++ b/plugbox/plug.ts @@ -7,16 +7,28 @@ export class Plug { sandbox: Sandbox; public manifest?: Manifest; readonly runtimeEnv: RuntimeEnvironment; + grantedPermissions: string[] = []; + name: string; - constructor(system: System, name: string, sandbox: Sandbox) { + constructor( + system: System, + name: string, + sandboxFactory: (plug: Plug) => Sandbox + ) { this.system = system; - this.sandbox = sandbox; + this.name = name; + this.sandbox = sandboxFactory(this); this.runtimeEnv = system.runtimeEnv; } async load(manifest: Manifest) { this.manifest = manifest; - await this.dispatchEvent("load"); + // TODO: These need to be explicitly granted, not just taken + this.grantedPermissions = manifest.requiredPermissions || []; + } + + syscall(name: string, args: any[]): Promise { + return this.system.syscallWithContext({ plug: this }, name, args); } canInvoke(name: string) { @@ -46,27 +58,6 @@ export class Plug { return await this.sandbox.invoke(name, args); } - async dispatchEvent(name: string, data?: any): Promise { - if (!this.manifest!.hooks?.events) { - return []; - } - let functionsToSpawn = this.manifest!.hooks.events[name]; - if (functionsToSpawn) { - return await Promise.all( - functionsToSpawn.map((functionToSpawn: string) => { - // Only dispatch functions on events when they're allowed to be invoked in this environment - if (this.canInvoke(functionToSpawn)) { - return this.invoke(functionToSpawn, [data]); - } else { - return Promise.resolve(); - } - }) - ); - } else { - return []; - } - } - async stop() { this.sandbox.stop(); } diff --git a/plugbox/plug_loader.ts b/plugbox/plug_loader.ts index f32e6085..ff88a8bd 100644 --- a/plugbox/plug_loader.ts +++ b/plugbox/plug_loader.ts @@ -20,9 +20,7 @@ export class DiskPlugLoader { watcher() { safeRun(async () => { - for await (const { filename, eventType } of watch(this.plugPath, { - recursive: true, - })) { + for await (const { filename, eventType } of watch(this.plugPath)) { if (!filename.endsWith(".plug.json")) { return; } @@ -50,7 +48,7 @@ export class DiskPlugLoader { console.log("Now loading plug", plugName); try { const plugDef = JSON.parse(plug); - await this.system.load(plugName, plugDef, createSandbox(this.system)); + await this.system.load(plugName, plugDef, createSandbox); return plugDef; } catch (e) { console.error("Could not parse plugin file", e); diff --git a/plugbox/runtime.test.ts b/plugbox/runtime.test.ts index afe35e08..1e3682c9 100644 --- a/plugbox/runtime.test.ts +++ b/plugbox/runtime.test.ts @@ -4,17 +4,28 @@ import { System } from "./system"; test("Run a Node sandbox", async () => { let system = new System("server"); - system.registerSyscalls({ - addNumbers: (a, b) => { + system.registerSyscalls("", [], { + addNumbers: (ctx, a, b) => { return a + b; }, failingSyscall: () => { throw new Error("#fail"); }, }); + system.registerSyscalls("", ["restricted"], { + restrictedSyscall: () => { + return "restricted"; + }, + }); + system.registerSyscalls("", ["dangerous"], { + dangerousSyscall: () => { + return "yay"; + }, + }); let plug = await system.load( "test", { + requiredPermissions: ["dangerous"], functions: { addTen: { code: `(() => { @@ -52,12 +63,30 @@ test("Run a Node sandbox", async () => { }; })()`, }, + restrictedTest: { + code: `(() => { + return { + default: async () => { + await self.syscall("restrictedSyscall"); + } + }; + })()`, + }, + dangerousTest: { + code: `(() => { + return { + default: async () => { + return await self.syscall("dangerousSyscall"); + } + }; + })()`, + }, }, hooks: { events: {}, }, }, - createSandbox(system) + createSandbox ); expect(await plug.invoke("addTen", [10])).toBe(20); for (let i = 0; i < 100; i++) { @@ -75,5 +104,15 @@ test("Run a Node sandbox", async () => { } catch (e: any) { expect(e.message).toBe("#fail"); } + try { + await plug.invoke("restrictedTest", []); + expect(true).toBe(false); + } catch (e: any) { + expect(e.message).toBe( + "Missing permission 'restricted' for syscall restrictedSyscall" + ); + } + expect(await plug.invoke("dangerousTest", [])).toBe("yay"); + await system.unloadAll(); }); diff --git a/plugbox/sandbox.ts b/plugbox/sandbox.ts index 3b32f684..8da85cb0 100644 --- a/plugbox/sandbox.ts +++ b/plugbox/sandbox.ts @@ -1,9 +1,11 @@ -import { System } from "./system"; import { ControllerMessage, WorkerLike, WorkerMessage, } from "./environment/worker"; +import { Plug } from "./plug"; + +export type SandboxFactory = (plug: Plug) => Sandbox; export class Sandbox { protected worker: WorkerLike; @@ -14,12 +16,12 @@ export class Sandbox { { resolve: (result: any) => void; reject: (e: any) => void } >(); protected loadedFunctions = new Set(); - protected system: System; + protected plug: Plug; - constructor(system: System, worker: WorkerLike) { + constructor(plug: Plug, worker: WorkerLike) { worker.onMessage = this.onMessage.bind(this); this.worker = worker; - this.system = system; + this.plug = plug; } isLoaded(name: string) { @@ -48,7 +50,7 @@ export class Sandbox { break; case "syscall": try { - let result = await this.system.syscall(data.name!, data.args!); + let result = await this.plug.syscall(data.name!, data.args!); this.worker.postMessage({ type: "syscall-response", diff --git a/plugbox/syscall/fetch.node.ts b/plugbox/syscall/fetch.node.ts new file mode 100644 index 00000000..0bb1b2f5 --- /dev/null +++ b/plugbox/syscall/fetch.node.ts @@ -0,0 +1,15 @@ +import fetch, { RequestInfo, RequestInit } from "node-fetch"; +import { SysCallMapping } from "../system"; + +export function fetchSyscalls(): SysCallMapping { + return { + async fetchJson(ctx, url: RequestInfo, init: RequestInit) { + let resp = await fetch(url, init); + return resp.json(); + }, + async fetchText(ctx, url: RequestInfo, init: RequestInit) { + let resp = await fetch(url, init); + return resp.text(); + }, + }; +} diff --git a/server/syscalls/shell.ts b/plugbox/syscall/shell.node.ts similarity index 54% rename from server/syscalls/shell.ts rename to plugbox/syscall/shell.node.ts index 5c3c1d62..3b3fd3a1 100644 --- a/server/syscalls/shell.ts +++ b/plugbox/syscall/shell.node.ts @@ -1,11 +1,16 @@ import { promisify } from "util"; import { execFile } from "child_process"; +import type { SysCallMapping } from "../system"; const execFilePromise = promisify(execFile); -export default function (cwd: string) { +export default function (cwd: string): SysCallMapping { return { - "shell.run": async (cmd: string, args: string[]) => { + run: async ( + ctx, + cmd: string, + args: string[] + ): Promise<{ stdout: string; stderr: string }> => { let { stdout, stderr } = await execFilePromise(cmd, args, { cwd: cwd, }); diff --git a/plugbox/syscall/store.dexie_browser.test.ts b/plugbox/syscall/store.dexie_browser.test.ts new file mode 100644 index 00000000..4b15cd38 --- /dev/null +++ b/plugbox/syscall/store.dexie_browser.test.ts @@ -0,0 +1,50 @@ +import { createSandbox } from "../environment/node_sandbox"; +import { expect, test } from "@jest/globals"; +import { System } from "../system"; +import { storeSyscalls } from "./store.dexie_browser"; + +// For testing in node.js +require("fake-indexeddb/auto"); + +test("Test store", async () => { + let system = new System("server"); + system.registerSyscalls("store", [], storeSyscalls("test", "test")); + let plug = await system.load( + "test", + { + hooks: {}, + functions: { + test1: { + code: `(() => { + return { + default: async () => { + await self.syscall("store.set", "name", "Pete"); + return await self.syscall("store.get", "name"); + } + }; + })()`, + }, + test2: { + code: `(() => { + return { + default: async () => { + await self.syscall("store.set", "page1:bl:page2:10", {title: "Something", meta: 20}); + await self.syscall("store.batchSet", [ + {key: "page2:bl:page3", value: {title: "Something2", meta: 10}}, + {key: "page2:bl:page4", value: {title: "Something3", meta: 10}}, + ]); + return await self.syscall("store.queryPrefix", "page2:"); + } + }; + })()`, + }, + }, + }, + createSandbox + ); + expect(await plug.invoke("test1", [])).toBe("Pete"); + let queryResults = await plug.invoke("test2", []); + expect(queryResults.length).toBe(2); + expect(queryResults[0].value.meta).toBe(10); + await system.unloadAll(); +}); diff --git a/plugbox/syscall/store.dexie_browser.ts b/plugbox/syscall/store.dexie_browser.ts new file mode 100644 index 00000000..125ae1d4 --- /dev/null +++ b/plugbox/syscall/store.dexie_browser.ts @@ -0,0 +1,66 @@ +import Dexie from "dexie"; +import { SysCallMapping } from "../system"; + +export type KV = { + key: string; + value: any; +}; + +export function storeSyscalls( + dbName: string, + tableName: string +): SysCallMapping { + const db = new Dexie(dbName); + db.version(1).stores({ + test: "key", + }); + const items = db.table(tableName); + + return { + async delete(ctx, key: string) { + await items.delete(key); + }, + + async deletePrefix(ctx, prefix: string) { + await items.where("key").startsWith(prefix).delete(); + }, + + async deleteAll() { + await items.clear(); + }, + + async set(ctx, key: string, value: any) { + await items.put({ + key, + value, + }); + }, + + async batchSet(ctx, kvs: KV[]) { + await items.bulkPut( + kvs.map(({ key, value }) => ({ + key, + value, + })) + ); + }, + + async get(ctx, key: string): Promise { + let result = await items.get({ + key, + }); + return result ? result.value : null; + }, + + async queryPrefix( + ctx, + keyPrefix: string + ): Promise<{ key: string; value: any }[]> { + let results = await items.where("key").startsWith(keyPrefix).toArray(); + return results.map((result) => ({ + key: result.key, + value: result.value, + })); + }, + }; +} diff --git a/plugbox/syscall/store.knex_node.test.ts b/plugbox/syscall/store.knex_node.test.ts new file mode 100644 index 00000000..2d142acf --- /dev/null +++ b/plugbox/syscall/store.knex_node.test.ts @@ -0,0 +1,50 @@ +import { createSandbox } from "../environment/node_sandbox"; +import { expect, test } from "@jest/globals"; +import { System } from "../system"; +import { + ensureTable, + storeReadSyscalls, + storeWriteSyscalls, +} from "./store.knex_node"; +import knex from "knex"; +import fs from "fs/promises"; + +test("Test store", async () => { + const db = knex({ + client: "better-sqlite3", + connection: { + filename: "test.db", + }, + useNullAsDefault: true, + }); + await ensureTable(db, "test_table"); + let system = new System("server"); + system.registerSyscalls( + "store", + [], + storeWriteSyscalls(db, "test_table"), + storeReadSyscalls(db, "test_table") + ); + let plug = await system.load( + "test", + { + hooks: {}, + functions: { + test1: { + code: `(() => { + return { + default: async () => { + await self.syscall("store.set", "name", "Pete"); + return await self.syscall("store.get", "name"); + } + }; + })()`, + }, + }, + }, + createSandbox + ); + expect(await plug.invoke("test1", [])).toBe("Pete"); + await system.unloadAll(); + await fs.unlink("test.db"); +}); diff --git a/plugbox/syscall/store.knex_node.ts b/plugbox/syscall/store.knex_node.ts new file mode 100644 index 00000000..d8d8961f --- /dev/null +++ b/plugbox/syscall/store.knex_node.ts @@ -0,0 +1,84 @@ +import { Knex } from "knex"; +import { SysCallMapping } from "../system"; + +type Item = { + page: string; + key: string; + value: any; +}; + +export type KV = { + key: string; + value: any; +}; + +export async function ensureTable(db: Knex, tableName: string) { + if (!(await db.schema.hasTable(tableName))) { + await db.schema.createTable(tableName, (table) => { + table.string("key"); + table.text("value"); + table.primary(["key"]); + }); + console.log(`Created table ${tableName}`); + } +} + +export function storeWriteSyscalls( + db: Knex, + tableName: string +): SysCallMapping { + const apiObj: SysCallMapping = { + delete: async (ctx, page: string, key: string) => { + await db(tableName).where({ page, key }).del(); + }, + deletePrefix: async (ctx, prefix: string) => { + return db(tableName).andWhereLike("key", `${prefix}%`).del(); + }, + deleteAll: async (ctx) => { + await db(tableName).del(); + }, + set: async (ctx, key: string, value: any) => { + let changed = await db(tableName) + .where({ key }) + .update("value", JSON.stringify(value)); + if (changed === 0) { + await db(tableName).insert({ + key, + value: JSON.stringify(value), + }); + } + }, + batchSet: async (ctx, kvs: KV[]) => { + for (let { key, value } of kvs) { + await apiObj["store.set"](ctx, key, value); + } + }, + }; + return apiObj; +} + +export function storeReadSyscalls( + db: Knex, + tableName: string +): SysCallMapping { + return { + get: async (ctx, key: string): Promise => { + let result = await db(tableName).where({ key }).select("value"); + if (result.length) { + return JSON.parse(result[0].value); + } else { + return null; + } + }, + queryPrefix: async (ctx, prefix: string) => { + return ( + await db(tableName) + .andWhereLike("key", `${prefix}%`) + .select("key", "value") + ).map(({ key, value }) => ({ + key, + value: JSON.parse(value), + })); + }, + }; +} diff --git a/plugbox/syscall/transport.ts b/plugbox/syscall/transport.ts new file mode 100644 index 00000000..dcfe9f78 --- /dev/null +++ b/plugbox/syscall/transport.ts @@ -0,0 +1,16 @@ +import { SysCallMapping } from "../system"; + +export function transportSyscalls( + names: string[], + transportCall: (name: string, ...args: any[]) => Promise +): SysCallMapping { + let syscalls: SysCallMapping = {}; + + for (let name of names) { + syscalls[name] = (ctx, ...args: any[]) => { + return transportCall(name, ...args); + }; + } + + return syscalls; +} diff --git a/plugbox/system.ts b/plugbox/system.ts index 08fb5f2d..f99c4f9f 100644 --- a/plugbox/system.ts +++ b/plugbox/system.ts @@ -1,10 +1,10 @@ import { Feature, Manifest, RuntimeEnvironment } from "./types"; import { EventEmitter } from "../common/event"; -import { Sandbox } from "./sandbox"; +import { SandboxFactory } from "./sandbox"; import { Plug } from "./plug"; export interface SysCallMapping { - [key: string]: (...args: any) => Promise | any; + [key: string]: (ctx: SyscallContext, ...args: any) => Promise | any; } export type SystemJSON = { [key: string]: Manifest }; @@ -14,9 +14,23 @@ export type SystemEvents = { plugUnloaded: (name: string, plug: Plug) => void; }; +type SyscallContext = { + plug: Plug | null; +}; + +type SyscallSignature = ( + ctx: SyscallContext, + ...args: any[] +) => Promise | any; + +type Syscall = { + requiredPermissions: string[]; + callback: SyscallSignature; +}; + export class System extends EventEmitter> { protected plugs = new Map>(); - registeredSyscalls: SysCallMapping = {}; + protected registeredSyscalls = new Map(); protected enabledFeatures = new Set>(); readonly runtimeEnv: RuntimeEnvironment; @@ -31,29 +45,46 @@ export class System extends EventEmitter> { feature.apply(this); } - registerSyscalls(...registrationObjects: SysCallMapping[]) { + registerSyscalls( + namespace: string, + requiredCapabilities: string[], + ...registrationObjects: SysCallMapping[] + ) { for (const registrationObject of registrationObjects) { - for (let [name, def] of Object.entries(registrationObject)) { - this.registeredSyscalls[name] = def; + for (let [name, callback] of Object.entries(registrationObject)) { + const callName = namespace ? `${namespace}.${name}` : name; + this.registeredSyscalls.set(callName, { + requiredPermissions: requiredCapabilities, + callback, + }); } } } - async syscall(name: string, args: any[]): Promise { - const callback = this.registeredSyscalls[name]; - if (!name) { + async syscallWithContext( + ctx: SyscallContext, + name: string, + args: any[] + ): Promise { + const syscall = this.registeredSyscalls.get(name); + if (!syscall) { throw Error(`Unregistered syscall ${name}`); } - if (!callback) { - throw Error(`Registered but not implemented syscall ${name}`); + for (const permission of syscall.requiredPermissions) { + if (!ctx.plug) { + throw Error(`Syscall ${name} requires permission and no plug is set`); + } + if (!ctx.plug.grantedPermissions.includes(permission)) { + throw Error(`Missing permission '${permission}' for syscall ${name}`); + } } - return Promise.resolve(callback(...args)); + return Promise.resolve(syscall.callback(ctx, ...args)); } async load( name: string, manifest: Manifest, - sandbox: Sandbox + sandboxFactory: SandboxFactory ): Promise> { if (this.plugs.has(name)) { await this.unload(name); @@ -67,7 +98,7 @@ export class System extends EventEmitter> { throw new Error(`Invalid manifest: ${errors.join(", ")}`); } // Ok, let's load this thing! - const plug = new Plug(this, name, sandbox); + const plug = new Plug(this, name, sandboxFactory); await plug.load(manifest); this.plugs.set(name, plug); this.emit("plugLoaded", name, plug); @@ -84,16 +115,6 @@ export class System extends EventEmitter> { this.plugs.delete(name); } - async dispatchEvent(name: string, data?: any): Promise { - let promises = []; - for (let plug of this.plugs.values()) { - for (let result of await plug.dispatchEvent(name, data)) { - promises.push(result); - } - } - return await Promise.all(promises); - } - get loadedPlugs(): Map> { return this.plugs; } @@ -111,12 +132,12 @@ export class System extends EventEmitter> { async replaceAllFromJSON( json: SystemJSON, - sandboxFactory: () => Sandbox + sandboxFactory: SandboxFactory ) { await this.unloadAll(); for (let [name, manifest] of Object.entries(json)) { console.log("Loading plug", name); - await this.load(name, manifest, sandboxFactory()); + await this.load(name, manifest, sandboxFactory); } } diff --git a/plugbox/types.ts b/plugbox/types.ts index 082c6e23..a6d2963d 100644 --- a/plugbox/types.ts +++ b/plugbox/types.ts @@ -1,7 +1,8 @@ import { System } from "./system"; export interface Manifest { - hooks: HookT & EventHook; + requiredPermissions?: string[]; + hooks: HookT; functions: { [key: string]: FunctionDef; }; @@ -15,10 +16,6 @@ export interface FunctionDef { export type RuntimeEnvironment = "client" | "server"; -export type EventHook = { - events?: { [key: string]: string[] }; -}; - export interface Feature { validateManifest(manifest: Manifest): string[]; diff --git a/plugs/core/core.plug.json b/plugs/core/core.plug.json index 196f65f1..32f8e2a2 100644 --- a/plugs/core/core.plug.json +++ b/plugs/core/core.plug.json @@ -45,12 +45,6 @@ "path": "/", "handler": "endpointTest" } - ], - "crons": [ - { - "cron": "*/15 * * * *", - "handler": "gitSnapshot" - } ] }, "functions": { @@ -96,10 +90,6 @@ "welcome": { "path": "./server.ts:welcome", "env": "server" - }, - "gitSnapshot": { - "path": "./git.ts:commit", - "env": "server" } } } diff --git a/plugs/core/dates.ts b/plugs/core/dates.ts index e653901b..0692f6aa 100644 --- a/plugs/core/dates.ts +++ b/plugs/core/dates.ts @@ -1,4 +1,4 @@ -import { syscall } from "./lib/syscall"; +import { syscall } from "../lib/syscall"; export async function insertToday() { console.log("Inserting date"); diff --git a/plugs/core/git.ts b/plugs/core/git.ts deleted file mode 100644 index ae89e7a1..00000000 --- a/plugs/core/git.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { syscall } from "./lib/syscall"; - -export async function commit() { - console.log("Snapshotting the current space to git"); - await syscall("shell.run", "git", ["add", "./*.md"]); - try { - await syscall("shell.run", "git", ["commit", "-a", "-m", "Snapshot"]); - } catch (e) { - // We can ignore, this happens when there's no changes to commit - } - console.log("Done!"); -} diff --git a/plugs/core/markup.ts b/plugs/core/markup.ts index a371553b..5e30cb2e 100644 --- a/plugs/core/markup.ts +++ b/plugs/core/markup.ts @@ -1,4 +1,4 @@ -import { syscall } from "./lib/syscall"; +import { syscall } from "../lib/syscall"; export async function toggleH1() { await togglePrefix("# "); diff --git a/plugs/core/navigate.ts b/plugs/core/navigate.ts index 362dfdd1..530c7ccd 100644 --- a/plugs/core/navigate.ts +++ b/plugs/core/navigate.ts @@ -1,5 +1,5 @@ -import { ClickEvent } from "../../webapp/src/app_event"; -import { syscall } from "./lib/syscall"; +import { ClickEvent } from "../../webapp/app_event"; +import { syscall } from "../lib/syscall"; async function navigate(syntaxNode: any) { if (!syntaxNode) { @@ -8,7 +8,6 @@ async function navigate(syntaxNode: any) { console.log("Attempting to navigate based on syntax node", syntaxNode); switch (syntaxNode.name) { case "WikiLinkPage": - case "AtMention": await syscall("editor.navigate", syntaxNode.text); break; case "URL": @@ -36,31 +35,16 @@ export async function clickNavigate(event: ClickEvent) { } export async function pageComplete() { - let prefix = await syscall( - "editor.matchBefore", - "(\\[\\[[\\w\\s]*|@[\\w\\.]*)" - ); + let prefix = await syscall("editor.matchBefore", "\\[\\[[\\w\\s]*"); if (!prefix) { return null; } let allPages = await syscall("space.listPages"); - if (prefix.text[0] === "@") { - return { - from: prefix.from, - options: allPages - .filter((page: any) => page.name.startsWith(prefix.text)) - .map((pageMeta: any) => ({ - label: pageMeta.name, - type: "page", - })), - }; - } else { - return { - from: prefix.from + 2, - options: allPages.map((pageMeta: any) => ({ - label: pageMeta.name, - type: "page", - })), - }; - } + return { + from: prefix.from + 2, + options: allPages.map((pageMeta: any) => ({ + label: pageMeta.name, + type: "page", + })), + }; } diff --git a/plugs/core/page.ts b/plugs/core/page.ts index 7fae98f9..3c6866cc 100644 --- a/plugs/core/page.ts +++ b/plugs/core/page.ts @@ -1,13 +1,13 @@ import { IndexEvent } from "../../webapp/app_event"; import { pageLinkRegex } from "../../webapp/constant"; -import { syscall } from "./lib/syscall"; +import { syscall } from "../lib/syscall"; const wikilinkRegex = new RegExp(pageLinkRegex, "g"); -const atMentionRegex = /(@[A-Za-z\.]+)/g; export async function indexLinks({ name, text }: IndexEvent) { let backLinks: { key: string; value: string }[] = []; // [[Style Links]] + for (let match of text.matchAll(wikilinkRegex)) { let toPage = match[1]; let pos = match.index!; @@ -16,15 +16,6 @@ export async function indexLinks({ name, text }: IndexEvent) { value: name, }); } - // @links - for (let match of text.matchAll(atMentionRegex)) { - let toPage = match[1]; - let pos = match.index!; - backLinks.push({ - key: `pl:${toPage}:${pos}`, - value: name, - }); - } console.log("Found", backLinks.length, "wiki link(s)"); // throw Error("Boom"); await syscall("indexer.batchSet", name, backLinks); diff --git a/plugs/core/server.ts b/plugs/core/server.ts index 3f3d2224..8eec1a9e 100644 --- a/plugs/core/server.ts +++ b/plugs/core/server.ts @@ -13,4 +13,5 @@ export function endpointTest(req: EndpointRequest): EndpointResponse { export function welcome() { console.log("Hello world!"); + return "hi"; } diff --git a/plugs/core/task.ts b/plugs/core/task.ts index 33ecd0c2..70cfce5a 100644 --- a/plugs/core/task.ts +++ b/plugs/core/task.ts @@ -1,5 +1,5 @@ import { ClickEvent } from "../../webapp/src/app_event"; -import { syscall } from "./lib/syscall"; +import { syscall } from "../lib/syscall"; export async function taskToggle(event: ClickEvent) { let syntaxNode = await syscall("editor.getSyntaxNodeAtPos", event.pos); diff --git a/plugs/core/word_count_command.ts b/plugs/core/word_count_command.ts index e7720c6a..9780f251 100644 --- a/plugs/core/word_count_command.ts +++ b/plugs/core/word_count_command.ts @@ -1,4 +1,4 @@ -import { syscall } from "./lib/syscall"; +import { syscall } from "../lib/syscall"; function countWords(str: string): number { var matches = str.match(/[\w\d\'\'-]+/gi); diff --git a/plugs/git/git.plug.json b/plugs/git/git.plug.json new file mode 100644 index 00000000..ccc2f8cd --- /dev/null +++ b/plugs/git/git.plug.json @@ -0,0 +1,37 @@ +{ + "requiredPermissions": ["shell"], + "hooks": { + "commands": { + "Git: Snapshot": { + "invoke": "snapshotCommand" + }, + "Git: Sync": { + "invoke": "syncCommand" + } + }, + "crons": [ + { + "cron": "*/15 * * * *", + "handler": "commit" + } + ] + }, + "functions": { + "snapshotCommand": { + "path": "./git.ts:snapshotCommand", + "env": "client" + }, + "syncCommand": { + "path": "./git.ts:syncCommand", + "env": "client" + }, + "commit": { + "path": "./git.ts:commit", + "env": "server" + }, + "sync": { + "path": "./git.ts:sync", + "env": "server" + } + } +} diff --git a/plugs/git/git.ts b/plugs/git/git.ts new file mode 100644 index 00000000..bd50452f --- /dev/null +++ b/plugs/git/git.ts @@ -0,0 +1,42 @@ +import { syscall } from "../lib/syscall"; + +export async function commit(message?: string) { + if (!message) { + message = "Snapshot"; + } + console.log( + "Snapshotting the current space to git with commit message", + message + ); + await syscall("shell.run", "git", ["add", "./*.md"]); + try { + await syscall("shell.run", "git", ["commit", "-a", "-m", message]); + } catch (e) { + // We can ignore, this happens when there's no changes to commit + } + console.log("Done!"); +} + +export async function snapshotCommand() { + let revName = await syscall("editor.prompt", `Revision name:`); + if (!revName) { + revName = "Snapshot"; + } + console.log("Revision name", revName); + await syscall("system.invokeFunctionOnServer", "commit", revName); +} + +export async function syncCommand() { + await syscall("system.invokeFunctionOnServer", "sync"); +} + +export async function sync() { + console.log("Going to sync with git"); + console.log("First locally committing everything"); + await commit(); + console.log("Then pulling from remote"); + await syscall("shell.run", "git", ["pull"]); + console.log("And then pushing to remote"); + await syscall("shell.run", "git", ["push"]); + console.log("Done!"); +} diff --git a/plugs/core/lib/syscall.ts b/plugs/lib/syscall.ts similarity index 100% rename from plugs/core/lib/syscall.ts rename to plugs/lib/syscall.ts diff --git a/server/api_server.ts b/server/api_server.ts index 839ce290..278a3a5b 100644 --- a/server/api_server.ts +++ b/server/api_server.ts @@ -46,7 +46,7 @@ export class SocketServer { public async init() { const indexApi = new IndexApi(this.rootPath); await this.registerApi("index", indexApi); - this.system.registerSyscalls(pageIndexSyscalls(indexApi.db)); + this.system.registerSyscalls("indexer", [], pageIndexSyscalls(indexApi.db)); await this.registerApi( "page", new PageApi( @@ -118,6 +118,24 @@ export class SocketServer { }); } + onCall( + "invokeFunction", + (plugName: string, name: string, ...args: any[]): Promise => { + let plug = this.system.loadedPlugs.get(plugName); + if (!plug) { + throw new Error(`Plug ${plugName} not loaded`); + } + console.log( + "Invoking function", + name, + "for plug", + plugName, + "as requested over socket" + ); + return plug.invoke(name, args); + } + ); + console.log("Sending the sytem to the client"); socket.emit("loadSystem", this.system.toJSON()); }); diff --git a/server/index_api.ts b/server/index_api.ts index d54a8192..8dd2c399 100644 --- a/server/index_api.ts +++ b/server/index_api.ts @@ -36,12 +36,13 @@ export class IndexApi implements ApiProvider { api() { const syscalls = pageIndexSyscalls(this.db); + const nullContext = { plug: null }; return { clearPageIndexForPage: async ( clientConn: ClientConnection, page: string ) => { - return syscalls["indexer.clearPageIndexForPage"](page); + return syscalls.clearPageIndexForPage(nullContext, page); }, set: async ( clientConn: ClientConnection, @@ -49,41 +50,41 @@ export class IndexApi implements ApiProvider { key: string, value: any ) => { - return syscalls["indexer.set"](page, key, value); + return syscalls.set(nullContext, page, key, value); }, get: async (clientConn: ClientConnection, page: string, key: string) => { - return syscalls["indexer.get"](page, key); + return syscalls.get(nullContext, page, key); }, delete: async ( clientConn: ClientConnection, page: string, key: string ) => { - return syscalls["indexer.delete"](page, key); + return syscalls.delete(nullContext, page, key); }, scanPrefixForPage: async ( clientConn: ClientConnection, page: string, prefix: string ) => { - return syscalls["indexer.scanPrefixForPage"](page, prefix); + return syscalls.scanPrefixForPage(nullContext, page, prefix); }, scanPrefixGlobal: async ( clientConn: ClientConnection, prefix: string ) => { - return syscalls["indexer.scanPrefixGlobal"](prefix); + return syscalls.scanPrefixGlobal(nullContext, prefix); }, deletePrefixForPage: async ( clientConn: ClientConnection, page: string, prefix: string ) => { - return syscalls["indexer.deletePrefixForPage"](page, prefix); + return syscalls.deletePrefixForPage(nullContext, page, prefix); }, clearPageIndex: async (clientConn: ClientConnection) => { - return syscalls["indexer.clearPageIndex"](); + return syscalls.clearPageIndex(nullContext); }, }; } diff --git a/server/page_api.ts b/server/page_api.ts index d44ef135..66202bb1 100644 --- a/server/page_api.ts +++ b/server/page_api.ts @@ -11,6 +11,7 @@ import { stat } from "fs/promises"; import { Cursor, cursorEffect } from "../webapp/cursorEffect"; import { SilverBulletHooks } from "../common/manifest"; import { System } from "../plugbox/system"; +import { EventFeature } from "../plugbox/feature/event"; export class PageApi implements ApiProvider { openPages: Map; @@ -18,6 +19,7 @@ export class PageApi implements ApiProvider { rootPath: string; connectedSockets: Set; private system: System; + private eventFeature: EventFeature; constructor( rootPath: string, @@ -30,6 +32,8 @@ export class PageApi implements ApiProvider { this.openPages = openPages; this.connectedSockets = connectedSockets; this.system = system; + this.eventFeature = new EventFeature(); + system.addFeature(this.eventFeature); } async init(): Promise { @@ -222,7 +226,7 @@ export class PageApi implements ApiProvider { ); await this.flushPageToDisk(pageName, page); - await this.system.dispatchEvent("page:index", { + await this.eventFeature.dispatchEvent("page:index", { name: pageName, text: page.text.sliceString(0), }); diff --git a/server/server.ts b/server/server.ts index 750f6200..b6b4796f 100644 --- a/server/server.ts +++ b/server/server.ts @@ -8,7 +8,7 @@ import { SilverBulletHooks } from "../common/manifest"; import { ExpressServer } from "./express_server"; import { DiskPlugLoader } from "../plugbox/plug_loader"; import { NodeCronFeature } from "../plugbox/feature/node_cron"; -import shellSyscalls from "./syscalls/shell"; +import shellSyscalls from "../plugbox/syscall/shell.node"; import { System } from "../plugbox/system"; let args = yargs(hideBin(process.argv)) @@ -53,9 +53,9 @@ expressServer `${__dirname}/../../plugs/dist` ); await plugLoader.loadPlugs(); - plugLoader.watcher(); - system.registerSyscalls(shellSyscalls(pagesPath)); - system.addFeature(new NodeCronFeature()); + plugLoader.watcher(); + system.registerSyscalls("shell", ["shell"], shellSyscalls(pagesPath)); + system.addFeature(new NodeCronFeature()); server.listen(port, () => { console.log(`Server listening on port ${port}`); }); diff --git a/server/syscalls/page_index.ts b/server/syscalls/page_index.ts index 3056fb4c..0b283f74 100644 --- a/server/syscalls/page_index.ts +++ b/server/syscalls/page_index.ts @@ -1,4 +1,5 @@ import { Knex } from "knex"; +import { SysCallMapping } from "../../plugbox/system"; type IndexItem = { page: string; @@ -11,12 +12,12 @@ export type KV = { value: any; }; -export default function (db: Knex) { - const apiObj = { - "indexer.clearPageIndexForPage": async (page: string) => { +export default function (db: Knex): SysCallMapping { + const apiObj: SysCallMapping = { + clearPageIndexForPage: async (ctx, page: string) => { await db("page_index").where({ page }).del(); }, - "indexer.set": async (page: string, key: string, value: any) => { + set: async (ctx, page: string, key: string, value: any) => { let changed = await db("page_index") .where({ page, key }) .update("value", JSON.stringify(value)); @@ -28,12 +29,12 @@ export default function (db: Knex) { }); } }, - "indexer.batchSet": async (page: string, kvs: KV[]) => { + batchSet: async (ctx, page: string, kvs: KV[]) => { for (let { key, value } of kvs) { - await apiObj["indexer.set"](page, key, value); + await apiObj.set(ctx, page, key, value); } }, - "indexer.get": async (page: string, key: string) => { + get: async (ctx, page: string, key: string) => { let result = await db("page_index") .where({ page, key }) .select("value"); @@ -43,10 +44,10 @@ export default function (db: Knex) { return null; } }, - "indexer.delete": async (page: string, key: string) => { + delete: async (ctx, page: string, key: string) => { await db("page_index").where({ page, key }).del(); }, - "indexer.scanPrefixForPage": async (page: string, prefix: string) => { + scanPrefixForPage: async (ctx, page: string, prefix: string) => { return ( await db("page_index") .where({ page }) @@ -58,7 +59,7 @@ export default function (db: Knex) { value: JSON.parse(value), })); }, - "indexer.scanPrefixGlobal": async (prefix: string) => { + scanPrefixGlobal: async (ctx, prefix: string) => { return ( await db("page_index") .andWhereLike("key", `${prefix}%`) @@ -69,13 +70,13 @@ export default function (db: Knex) { value: JSON.parse(value), })); }, - "indexer.deletePrefixForPage": async (page: string, prefix: string) => { + deletePrefixForPage: async (ctx, page: string, prefix: string) => { return db("page_index") .where({ page }) .andWhereLike("key", `${prefix}%`) .del(); }, - "indexer.clearPageIndex": async () => { + clearPageIndex: async () => { return db("page_index").del(); }, }; diff --git a/tsconfig.json b/tsconfig.json index 8737af9e..7d70674e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,5 @@ { - "include": [ - "webapp/**/*", - "server/**/*", - "plugbox/**/*" - ], + "include": ["webapp/**/*", "server/**/*", "plugbox/**/*", "plugs/**/*"], "compilerOptions": { "target": "esnext", "strict": true, @@ -12,6 +8,7 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "downlevelIteration": true } } diff --git a/webapp/collab.ts b/webapp/collab.ts index f337e4b3..344b33d2 100644 --- a/webapp/collab.ts +++ b/webapp/collab.ts @@ -179,7 +179,7 @@ export function collabExtension( if (this.pushing || !updates.length) return; this.pushing = true; let version = getSyncedVersion(this.view.state); - console.log("Updates", updates, "to apply to version", version); + // console.log("Updates", updates, "to apply to version", version); let success = await callbacks.pushUpdates(pageName, version, updates); this.pushing = false; @@ -201,7 +201,6 @@ export function collabExtension( // Regardless of whether the push failed or new updates came in // while it was running, try again if there's updates remaining if (!this.done && sendableUpdates(this.view.state).length) { - // setTimeout(() => this.push(), 100); this.throttledPush(); } } diff --git a/webapp/components/top_bar.tsx b/webapp/components/top_bar.tsx index ef8cf9db..e81e0761 100644 --- a/webapp/components/top_bar.tsx +++ b/webapp/components/top_bar.tsx @@ -1,7 +1,6 @@ -import { AppViewState, PageMeta } from "../types"; +import { Notification } from "../types"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faFileLines } from "@fortawesome/free-solid-svg-icons"; -import { Notification } from "../types"; function prettyName(s: string | undefined): string { if (!s) { @@ -28,11 +27,13 @@ export function TopBar({ {prettyName(pageName)} -
- {notifications.map((notification) => ( -
{notification.message}
- ))} -
+ {notifications.length > 0 && ( +
+ {notifications.map((notification) => ( +
{notification.message}
+ ))} +
+ )} ); diff --git a/webapp/editor.tsx b/webapp/editor.tsx index dbb6f3d8..9747add4 100644 --- a/webapp/editor.tsx +++ b/webapp/editor.tsx @@ -37,9 +37,9 @@ import reducer from "./reducer"; import { smartQuoteKeymap } from "./smart_quotes"; import { Space } from "./space"; import customMarkdownStyle from "./style"; -import editorSyscalls from "./syscalls/editor.browser"; -import indexerSyscalls from "./syscalls/indexer.native"; -import spaceSyscalls from "./syscalls/space.native"; +import editorSyscalls from "./syscalls/editor"; +import indexerSyscalls from "./syscalls/indexer"; +import spaceSyscalls from "./syscalls/space"; import { Action, AppCommand, @@ -50,6 +50,8 @@ import { import { SilverBulletHooks } from "../common/manifest"; import { safeRun } from "./util"; import { System } from "../plugbox/system"; +import { EventFeature } from "../plugbox/feature/event"; +import { systemSyscalls } from "./syscalls/system"; class PageState { scrollTop: number; @@ -71,11 +73,16 @@ export class Editor implements AppEventDispatcher { space: Space; navigationResolve?: (val: undefined) => void; pageNavigator: IPageNavigator; + private eventFeature: EventFeature; constructor(space: Space, parent: Element) { this.space = space; this.viewState = initialViewState; this.viewDispatch = () => {}; + + this.eventFeature = new EventFeature(); + this.system.addFeature(this.eventFeature); + this.render(parent); this.editorView = new EditorView({ state: this.createEditorState( @@ -86,11 +93,10 @@ export class Editor implements AppEventDispatcher { }); this.pageNavigator = new PathPageNavigator(); - this.system.registerSyscalls( - editorSyscalls(this), - spaceSyscalls(this), - indexerSyscalls(this.space) - ); + this.system.registerSyscalls("editor", [], editorSyscalls(this)); + this.system.registerSyscalls("space", [], spaceSyscalls(this)); + this.system.registerSyscalls("indexer", [], indexerSyscalls(this.space)); + this.system.registerSyscalls("system", [], systemSyscalls(this.space)); } async init() { @@ -129,20 +135,14 @@ export class Editor implements AppEventDispatcher { }, loadSystem: (systemJSON) => { safeRun(async () => { - await this.system.replaceAllFromJSON(systemJSON, () => - createIFrameSandbox(this.system) - ); + await this.system.replaceAllFromJSON(systemJSON, createIFrameSandbox); this.buildAllCommands(); }); }, plugLoaded: (plugName, plug) => { safeRun(async () => { console.log("Plug load", plugName); - await this.system.load( - plugName, - plug, - createIFrameSandbox(this.system) - ); + await this.system.load(plugName, plug, createIFrameSandbox); this.buildAllCommands(); }); }, @@ -199,7 +199,7 @@ export class Editor implements AppEventDispatcher { } async dispatchAppEvent(name: AppEvent, data?: any): Promise { - return this.system.dispatchEvent(name, data); + return this.eventFeature.dispatchEvent(name, data); } get currentPage(): string | undefined { diff --git a/webapp/space.ts b/webapp/space.ts index b4c7640f..95e2e711 100644 --- a/webapp/space.ts +++ b/webapp/space.ts @@ -80,7 +80,7 @@ export class Space extends EventEmitter { }); } - private wsCall(eventName: string, ...args: any[]): Promise { + public wsCall(eventName: string, ...args: any[]): Promise { return new Promise((resolve, reject) => { this.reqId++; this.socket!.once(`${eventName}Resp${this.reqId}`, (err, result) => { @@ -160,40 +160,4 @@ export class Space extends EventEmitter { async getPageMeta(name: string): Promise { return this.wsCall("page.getPageMeta", name); } - - async indexSet(pageName: string, key: string, value: any) { - await this.wsCall("index.set", pageName, key, value); - } - - async indexBatchSet(pageName: string, kvs: KV[]) { - // TODO: Optimize with batch call - for (let { key, value } of kvs) { - await this.indexSet(pageName, key, value); - } - } - - async indexGet(pageName: string, key: string): Promise { - return await this.wsCall("index.get", pageName, key); - } - - async indexScanPrefixForPage( - pageName: string, - keyPrefix: string - ): Promise<{ key: string; value: any }[]> { - return await this.wsCall("index.scanPrefixForPage", pageName, keyPrefix); - } - - async indexScanPrefixGlobal( - keyPrefix: string - ): Promise<{ key: string; value: any }[]> { - return await this.wsCall("index.scanPrefixGlobal", keyPrefix); - } - - async indexDeletePrefixForPage(pageName: string, keyPrefix: string) { - await this.wsCall("index.deletePrefixForPage", keyPrefix); - } - - async indexDelete(pageName: string, key: string) { - await this.wsCall("index.delete", pageName, key); - } } diff --git a/webapp/styles/editor.scss b/webapp/styles/editor.scss index e45cd1d5..c79d55cd 100644 --- a/webapp/styles/editor.scss +++ b/webapp/styles/editor.scss @@ -129,7 +129,6 @@ .mention { color: #0330cb; - text-decoration: underline; } .tag { diff --git a/webapp/syscalls/db.localstorage.ts b/webapp/syscalls/db.localstorage.ts deleted file mode 100644 index b666f958..00000000 --- a/webapp/syscalls/db.localstorage.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default { - "db.put": (key: string, value: any) => { - localStorage.setItem(key, value); - }, - "db.get": (key: string) => { - return localStorage.getItem(key); - }, -}; diff --git a/webapp/syscalls/editor.browser.ts b/webapp/syscalls/editor.ts similarity index 78% rename from webapp/syscalls/editor.browser.ts rename to webapp/syscalls/editor.ts index 8e83e5ae..ba94084d 100644 --- a/webapp/syscalls/editor.browser.ts +++ b/webapp/syscalls/editor.ts @@ -1,7 +1,7 @@ import { Editor } from "../editor"; import { syntaxTree } from "@codemirror/language"; import { Transaction } from "@codemirror/state"; -import { PageMeta } from "../types"; +import { SysCallMapping } from "../../plugbox/system"; type SyntaxNode = { name: string; @@ -26,23 +26,23 @@ function ensureAnchor(expr: any, start: boolean) { ); } -export default (editor: Editor) => ({ - "editor.getCurrentPage": (): string => { +export default (editor: Editor): SysCallMapping => ({ + getCurrentPage: (): string => { return editor.currentPage!; }, - "editor.getText": () => { + getText: () => { return editor.editorView?.state.sliceDoc(); }, - "editor.getCursor": (): number => { + getCursor: (): number => { return editor.editorView!.state.selection.main.from; }, - "editor.navigate": async (name: string) => { + navigate: async (ctx, name: string) => { await editor.navigate(name); }, - "editor.openUrl": async (url: string) => { + openUrl: async (ctx, url: string) => { window.open(url, "_blank")!.focus(); }, - "editor.insertAtPos": (text: string, pos: number) => { + insertAtPos: (ctx, text: string, pos: number) => { editor.editorView!.dispatch({ changes: { insert: text, @@ -50,7 +50,7 @@ export default (editor: Editor) => ({ }, }); }, - "editor.replaceRange": (from: number, to: number, text: string) => { + replaceRange: (ctx, from: number, to: number, text: string) => { editor.editorView!.dispatch({ changes: { insert: text, @@ -59,14 +59,14 @@ export default (editor: Editor) => ({ }, }); }, - "editor.moveCursor": (pos: number) => { + moveCursor: (ctx, pos: number) => { editor.editorView!.dispatch({ selection: { anchor: pos, }, }); }, - "editor.insertAtCursor": (text: string) => { + insertAtCursor: (ctx, text: string) => { let editorView = editor.editorView!; let from = editorView.state.selection.main.from; editorView.dispatch({ @@ -79,7 +79,7 @@ export default (editor: Editor) => ({ }, }); }, - "editor.getSyntaxNodeUnderCursor": (): SyntaxNode | undefined => { + getSyntaxNodeUnderCursor: (): SyntaxNode | undefined => { const editorState = editor.editorView!.state; let selection = editorState.selection.main; if (selection.empty) { @@ -94,7 +94,8 @@ export default (editor: Editor) => ({ } } }, - "editor.matchBefore": ( + matchBefore: ( + ctx, regexp: string ): { from: number; to: number; text: string } | null => { const editorState = editor.editorView!.state; @@ -112,7 +113,7 @@ export default (editor: Editor) => ({ } return null; }, - "editor.getSyntaxNodeAtPos": (pos: number): SyntaxNode | undefined => { + getSyntaxNodeAtPos: (ctx, pos: number): SyntaxNode | undefined => { const editorState = editor.editorView!.state; let node = syntaxTree(editorState).resolveInner(pos); if (node) { @@ -124,10 +125,10 @@ export default (editor: Editor) => ({ }; } }, - "editor.dispatch": (change: Transaction) => { + dispatch: (ctx, change: Transaction) => { editor.editorView!.dispatch(change); }, - "editor.prompt": (message: string, defaultValue = ""): string | null => { + prompt: (ctx, message: string, defaultValue = ""): string | null => { return prompt(message, defaultValue); }, }); diff --git a/webapp/syscalls/indexer.native.ts b/webapp/syscalls/indexer.native.ts deleted file mode 100644 index fbb97ba4..00000000 --- a/webapp/syscalls/indexer.native.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Space, KV } from "../space"; - -export default (space: Space) => ({ - "indexer.scanPrefixForPage": async (pageName: string, keyPrefix: string) => { - return await space.indexScanPrefixForPage(pageName, keyPrefix); - }, - "indexer.scanPrefixGlobal": async (keyPrefix: string) => { - return await space.indexScanPrefixGlobal(keyPrefix); - }, - "indexer.get": async (pageName: string, key: string): Promise => { - return await space.indexGet(pageName, key); - }, - "indexer.set": async (pageName: string, key: string, value: any) => { - await space.indexSet(pageName, key, value); - }, - "indexer.batchSet": async (pageName: string, kvs: KV[]) => { - await space.indexBatchSet(pageName, kvs); - }, - "indexer.delete": async (pageName: string, key: string) => { - await space.indexDelete(pageName, key); - }, -}); diff --git a/webapp/syscalls/indexer.ts b/webapp/syscalls/indexer.ts new file mode 100644 index 00000000..b71350a7 --- /dev/null +++ b/webapp/syscalls/indexer.ts @@ -0,0 +1,17 @@ +import { Space } from "../space"; +import { SysCallMapping } from "../../plugbox/system"; +import { transportSyscalls } from "../../plugbox/syscall/transport"; + +export default function indexerSyscalls(space: Space): SysCallMapping { + return transportSyscalls( + [ + "scanPrefixForPage", + "scanPrefixGlobal", + "get", + "set", + "batchSet", + "delete", + ], + (name, ...args) => space.wsCall(`index.${name}`, ...args) + ); +} diff --git a/webapp/syscalls/space.native.ts b/webapp/syscalls/space.ts similarity index 64% rename from webapp/syscalls/space.native.ts rename to webapp/syscalls/space.ts index ddc5eae4..204f2c56 100644 --- a/webapp/syscalls/space.native.ts +++ b/webapp/syscalls/space.ts @@ -1,21 +1,21 @@ import { Editor } from "../editor"; import { PageMeta } from "../types"; +import { SysCallMapping } from "../../plugbox/system"; -export default (editor: Editor) => ({ - "space.listPages": (): PageMeta[] => { +export default (editor: Editor): SysCallMapping => ({ + listPages: (): PageMeta[] => { return [...editor.viewState.allPages]; }, - "space.readPage": async ( + readPage: async ( + ctx, name: string ): Promise<{ text: string; meta: PageMeta }> => { return await editor.space.readPage(name); }, - "space.writePage": async (name: string, text: string): Promise => { + writePage: async (ctx, name: string, text: string): Promise => { return await editor.space.writePage(name, text); }, - "space.deletePage": async (name: string) => { - console.log("Clearing page index", name); - await editor.space.indexDeletePrefixForPage(name, ""); + deletePage: async (ctx, name: string) => { // If we're deleting the current page, navigate to the start page if (editor.currentPage === name) { await editor.navigate("start"); diff --git a/webapp/syscalls/system.ts b/webapp/syscalls/system.ts new file mode 100644 index 00000000..89b89f82 --- /dev/null +++ b/webapp/syscalls/system.ts @@ -0,0 +1,13 @@ +import { SysCallMapping } from "../../plugbox/system"; +import { Space } from "../space"; + +export function systemSyscalls(space: Space): SysCallMapping { + return { + async invokeFunctionOnServer(ctx, name: string, ...args: any[]) { + if (!ctx.plug) { + throw Error("No plug associated with context"); + } + return await space.wsCall("invokeFunction", ctx.plug.name, name, ...args); + }, + }; +} diff --git a/webapp/syscalls/ui.browser.ts b/webapp/syscalls/ui.browser.ts deleted file mode 100644 index 120b0220..00000000 --- a/webapp/syscalls/ui.browser.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @ts-ignore -let frameTest = document.getElementById("main-frame"); - -window.addEventListener("message", async (event) => { - let messageEvent = event as MessageEvent; - let data = messageEvent.data; - if (data.type === "iframe_event") { - // @ts-ignore - window.mainPlug.dispatchEvent(data.data.event, data.data.data); - } -}); - -export default { - "ui.update": function (doc: any) { - // frameTest.contentWindow.postMessage({ - // type: "loadContent", - // doc: doc, - // }); - }, -}; diff --git a/webapp/tsconfig.json b/webapp/tsconfig.json deleted file mode 100644 index cab209a3..00000000 --- a/webapp/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "include": ["src/**/*"], - "compilerOptions": { - "target": "esnext", - "strict": true, - "moduleResolution": "node", - "module": "ESNext", - "allowSyntheticDefaultImports": true, - "resolveJsonModule": true, - "jsx": "react-jsx" - } -} diff --git a/yarn.lock b/yarn.lock index 3bdde9fa..6a22a806 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1631,6 +1631,14 @@ resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.1.tgz#e01a874d4c2aa1a02ebc64cfd1cd8ebdbad7a996" integrity sha512-BkMHHonDT8NJUE/pQ3kr5v2GLDKm5or9btLBoBx4F2MB2cuqYC748LYMDC55VlrLI5qZZv+Qgc3m4P3dBPcmeg== +"@types/node-fetch@^2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975" + integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + "@types/node@*", "@types/node@>=10.0.0", "@types/node@^17.0.21": version "17.0.21" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644" @@ -1976,6 +1984,11 @@ base-x@^3.0.8: dependencies: safe-buffer "^5.0.1" +base64-arraybuffer-es6@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.7.0.tgz#dbe1e6c87b1bf1ca2875904461a7de40f21abc86" + integrity sha512-ESyU/U1CFZDJUdr+neHRhNozeCv72Y7Vm0m1DCbjX3KBjT6eYocvAJlSk6+8+HkVwXlT1FNxhGW6q3UKAlCvvw== + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -2486,6 +2499,11 @@ cookiejar@^2.1.3: resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== +core-js@^3.4: + version "3.21.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94" + integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig== + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -2691,6 +2709,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33" integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw== +data-uri-to-buffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b" + integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA== + data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -2863,6 +2886,13 @@ domelementtype@^2.0.1, domelementtype@^2.2.0: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== +domexception@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" + integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== + dependencies: + webidl-conversions "^4.0.2" + domexception@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" @@ -3320,6 +3350,13 @@ express@^4.17.3: utils-merge "1.0.1" vary "~1.1.2" +fake-indexeddb@^3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-3.1.7.tgz#d9efbeade113c15efbe862e4598a4b0a1797ed9f" + integrity sha512-CUGeCzCOVjmeKi2C0pcvSh6NDU6uQIaS+7YyR++tO/atJJujkBYVhDvfePdz/U8bD33BMVWirsr1MKczfAqbjA== + dependencies: + realistic-structured-clone "^2.0.1" + fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -3342,6 +3379,14 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.1.5" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.1.5.tgz#0077bf5f3fcdbd9d75a0b5362f77dbb743489863" + integrity sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -3398,6 +3443,13 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + formidable@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.0.1.tgz#4310bc7965d185536f9565184dee74fbb75557ff" @@ -4914,6 +4966,20 @@ node-cron@^3.0.0: dependencies: moment-timezone "^0.5.31" +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + +node-fetch@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.3.tgz#a03c9cc2044d21d1a021566bd52f080f333719a6" + integrity sha512-AXP18u4pidSZ1xYXRDPY/8jdv3RAozIt/WLNR/MBGZAz+xjtlr90RvCnsvHQRiXyWliZF/CpytExp32UU67/SA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + node-gyp-build@^4.2.3, node-gyp-build@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" @@ -5757,6 +5823,16 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +realistic-structured-clone@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/realistic-structured-clone/-/realistic-structured-clone-2.0.4.tgz#7eb4c2319fc3cb72f4c8d3c9e888b11647894b50" + integrity sha512-lItAdBIFHUSe6fgztHPtmmWqKUgs+qhcYLi3wTRUl4OTB3Vb8aBVSjGfQZUvkmJCKoX3K9Wf7kyLp/F/208+7A== + dependencies: + core-js "^3.4" + domexception "^1.0.1" + typeson "^6.1.0" + typeson-registry "^1.0.0-alpha.20" + rechoir@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" @@ -6493,6 +6569,20 @@ typescript@^4.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4" integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg== +typeson-registry@^1.0.0-alpha.20: + version "1.0.0-alpha.39" + resolved "https://registry.yarnpkg.com/typeson-registry/-/typeson-registry-1.0.0-alpha.39.tgz#9e0f5aabd5eebfcffd65a796487541196f4b1211" + integrity sha512-NeGDEquhw+yfwNhguLPcZ9Oj0fzbADiX4R0WxvoY8nGhy98IbzQy1sezjoEFWOywOboj/DWehI+/aUlRVrJnnw== + dependencies: + base64-arraybuffer-es6 "^0.7.0" + typeson "^6.0.0" + whatwg-url "^8.4.0" + +typeson@^6.0.0, typeson@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/typeson/-/typeson-6.1.0.tgz#5b2a53705a5f58ff4d6f82f965917cabd0d7448b" + integrity sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA== + uglify-js@^3.15.1: version "3.15.3" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.3.tgz#9aa82ca22419ba4c0137642ba0df800cb06e0471" @@ -6650,6 +6740,16 @@ weak-lru-cache@^1.2.2: resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19" integrity sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw== +web-streams-polyfill@^3.0.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965" + integrity sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA== + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" @@ -6672,7 +6772,7 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== -whatwg-url@^8.0.0, whatwg-url@^8.5.0: +whatwg-url@^8.0.0, whatwg-url@^8.4.0, whatwg-url@^8.5.0: version "8.7.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==