Refactoring
parent
b51730d33f
commit
08e6c3bad8
|
@ -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<SilverBulletHooks>;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<any>) {
|
||||
return new Sandbox(system, new IFrameWrapper());
|
||||
export function createSandbox(plug: Plug<any>) {
|
||||
return new Sandbox(plug, new IFrameWrapper());
|
||||
}
|
||||
|
|
|
@ -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<void>;
|
||||
|
@ -33,12 +33,12 @@ class NodeWorkerWrapper implements WorkerLike {
|
|||
}
|
||||
}
|
||||
|
||||
export function createSandbox(system: System<any>) {
|
||||
export function createSandbox(plug: Plug<any>) {
|
||||
let worker = new Worker(workerCode, {
|
||||
eval: true,
|
||||
});
|
||||
return new Sandbox(
|
||||
system,
|
||||
plug,
|
||||
new NodeWorkerWrapper(
|
||||
new Worker(workerCode, {
|
||||
eval: true,
|
||||
|
|
|
@ -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<any>) {
|
||||
export function createSandbox(plug: Plug<any>) {
|
||||
// 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));
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ test("Run a plugbox endpoint server", async () => {
|
|||
endpoints: [{ method: "GET", path: "/", handler: "testhandler" }],
|
||||
},
|
||||
} as Manifest<EndpointHook>,
|
||||
createSandbox(system)
|
||||
createSandbox
|
||||
);
|
||||
|
||||
const app = express();
|
||||
|
|
|
@ -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<EventHook> {
|
||||
private system?: System<EventHook>;
|
||||
|
||||
async dispatchEvent(name: string, data?: any): Promise<any[]> {
|
||||
if (!this.system) {
|
||||
throw new Error("EventFeature is not initialized");
|
||||
}
|
||||
let promises: Promise<any>[] = [];
|
||||
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<EventHook>): void {
|
||||
this.system = system;
|
||||
system.on({
|
||||
plugLoaded: (name, plug) => {},
|
||||
});
|
||||
}
|
||||
|
||||
validateManifest(manifest: Manifest<EventHook>): string[] {
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -7,16 +7,28 @@ export class Plug<HookT> {
|
|||
sandbox: Sandbox;
|
||||
public manifest?: Manifest<HookT>;
|
||||
readonly runtimeEnv: RuntimeEnvironment;
|
||||
grantedPermissions: string[] = [];
|
||||
name: string;
|
||||
|
||||
constructor(system: System<HookT>, name: string, sandbox: Sandbox) {
|
||||
constructor(
|
||||
system: System<HookT>,
|
||||
name: string,
|
||||
sandboxFactory: (plug: Plug<HookT>) => Sandbox
|
||||
) {
|
||||
this.system = system;
|
||||
this.sandbox = sandbox;
|
||||
this.name = name;
|
||||
this.sandbox = sandboxFactory(this);
|
||||
this.runtimeEnv = system.runtimeEnv;
|
||||
}
|
||||
|
||||
async load(manifest: Manifest<HookT>) {
|
||||
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<any> {
|
||||
return this.system.syscallWithContext({ plug: this }, name, args);
|
||||
}
|
||||
|
||||
canInvoke(name: string) {
|
||||
|
@ -46,27 +58,6 @@ export class Plug<HookT> {
|
|||
return await this.sandbox.invoke(name, args);
|
||||
}
|
||||
|
||||
async dispatchEvent(name: string, data?: any): Promise<any[]> {
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -20,9 +20,7 @@ export class DiskPlugLoader<HookT> {
|
|||
|
||||
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<HookT> {
|
|||
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);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { System } from "./system";
|
||||
import {
|
||||
ControllerMessage,
|
||||
WorkerLike,
|
||||
WorkerMessage,
|
||||
} from "./environment/worker";
|
||||
import { Plug } from "./plug";
|
||||
|
||||
export type SandboxFactory<HookT> = (plug: Plug<HookT>) => 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<string>();
|
||||
protected system: System<any>;
|
||||
protected plug: Plug<any>;
|
||||
|
||||
constructor(system: System<any>, worker: WorkerLike) {
|
||||
constructor(plug: Plug<any>, 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",
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
});
|
|
@ -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();
|
||||
});
|
|
@ -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<any | null> {
|
||||
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,
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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");
|
||||
});
|
|
@ -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<any, unknown>, 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<any, unknown>,
|
||||
tableName: string
|
||||
): SysCallMapping {
|
||||
const apiObj: SysCallMapping = {
|
||||
delete: async (ctx, page: string, key: string) => {
|
||||
await db<Item>(tableName).where({ page, key }).del();
|
||||
},
|
||||
deletePrefix: async (ctx, prefix: string) => {
|
||||
return db<Item>(tableName).andWhereLike("key", `${prefix}%`).del();
|
||||
},
|
||||
deleteAll: async (ctx) => {
|
||||
await db<Item>(tableName).del();
|
||||
},
|
||||
set: async (ctx, key: string, value: any) => {
|
||||
let changed = await db<Item>(tableName)
|
||||
.where({ key })
|
||||
.update("value", JSON.stringify(value));
|
||||
if (changed === 0) {
|
||||
await db<Item>(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<any, unknown>,
|
||||
tableName: string
|
||||
): SysCallMapping {
|
||||
return {
|
||||
get: async (ctx, key: string): Promise<any | null> => {
|
||||
let result = await db<Item>(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<Item>(tableName)
|
||||
.andWhereLike("key", `${prefix}%`)
|
||||
.select("key", "value")
|
||||
).map(({ key, value }) => ({
|
||||
key,
|
||||
value: JSON.parse(value),
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { SysCallMapping } from "../system";
|
||||
|
||||
export function transportSyscalls(
|
||||
names: string[],
|
||||
transportCall: (name: string, ...args: any[]) => Promise<any>
|
||||
): SysCallMapping {
|
||||
let syscalls: SysCallMapping = {};
|
||||
|
||||
for (let name of names) {
|
||||
syscalls[name] = (ctx, ...args: any[]) => {
|
||||
return transportCall(name, ...args);
|
||||
};
|
||||
}
|
||||
|
||||
return syscalls;
|
||||
}
|
|
@ -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> | any;
|
||||
[key: string]: (ctx: SyscallContext, ...args: any) => Promise<any> | any;
|
||||
}
|
||||
|
||||
export type SystemJSON<HookT> = { [key: string]: Manifest<HookT> };
|
||||
|
@ -14,9 +14,23 @@ export type SystemEvents<HookT> = {
|
|||
plugUnloaded: (name: string, plug: Plug<HookT>) => void;
|
||||
};
|
||||
|
||||
type SyscallContext = {
|
||||
plug: Plug<any> | null;
|
||||
};
|
||||
|
||||
type SyscallSignature = (
|
||||
ctx: SyscallContext,
|
||||
...args: any[]
|
||||
) => Promise<any> | any;
|
||||
|
||||
type Syscall = {
|
||||
requiredPermissions: string[];
|
||||
callback: SyscallSignature;
|
||||
};
|
||||
|
||||
export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
||||
protected plugs = new Map<string, Plug<HookT>>();
|
||||
registeredSyscalls: SysCallMapping = {};
|
||||
protected registeredSyscalls = new Map<string, Syscall>();
|
||||
protected enabledFeatures = new Set<Feature<HookT>>();
|
||||
|
||||
readonly runtimeEnv: RuntimeEnvironment;
|
||||
|
@ -31,29 +45,46 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
|||
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<any> {
|
||||
const callback = this.registeredSyscalls[name];
|
||||
if (!name) {
|
||||
async syscallWithContext(
|
||||
ctx: SyscallContext,
|
||||
name: string,
|
||||
args: any[]
|
||||
): Promise<any> {
|
||||
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`);
|
||||
}
|
||||
return Promise.resolve(callback(...args));
|
||||
if (!ctx.plug.grantedPermissions.includes(permission)) {
|
||||
throw Error(`Missing permission '${permission}' for syscall ${name}`);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(syscall.callback(ctx, ...args));
|
||||
}
|
||||
|
||||
async load(
|
||||
name: string,
|
||||
manifest: Manifest<HookT>,
|
||||
sandbox: Sandbox
|
||||
sandboxFactory: SandboxFactory<HookT>
|
||||
): Promise<Plug<HookT>> {
|
||||
if (this.plugs.has(name)) {
|
||||
await this.unload(name);
|
||||
|
@ -67,7 +98,7 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
|||
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<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
|||
this.plugs.delete(name);
|
||||
}
|
||||
|
||||
async dispatchEvent(name: string, data?: any): Promise<any[]> {
|
||||
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<string, Plug<HookT>> {
|
||||
return this.plugs;
|
||||
}
|
||||
|
@ -111,12 +132,12 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
|||
|
||||
async replaceAllFromJSON(
|
||||
json: SystemJSON<HookT>,
|
||||
sandboxFactory: () => Sandbox
|
||||
sandboxFactory: SandboxFactory<HookT>
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { System } from "./system";
|
||||
|
||||
export interface Manifest<HookT> {
|
||||
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<HookT> {
|
||||
validateManifest(manifest: Manifest<HookT>): string[];
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { syscall } from "./lib/syscall";
|
||||
import { syscall } from "../lib/syscall";
|
||||
|
||||
export async function insertToday() {
|
||||
console.log("Inserting date");
|
||||
|
|
|
@ -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!");
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { syscall } from "./lib/syscall";
|
||||
import { syscall } from "../lib/syscall";
|
||||
|
||||
export async function toggleH1() {
|
||||
await togglePrefix("# ");
|
||||
|
|
|
@ -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,25 +35,11 @@ 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) => ({
|
||||
|
@ -63,4 +48,3 @@ export async function pageComplete() {
|
|||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -13,4 +13,5 @@ export function endpointTest(req: EndpointRequest): EndpointResponse {
|
|||
|
||||
export function welcome() {
|
||||
console.log("Hello world!");
|
||||
return "hi";
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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!");
|
||||
}
|
|
@ -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<any> => {
|
||||
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());
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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<string, Page>;
|
||||
|
@ -18,6 +19,7 @@ export class PageApi implements ApiProvider {
|
|||
rootPath: string;
|
||||
connectedSockets: Set<Socket>;
|
||||
private system: System<SilverBulletHooks>;
|
||||
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<void> {
|
||||
|
@ -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),
|
||||
});
|
||||
|
|
|
@ -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))
|
||||
|
@ -54,7 +54,7 @@ expressServer
|
|||
);
|
||||
await plugLoader.loadPlugs();
|
||||
plugLoader.watcher();
|
||||
system.registerSyscalls(shellSyscalls(pagesPath));
|
||||
system.registerSyscalls("shell", ["shell"], shellSyscalls(pagesPath));
|
||||
system.addFeature(new NodeCronFeature());
|
||||
server.listen(port, () => {
|
||||
console.log(`Server listening on port ${port}`);
|
||||
|
|
|
@ -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<IndexItem>("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<IndexItem>("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<IndexItem>("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<IndexItem>("page_index").where({ page, key }).del();
|
||||
},
|
||||
"indexer.scanPrefixForPage": async (page: string, prefix: string) => {
|
||||
scanPrefixForPage: async (ctx, page: string, prefix: string) => {
|
||||
return (
|
||||
await db<IndexItem>("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<IndexItem>("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<IndexItem>("page_index")
|
||||
.where({ page })
|
||||
.andWhereLike("key", `${prefix}%`)
|
||||
.del();
|
||||
},
|
||||
"indexer.clearPageIndex": async () => {
|
||||
clearPageIndex: async () => {
|
||||
return db<IndexItem>("page_index").del();
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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({
|
|||
<FontAwesomeIcon icon={faFileLines} />
|
||||
</span>
|
||||
<span className="current-page">{prettyName(pageName)}</span>
|
||||
{notifications.length > 0 && (
|
||||
<div className="status">
|
||||
{notifications.map((notification) => (
|
||||
<div key={notification.id}>{notification.message}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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<any[]> {
|
||||
return this.system.dispatchEvent(name, data);
|
||||
return this.eventFeature.dispatchEvent(name, data);
|
||||
}
|
||||
|
||||
get currentPage(): string | undefined {
|
||||
|
|
|
@ -80,7 +80,7 @@ export class Space extends EventEmitter<SpaceEvents> {
|
|||
});
|
||||
}
|
||||
|
||||
private wsCall(eventName: string, ...args: any[]): Promise<any> {
|
||||
public wsCall(eventName: string, ...args: any[]): Promise<any> {
|
||||
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<SpaceEvents> {
|
|||
async getPageMeta(name: string): Promise<PageMeta> {
|
||||
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<any | null> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,7 +129,6 @@
|
|||
|
||||
.mention {
|
||||
color: #0330cb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.tag {
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
export default {
|
||||
"db.put": (key: string, value: any) => {
|
||||
localStorage.setItem(key, value);
|
||||
},
|
||||
"db.get": (key: string) => {
|
||||
return localStorage.getItem(key);
|
||||
},
|
||||
};
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -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<any> => {
|
||||
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);
|
||||
},
|
||||
});
|
|
@ -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)
|
||||
);
|
||||
}
|
|
@ -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<PageMeta> => {
|
||||
writePage: async (ctx, name: string, text: string): Promise<PageMeta> => {
|
||||
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");
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
// });
|
||||
},
|
||||
};
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"include": ["src/**/*"],
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"module": "ESNext",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
102
yarn.lock
102
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==
|
||||
|
|
Loading…
Reference in New Issue