Tons of refactoring, moving commands and slash commands into hooks
This commit is contained in:
parent
bf32d6d0bd
commit
b89aee97d7
@ -1,24 +1,14 @@
|
||||
import * as plugos from "../plugos/types";
|
||||
import { EndpointHook } from "../plugos/feature/endpoint";
|
||||
import { CronHook } from "../plugos/feature/node_cron";
|
||||
import { EventHook } from "../plugos/feature/event";
|
||||
import { EndpointHookT } from "../plugos/hooks/endpoint";
|
||||
import { CronHookT } from "../plugos/hooks/node_cron";
|
||||
import { EventHookT } from "../plugos/hooks/event";
|
||||
import { CommandHookT } from "../webapp/hooks/command";
|
||||
import { SlashCommandHookT } from "../webapp/hooks/slash_command";
|
||||
|
||||
export type CommandDef = {
|
||||
name: string;
|
||||
|
||||
// Bind to keyboard shortcut
|
||||
key?: string;
|
||||
mac?: string;
|
||||
|
||||
// If to show in slash invoked menu and if so, with what label
|
||||
// should match slashCommandRegexp
|
||||
slashCommand?: string;
|
||||
};
|
||||
|
||||
export type SilverBulletHooks = {
|
||||
command?: CommandDef | CommandDef[];
|
||||
} & EndpointHook &
|
||||
CronHook &
|
||||
EventHook;
|
||||
export type SilverBulletHooks = CommandHookT &
|
||||
SlashCommandHookT &
|
||||
EndpointHookT &
|
||||
CronHookT &
|
||||
EventHookT;
|
||||
|
||||
export type Manifest = plugos.Manifest<SilverBulletHooks>;
|
||||
|
@ -14,7 +14,7 @@
|
||||
"clean": "rm -rf dist",
|
||||
"plugs": "cd plugs && ../plugos/dist/plugos/plugos-bundle.js -w --dist dist */*.plug.yaml",
|
||||
"server": "nodemon -w dist/server dist/server/server.js pages",
|
||||
"test": "jest dist/test"
|
||||
"test": "jest dist/test plugos/dist/test"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
@ -2,22 +2,22 @@
|
||||
|
||||
import express from "express";
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
import { DiskPlugLoader } from "../plug_loader";
|
||||
import { CronHook, NodeCronFeature } from "../feature/node_cron";
|
||||
import shellSyscalls from "../syscall/shell.node";
|
||||
import { System } from "../system";
|
||||
import { EndpointFeature, EndpointHook } from "../feature/endpoint";
|
||||
import { safeRun } from "../util";
|
||||
import {hideBin} from "yargs/helpers";
|
||||
import {DiskPlugLoader} from "../plug_loader";
|
||||
import {CronHookT, NodeCronHook} from "../hooks/node_cron";
|
||||
import shellSyscalls from "../syscalls/shell.node";
|
||||
import {System} from "../system";
|
||||
import {EndpointHook, EndpointHookT} from "../hooks/endpoint";
|
||||
import {safeRun} from "../util";
|
||||
import knex from "knex";
|
||||
import {
|
||||
ensureTable,
|
||||
storeReadSyscalls,
|
||||
storeWriteSyscalls,
|
||||
} from "../syscall/store.knex_node";
|
||||
import { fetchSyscalls } from "../syscall/fetch.node";
|
||||
import { EventFeature, EventHook } from "../feature/event";
|
||||
import { eventSyscalls } from "../syscall/event";
|
||||
} from "../syscalls/store.knex_node";
|
||||
import {fetchSyscalls} from "../syscalls/fetch.node";
|
||||
import {EventHook, EventHookT} from "../hooks/event";
|
||||
import {eventSyscalls} from "../syscalls/event";
|
||||
|
||||
let args = yargs(hideBin(process.argv))
|
||||
.option("port", {
|
||||
@ -35,7 +35,7 @@ const plugPath = args._[0] as string;
|
||||
|
||||
const app = express();
|
||||
|
||||
type ServerHook = EndpointHook & CronHook & EventHook;
|
||||
type ServerHook = EndpointHookT & CronHookT & EventHookT;
|
||||
const system = new System<ServerHook>("server");
|
||||
|
||||
safeRun(async () => {
|
||||
@ -52,11 +52,11 @@ safeRun(async () => {
|
||||
let plugLoader = new DiskPlugLoader(system, plugPath);
|
||||
await plugLoader.loadPlugs();
|
||||
plugLoader.watcher();
|
||||
system.addFeature(new NodeCronFeature());
|
||||
let eventFeature = new EventFeature();
|
||||
system.addFeature(eventFeature);
|
||||
system.registerSyscalls("event", [], eventSyscalls(eventFeature));
|
||||
system.addFeature(new EndpointFeature(app, ""));
|
||||
system.addHook(new NodeCronHook());
|
||||
let eventHook = new EventHook();
|
||||
system.addHook(eventHook);
|
||||
system.registerSyscalls("event", [], eventSyscalls(eventHook));
|
||||
system.addHook(new EndpointHook(app, ""));
|
||||
system.registerSyscalls("shell", [], shellSyscalls("."));
|
||||
system.registerSyscalls("fetch", [], fetchSyscalls());
|
||||
system.registerSyscalls(
|
||||
|
@ -50,7 +50,6 @@ parentPort.on("message", (data: any) => {
|
||||
safeRun(async () => {
|
||||
switch (data.type) {
|
||||
case "load":
|
||||
console.log("Booting", data.name);
|
||||
loadedFunctions.set(data.name, new VMScript(wrapScript(data.code)));
|
||||
parentPort.postMessage({
|
||||
type: "inited",
|
@ -48,7 +48,6 @@ self.addEventListener("message", (event: { data: WorkerMessage }) => {
|
||||
let data = messageEvent.data;
|
||||
switch (data.type) {
|
||||
case "load":
|
||||
console.log("Booting", data.name);
|
||||
loadedFunctions.set(data.name!, new Function(wrapScript(data.code!)));
|
||||
postMessage(
|
||||
{
|
@ -1,13 +1,13 @@
|
||||
import { createSandbox } from "../environment/node_sandbox";
|
||||
import { createSandbox } from "../environments/node_sandbox";
|
||||
import { expect, test } from "@jest/globals";
|
||||
import { Manifest } from "../types";
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import { EndpointFeature, EndpointHook } from "./endpoint";
|
||||
import { EndpointHook, EndpointHookT } from "./endpoint";
|
||||
import { System } from "../system";
|
||||
|
||||
test("Run a plugos endpoint server", async () => {
|
||||
let system = new System<EndpointHook>("server");
|
||||
let system = new System<EndpointHookT>("server");
|
||||
let plug = await system.load(
|
||||
"test",
|
||||
{
|
||||
@ -26,14 +26,14 @@ test("Run a plugos endpoint server", async () => {
|
||||
})()`,
|
||||
},
|
||||
},
|
||||
} as Manifest<EndpointHook>,
|
||||
} as Manifest<EndpointHookT>,
|
||||
createSandbox
|
||||
);
|
||||
|
||||
const app = express();
|
||||
const port = 3123;
|
||||
|
||||
system.addFeature(new EndpointFeature(app, "/_"));
|
||||
system.addHook(new EndpointHook(app, "/_"));
|
||||
|
||||
let server = app.listen(port, () => {
|
||||
console.log(`Listening on port ${port}`);
|
@ -1,4 +1,4 @@
|
||||
import { Feature, Manifest } from "../types";
|
||||
import { Hook, Manifest } from "../types";
|
||||
import { Express, NextFunction, Request, Response } from "express";
|
||||
import { System } from "../system";
|
||||
|
||||
@ -16,7 +16,7 @@ export type EndpointResponse = {
|
||||
body: any;
|
||||
};
|
||||
|
||||
export type EndpointHook = {
|
||||
export type EndpointHookT = {
|
||||
http?: EndPointDef | EndPointDef[];
|
||||
};
|
||||
|
||||
@ -25,7 +25,7 @@ export type EndPointDef = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
export class EndpointFeature implements Feature<EndpointHook> {
|
||||
export class EndpointHook implements Hook<EndpointHookT> {
|
||||
private app: Express;
|
||||
private prefix: string;
|
||||
|
||||
@ -34,7 +34,7 @@ export class EndpointFeature implements Feature<EndpointHook> {
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
apply(system: System<EndpointHook>): void {
|
||||
apply(system: System<EndpointHookT>): void {
|
||||
this.app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.path.startsWith(this.prefix)) {
|
||||
return next();
|
||||
@ -106,7 +106,7 @@ export class EndpointFeature implements Feature<EndpointHook> {
|
||||
});
|
||||
}
|
||||
|
||||
validateManifest(manifest: Manifest<EndpointHook>): string[] {
|
||||
validateManifest(manifest: Manifest<EndpointHookT>): string[] {
|
||||
let errors = [];
|
||||
for (const [name, functionDef] of Object.entries(manifest.functions)) {
|
||||
if (!functionDef.http) {
|
@ -1,19 +1,19 @@
|
||||
import { Feature, Manifest } from "../types";
|
||||
import { Hook, Manifest } from "../types";
|
||||
import { System } from "../system";
|
||||
|
||||
// System events:
|
||||
// - plug:load (plugName: string)
|
||||
|
||||
export type EventHook = {
|
||||
export type EventHookT = {
|
||||
events?: string[];
|
||||
};
|
||||
|
||||
export class EventFeature implements Feature<EventHook> {
|
||||
private system?: System<EventHook>;
|
||||
export class EventHook implements Hook<EventHookT> {
|
||||
private system?: System<EventHookT>;
|
||||
|
||||
async dispatchEvent(eventName: string, data?: any): Promise<any[]> {
|
||||
if (!this.system) {
|
||||
throw new Error("EventFeature is not initialized");
|
||||
throw new Error("Event hook is not initialized");
|
||||
}
|
||||
let promises: Promise<any>[] = [];
|
||||
for (const plug of this.system.loadedPlugs.values()) {
|
||||
@ -31,7 +31,7 @@ export class EventFeature implements Feature<EventHook> {
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
apply(system: System<EventHook>): void {
|
||||
apply(system: System<EventHookT>): void {
|
||||
this.system = system;
|
||||
this.system.on({
|
||||
plugLoaded: (name) => {
|
||||
@ -40,7 +40,7 @@ export class EventFeature implements Feature<EventHook> {
|
||||
});
|
||||
}
|
||||
|
||||
validateManifest(manifest: Manifest<EventHook>): string[] {
|
||||
validateManifest(manifest: Manifest<EventHookT>): string[] {
|
||||
let errors = [];
|
||||
for (const [name, functionDef] of Object.entries(manifest.functions)) {
|
||||
if (functionDef.events && !Array.isArray(functionDef.events)) {
|
@ -1,14 +1,14 @@
|
||||
import { Feature, Manifest } from "../types";
|
||||
import { Hook, Manifest } from "../types";
|
||||
import cron, { ScheduledTask } from "node-cron";
|
||||
import { safeRun } from "../util";
|
||||
import { System } from "../system";
|
||||
|
||||
export type CronHook = {
|
||||
export type CronHookT = {
|
||||
cron?: string | string[];
|
||||
};
|
||||
|
||||
export class NodeCronFeature implements Feature<CronHook> {
|
||||
apply(system: System<CronHook>): void {
|
||||
export class NodeCronHook implements Hook<CronHookT> {
|
||||
apply(system: System<CronHookT>): void {
|
||||
let tasks: ScheduledTask[] = [];
|
||||
system.on({
|
||||
plugLoaded: (name, plug) => {
|
||||
@ -56,7 +56,7 @@ export class NodeCronFeature implements Feature<CronHook> {
|
||||
}
|
||||
}
|
||||
|
||||
validateManifest(manifest: Manifest<CronHook>): string[] {
|
||||
validateManifest(manifest: Manifest<CronHookT>): string[] {
|
||||
let errors = [];
|
||||
for (const [name, functionDef] of Object.entries(manifest.functions)) {
|
||||
if (!functionDef.cron) {
|
@ -10,7 +10,7 @@
|
||||
"watch": "rm -rf .parcel-cache && parcel watch",
|
||||
"build": "parcel build",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "jest dist"
|
||||
"test": "jest dist/test"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
@ -28,9 +28,9 @@
|
||||
"test": {
|
||||
"source": [
|
||||
"runtime.test.ts",
|
||||
"feature/endpoint.test.ts",
|
||||
"syscall/store.knex_node.test.ts",
|
||||
"syscall/store.dexie_browser.test.ts"
|
||||
"hooks/endpoint.test.ts",
|
||||
"syscalls/store.knex_node.test.ts",
|
||||
"syscalls/store.dexie_browser.test.ts"
|
||||
],
|
||||
"outputFormat": "commonjs",
|
||||
"isLibrary": true,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import fs from "fs/promises";
|
||||
import watch from "node-watch";
|
||||
import path from "path";
|
||||
import { createSandbox } from "./environment/node_sandbox";
|
||||
import { createSandbox } from "./environments/node_sandbox";
|
||||
import { safeRun } from "../server/util";
|
||||
import { System } from "./system";
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createSandbox } from "./environment/node_sandbox";
|
||||
import { createSandbox } from "./environments/node_sandbox";
|
||||
import { expect, test } from "@jest/globals";
|
||||
import { System } from "./system";
|
||||
|
||||
|
@ -2,7 +2,7 @@ import {
|
||||
ControllerMessage,
|
||||
WorkerLike,
|
||||
WorkerMessage,
|
||||
} from "./environment/worker";
|
||||
} from "./environments/worker";
|
||||
import { Plug } from "./plug";
|
||||
|
||||
export type SandboxFactory<HookT> = (plug: Plug<HookT>) => Sandbox;
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { SysCallMapping } from "../system";
|
||||
import { EventFeature } from "../feature/event";
|
||||
|
||||
export function eventSyscalls(eventFeature: EventFeature): SysCallMapping {
|
||||
return {
|
||||
async dispatch(ctx, eventName: string, data: any) {
|
||||
return eventFeature.dispatchEvent(eventName, data);
|
||||
},
|
||||
};
|
||||
}
|
10
plugos/syscalls/event.ts
Normal file
10
plugos/syscalls/event.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { SysCallMapping } from "../system";
|
||||
import { EventHook } from "../hooks/event";
|
||||
|
||||
export function eventSyscalls(eventHook: EventHook): SysCallMapping {
|
||||
return {
|
||||
async dispatch(ctx, eventName: string, data: any) {
|
||||
return eventHook.dispatchEvent(eventName, data);
|
||||
},
|
||||
};
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { createSandbox } from "../environment/node_sandbox";
|
||||
import { createSandbox } from "../environments/node_sandbox";
|
||||
import { expect, test } from "@jest/globals";
|
||||
import { System } from "../system";
|
||||
import { storeSyscalls } from "./store.dexie_browser";
|
@ -1,4 +1,4 @@
|
||||
import { createSandbox } from "../environment/node_sandbox";
|
||||
import { createSandbox } from "../environments/node_sandbox";
|
||||
import { expect, test } from "@jest/globals";
|
||||
import { System } from "../system";
|
||||
import {
|
@ -1,4 +1,4 @@
|
||||
import { Feature, Manifest, RuntimeEnvironment } from "./types";
|
||||
import { Hook, Manifest, RuntimeEnvironment } from "./types";
|
||||
import { EventEmitter } from "../common/event";
|
||||
import { SandboxFactory } from "./sandbox";
|
||||
import { Plug } from "./plug";
|
||||
@ -31,7 +31,7 @@ type Syscall = {
|
||||
export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
||||
protected plugs = new Map<string, Plug<HookT>>();
|
||||
protected registeredSyscalls = new Map<string, Syscall>();
|
||||
protected enabledFeatures = new Set<Feature<HookT>>();
|
||||
protected enabledHooks = new Set<Hook<HookT>>();
|
||||
|
||||
readonly runtimeEnv: RuntimeEnvironment;
|
||||
|
||||
@ -40,8 +40,8 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
||||
this.runtimeEnv = env;
|
||||
}
|
||||
|
||||
addFeature(feature: Feature<HookT>) {
|
||||
this.enabledFeatures.add(feature);
|
||||
addHook(feature: Hook<HookT>) {
|
||||
this.enabledHooks.add(feature);
|
||||
feature.apply(this);
|
||||
}
|
||||
|
||||
@ -91,7 +91,7 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
||||
}
|
||||
// Validate
|
||||
let errors: string[] = [];
|
||||
for (const feature of this.enabledFeatures) {
|
||||
for (const feature of this.enabledHooks) {
|
||||
errors = [...errors, ...feature.validateManifest(manifest)];
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"include": ["bin/*", "environment/*", "feature/**", "syscall/*", "*"],
|
||||
"include": ["bin/*", "environments/*", "hooks/**", "syscalls/*", "*"],
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"strict": true,
|
||||
|
@ -15,7 +15,7 @@ export type FunctionDef<HookT> = {
|
||||
|
||||
export type RuntimeEnvironment = "client" | "server";
|
||||
|
||||
export interface Feature<HookT> {
|
||||
export interface Hook<HookT> {
|
||||
validateManifest(manifest: Manifest<HookT>): string[];
|
||||
|
||||
apply(system: System<HookT>): void;
|
||||
|
@ -44,15 +44,10 @@ functions:
|
||||
- page:click
|
||||
insertToday:
|
||||
path: "./dates.ts:insertToday"
|
||||
command:
|
||||
name: Insert Current Date
|
||||
slashCommand: today
|
||||
slashCommand:
|
||||
name: today
|
||||
welcome:
|
||||
path: "./server.ts:welcome"
|
||||
events:
|
||||
- plug:load
|
||||
env: server
|
||||
# renderMD:
|
||||
# path: "./markdown.ts:renderMD"
|
||||
# command:
|
||||
# name: Render Markdown
|
||||
|
@ -1,7 +1,4 @@
|
||||
import {
|
||||
EndpointRequest,
|
||||
EndpointResponse,
|
||||
} from "../../plugos/feature/endpoint";
|
||||
import { EndpointRequest, EndpointResponse } from "../../plugos/hooks/endpoint";
|
||||
|
||||
export function endpointTest(req: EndpointRequest): EndpointResponse {
|
||||
console.log("I'm running on the server!", req);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Express } from "express";
|
||||
import { SilverBulletHooks } from "../common/manifest";
|
||||
import { EndpointFeature } from "../plugos/feature/endpoint";
|
||||
import { EndpointHook } from "../plugos/hooks/endpoint";
|
||||
import { readFile } from "fs/promises";
|
||||
import { System } from "../plugos/system";
|
||||
|
||||
@ -19,7 +19,7 @@ export class ExpressServer {
|
||||
this.rootPath = rootPath;
|
||||
this.system = system;
|
||||
|
||||
system.addFeature(new EndpointFeature(app, "/_"));
|
||||
system.addHook(new EndpointHook(app, "/_"));
|
||||
|
||||
// Fallback, serve index.html
|
||||
let cachedIndex: string | undefined = undefined;
|
||||
|
@ -11,9 +11,9 @@ import { stat } from "fs/promises";
|
||||
import { Cursor, cursorEffect } from "../webapp/cursorEffect";
|
||||
import { SilverBulletHooks } from "../common/manifest";
|
||||
import { System } from "../plugos/system";
|
||||
import { EventFeature } from "../plugos/feature/event";
|
||||
import { EventHook } from "../plugos/hooks/event";
|
||||
import spaceSyscalls from "./syscalls/space";
|
||||
import { eventSyscalls } from "../plugos/syscall/event";
|
||||
import { eventSyscalls } from "../plugos/syscalls/event";
|
||||
|
||||
export class PageApi implements ApiProvider {
|
||||
openPages: Map<string, Page>;
|
||||
@ -21,7 +21,7 @@ export class PageApi implements ApiProvider {
|
||||
rootPath: string;
|
||||
connectedSockets: Set<Socket>;
|
||||
private system: System<SilverBulletHooks>;
|
||||
private eventFeature: EventFeature;
|
||||
private eventHook: EventHook;
|
||||
|
||||
constructor(
|
||||
rootPath: string,
|
||||
@ -34,10 +34,10 @@ export class PageApi implements ApiProvider {
|
||||
this.openPages = openPages;
|
||||
this.connectedSockets = connectedSockets;
|
||||
this.system = system;
|
||||
this.eventFeature = new EventFeature();
|
||||
system.addFeature(this.eventFeature);
|
||||
this.eventHook = new EventHook();
|
||||
system.addHook(this.eventHook);
|
||||
system.registerSyscalls("space", [], spaceSyscalls(this));
|
||||
system.registerSyscalls("event", [], eventSyscalls(this.eventFeature));
|
||||
system.registerSyscalls("event", [], eventSyscalls(this.eventHook));
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
@ -229,11 +229,8 @@ export class PageApi implements ApiProvider {
|
||||
" to disk and indexing."
|
||||
);
|
||||
await this.flushPageToDisk(pageName, page);
|
||||
await this.eventFeature.dispatchEvent(
|
||||
"page:saved",
|
||||
pageName
|
||||
);
|
||||
await this.eventFeature.dispatchEvent("page:index", {
|
||||
await this.eventHook.dispatchEvent("page:saved", pageName);
|
||||
await this.eventHook.dispatchEvent("page:index", {
|
||||
name: pageName,
|
||||
text: page.text.sliceString(0),
|
||||
});
|
||||
@ -312,8 +309,8 @@ export class PageApi implements ApiProvider {
|
||||
this.openPages.delete(pageName);
|
||||
}
|
||||
// Trigger system events
|
||||
await this.eventFeature.dispatchEvent("page:saved", pageName);
|
||||
await this.eventFeature.dispatchEvent("page:index", {
|
||||
await this.eventHook.dispatchEvent("page:saved", pageName);
|
||||
await this.eventHook.dispatchEvent("page:index", {
|
||||
name: pageName,
|
||||
text: text,
|
||||
});
|
||||
@ -325,7 +322,7 @@ export class PageApi implements ApiProvider {
|
||||
clientConn.openPages.delete(pageName);
|
||||
// Cascading of this to all connected clients will be handled by file watcher
|
||||
await this.pageStore.deletePage(pageName);
|
||||
await this.eventFeature.dispatchEvent("page:deleted", pageName);
|
||||
await this.eventHook.dispatchEvent("page:deleted", pageName);
|
||||
},
|
||||
|
||||
listPages: async (clientConn: ClientConnection): Promise<PageMeta[]> => {
|
||||
|
@ -9,8 +9,8 @@ import {hideBin} from "yargs/helpers";
|
||||
import {SilverBulletHooks} from "../common/manifest";
|
||||
import {ExpressServer} from "./express_server";
|
||||
import {DiskPlugLoader} from "../plugos/plug_loader";
|
||||
import {NodeCronFeature} from "../plugos/feature/node_cron";
|
||||
import shellSyscalls from "../plugos/syscall/shell.node";
|
||||
import {NodeCronHook} from "../plugos/hooks/node_cron";
|
||||
import shellSyscalls from "../plugos/syscalls/shell.node";
|
||||
import {System} from "../plugos/system";
|
||||
|
||||
let args = yargs(hideBin(process.argv))
|
||||
@ -59,7 +59,7 @@ expressServer
|
||||
await plugLoader.loadPlugs();
|
||||
plugLoader.watcher();
|
||||
system.registerSyscalls("shell", ["shell"], shellSyscalls(pagesPath));
|
||||
system.addFeature(new NodeCronFeature());
|
||||
system.addHook(new NodeCronHook());
|
||||
server.listen(port, () => {
|
||||
console.log(`Server listening on port ${port}`);
|
||||
});
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
ensureTable,
|
||||
storeReadSyscalls,
|
||||
storeWriteSyscalls,
|
||||
} from "../../plugos/syscall/store.knex_node";
|
||||
} from "../../plugos/syscalls/store.knex_node";
|
||||
|
||||
type IndexItem = {
|
||||
page: string;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { AppCommand } from "../types";
|
||||
import { isMacLike } from "../util";
|
||||
import { FilterList, Option } from "./filter";
|
||||
import { faPersonRunning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { AppCommand } from "../hooks/command";
|
||||
|
||||
export function CommandPalette({
|
||||
commands,
|
||||
@ -18,7 +18,6 @@ export function CommandPalette({
|
||||
hint: isMac && def.command.mac ? def.command.mac : def.command.key,
|
||||
});
|
||||
}
|
||||
console.log("Commands", options);
|
||||
return (
|
||||
<FilterList
|
||||
label="Run"
|
||||
|
@ -1,7 +1,5 @@
|
||||
import {
|
||||
autocompletion,
|
||||
Completion,
|
||||
CompletionContext,
|
||||
completionKeymap,
|
||||
CompletionResult,
|
||||
} from "@codemirror/autocomplete";
|
||||
@ -21,7 +19,7 @@ import {
|
||||
} from "@codemirror/view";
|
||||
import React, { useEffect, useReducer } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { createSandbox as createIFrameSandbox } from "../plugos/environment/iframe_sandbox";
|
||||
import { createSandbox as createIFrameSandbox } from "../plugos/environments/iframe_sandbox";
|
||||
import { AppEvent, AppEventDispatcher, ClickEvent } from "./app_event";
|
||||
import { CollabDocument, collabExtension } from "./collab";
|
||||
import * as commands from "./commands";
|
||||
@ -40,19 +38,15 @@ import customMarkdownStyle from "./style";
|
||||
import editorSyscalls from "./syscalls/editor";
|
||||
import indexerSyscalls from "./syscalls/indexer";
|
||||
import spaceSyscalls from "./syscalls/space";
|
||||
import {
|
||||
Action,
|
||||
AppCommand,
|
||||
AppViewState,
|
||||
initialViewState,
|
||||
slashCommandRegexp,
|
||||
} from "./types";
|
||||
import { Action, AppViewState, initialViewState } from "./types";
|
||||
import { SilverBulletHooks } from "../common/manifest";
|
||||
import { safeRun } from "./util";
|
||||
import { System } from "../plugos/system";
|
||||
import { EventFeature } from "../plugos/feature/event";
|
||||
import { EventHook } from "../plugos/hooks/event";
|
||||
import { systemSyscalls } from "./syscalls/system";
|
||||
import { Panel } from "./components/panel";
|
||||
import { CommandHook } from "./hooks/command";
|
||||
import { SlashCommandHook } from "./hooks/slash_command";
|
||||
|
||||
class PageState {
|
||||
scrollTop: number;
|
||||
@ -67,22 +61,39 @@ class PageState {
|
||||
export class Editor implements AppEventDispatcher {
|
||||
private system = new System<SilverBulletHooks>("client");
|
||||
openPages = new Map<string, PageState>();
|
||||
editorCommands = new Map<string, AppCommand>();
|
||||
commandHook: CommandHook;
|
||||
editorView?: EditorView;
|
||||
viewState: AppViewState;
|
||||
viewDispatch: React.Dispatch<Action>;
|
||||
space: Space;
|
||||
navigationResolve?: (val: undefined) => void;
|
||||
pageNavigator: PathPageNavigator;
|
||||
private eventFeature: EventFeature;
|
||||
eventHook: EventHook;
|
||||
private slashCommandHook: SlashCommandHook;
|
||||
|
||||
constructor(space: Space, parent: Element) {
|
||||
this.space = space;
|
||||
this.viewState = initialViewState;
|
||||
this.viewDispatch = () => {};
|
||||
|
||||
this.eventFeature = new EventFeature();
|
||||
this.system.addFeature(this.eventFeature);
|
||||
// Event hook
|
||||
this.eventHook = new EventHook();
|
||||
this.system.addHook(this.eventHook);
|
||||
|
||||
// Command hook
|
||||
this.commandHook = new CommandHook();
|
||||
this.commandHook.on({
|
||||
commandsUpdated: (commandMap) => {
|
||||
this.viewDispatch({
|
||||
type: "update-commands",
|
||||
commands: commandMap,
|
||||
});
|
||||
},
|
||||
});
|
||||
this.system.addHook(this.commandHook);
|
||||
|
||||
// Slash command hook
|
||||
this.slashCommandHook = new SlashCommandHook(this);
|
||||
this.system.addHook(this.slashCommandHook);
|
||||
|
||||
this.render(parent);
|
||||
this.editorView = new EditorView({
|
||||
@ -142,14 +153,12 @@ export class Editor implements AppEventDispatcher {
|
||||
loadSystem: (systemJSON) => {
|
||||
safeRun(async () => {
|
||||
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.buildAllCommands();
|
||||
});
|
||||
},
|
||||
plugUnloaded: (plugName) => {
|
||||
@ -161,39 +170,10 @@ export class Editor implements AppEventDispatcher {
|
||||
});
|
||||
|
||||
if (this.pageNavigator.getCurrentPage() === "") {
|
||||
this.pageNavigator.navigate("start");
|
||||
await this.pageNavigator.navigate("start");
|
||||
}
|
||||
}
|
||||
|
||||
private buildAllCommands() {
|
||||
console.log("Loaded plugs, now updating editor commands");
|
||||
this.editorCommands.clear();
|
||||
for (let plug of this.system.loadedPlugs.values()) {
|
||||
for (const [name, functionDef] of Object.entries(
|
||||
plug.manifest!.functions
|
||||
)) {
|
||||
if (!functionDef.command) {
|
||||
continue;
|
||||
}
|
||||
const cmds = Array.isArray(functionDef.command)
|
||||
? functionDef.command
|
||||
: [functionDef.command];
|
||||
for (let cmd of cmds) {
|
||||
this.editorCommands.set(cmd.name, {
|
||||
command: cmd,
|
||||
run: () => {
|
||||
return plug.invoke(name, []);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
this.viewDispatch({
|
||||
type: "update-commands",
|
||||
commands: this.editorCommands,
|
||||
});
|
||||
}
|
||||
|
||||
flashNotification(message: string) {
|
||||
let id = Math.floor(Math.random() * 1000000);
|
||||
this.viewDispatch({
|
||||
@ -213,7 +193,7 @@ export class Editor implements AppEventDispatcher {
|
||||
}
|
||||
|
||||
async dispatchAppEvent(name: AppEvent, data?: any): Promise<any[]> {
|
||||
return this.eventFeature.dispatchEvent(name, data);
|
||||
return this.eventHook.dispatchEvent(name, data);
|
||||
}
|
||||
|
||||
get currentPage(): string | undefined {
|
||||
@ -222,7 +202,7 @@ export class Editor implements AppEventDispatcher {
|
||||
|
||||
createEditorState(pageName: string, doc: CollabDocument): EditorState {
|
||||
let commandKeyBindings: KeyBinding[] = [];
|
||||
for (let def of this.editorCommands.values()) {
|
||||
for (let def of this.commandHook.editorCommands.values()) {
|
||||
if (def.command.key) {
|
||||
commandKeyBindings.push({
|
||||
key: def.command.key,
|
||||
@ -257,7 +237,9 @@ export class Editor implements AppEventDispatcher {
|
||||
autocompletion({
|
||||
override: [
|
||||
this.plugCompleter.bind(this),
|
||||
this.commandCompleter.bind(this),
|
||||
this.slashCommandHook.slashCommandCompleter.bind(
|
||||
this.slashCommandHook
|
||||
),
|
||||
],
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
@ -361,39 +343,6 @@ export class Editor implements AppEventDispatcher {
|
||||
return null;
|
||||
}
|
||||
|
||||
commandCompleter(ctx: CompletionContext): CompletionResult | null {
|
||||
let prefix = ctx.matchBefore(slashCommandRegexp);
|
||||
if (!prefix) {
|
||||
return null;
|
||||
}
|
||||
let options: Completion[] = [];
|
||||
for (let [name, def] of this.viewState.commands) {
|
||||
if (!def.command.slashCommand) {
|
||||
continue;
|
||||
}
|
||||
options.push({
|
||||
label: def.command.slashCommand,
|
||||
detail: name,
|
||||
apply: () => {
|
||||
this.editorView?.dispatch({
|
||||
changes: {
|
||||
from: prefix!.from,
|
||||
to: ctx.pos,
|
||||
insert: "",
|
||||
},
|
||||
});
|
||||
safeRun(async () => {
|
||||
await def.run();
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
return {
|
||||
from: prefix.from + 1,
|
||||
options: options,
|
||||
};
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.editorView!.focus();
|
||||
}
|
||||
@ -469,7 +418,7 @@ export class Editor implements AppEventDispatcher {
|
||||
editor.focus();
|
||||
if (page) {
|
||||
safeRun(async () => {
|
||||
editor.navigate(page);
|
||||
await editor.navigate(page);
|
||||
});
|
||||
}
|
||||
}}
|
||||
@ -497,7 +446,7 @@ export class Editor implements AppEventDispatcher {
|
||||
dispatch({ type: "start-navigate" });
|
||||
}}
|
||||
/>
|
||||
<div id="editor"></div>
|
||||
<div id="editor" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
75
webapp/hooks/command.ts
Normal file
75
webapp/hooks/command.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { Hook, Manifest } from "../../plugos/types";
|
||||
import { System } from "../../plugos/system";
|
||||
import { EventEmitter } from "../../common/event";
|
||||
|
||||
export type CommandDef = {
|
||||
name: string;
|
||||
|
||||
// Bind to keyboard shortcut
|
||||
key?: string;
|
||||
mac?: string;
|
||||
};
|
||||
|
||||
export type AppCommand = {
|
||||
command: CommandDef;
|
||||
run: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type CommandHookT = {
|
||||
command?: CommandDef;
|
||||
};
|
||||
|
||||
export type CommandHookEvents = {
|
||||
commandsUpdated(commandMap: Map<string, AppCommand>): void;
|
||||
};
|
||||
|
||||
export class CommandHook
|
||||
extends EventEmitter<CommandHookEvents>
|
||||
implements Hook<CommandHookT>
|
||||
{
|
||||
editorCommands = new Map<string, AppCommand>();
|
||||
|
||||
buildAllCommands(system: System<CommandHookT>) {
|
||||
this.editorCommands.clear();
|
||||
for (let plug of system.loadedPlugs.values()) {
|
||||
for (const [name, functionDef] of Object.entries(
|
||||
plug.manifest!.functions
|
||||
)) {
|
||||
if (!functionDef.command) {
|
||||
continue;
|
||||
}
|
||||
const cmd = functionDef.command;
|
||||
this.editorCommands.set(cmd.name, {
|
||||
command: cmd,
|
||||
run: () => {
|
||||
return plug.invoke(name, []);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
this.emit("commandsUpdated", this.editorCommands);
|
||||
}
|
||||
|
||||
apply(system: System<CommandHookT>): void {
|
||||
this.buildAllCommands(system);
|
||||
system.on({
|
||||
plugLoaded: () => {
|
||||
this.buildAllCommands(system);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
validateManifest(manifest: Manifest<CommandHookT>): string[] {
|
||||
let errors = [];
|
||||
for (const [name, functionDef] of Object.entries(manifest.functions)) {
|
||||
if (!functionDef.command) {
|
||||
continue;
|
||||
}
|
||||
const cmd = functionDef.command;
|
||||
if (!cmd.name) {
|
||||
errors.push(`Function ${name} has a command but no name`);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
111
webapp/hooks/slash_command.ts
Normal file
111
webapp/hooks/slash_command.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { Hook, Manifest } from "../../plugos/types";
|
||||
import { System } from "../../plugos/system";
|
||||
import {
|
||||
Completion,
|
||||
CompletionContext,
|
||||
CompletionResult,
|
||||
} from "@codemirror/autocomplete";
|
||||
import { slashCommandRegexp } from "../types";
|
||||
import { safeRun } from "../util";
|
||||
import { Editor } from "../editor";
|
||||
|
||||
export type SlashCommandDef = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type AppSlashCommand = {
|
||||
slashCommand: SlashCommandDef;
|
||||
run: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type SlashCommandHookT = {
|
||||
slashCommand?: SlashCommandDef;
|
||||
};
|
||||
|
||||
export class SlashCommandHook implements Hook<SlashCommandHookT> {
|
||||
slashCommands = new Map<string, AppSlashCommand>();
|
||||
private editor: Editor;
|
||||
|
||||
constructor(editor: Editor) {
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
buildAllCommands(system: System<SlashCommandHookT>) {
|
||||
this.slashCommands.clear();
|
||||
for (let plug of system.loadedPlugs.values()) {
|
||||
for (const [name, functionDef] of Object.entries(
|
||||
plug.manifest!.functions
|
||||
)) {
|
||||
if (!functionDef.slashCommand) {
|
||||
continue;
|
||||
}
|
||||
const cmd = functionDef.slashCommand;
|
||||
this.slashCommands.set(cmd.name, {
|
||||
slashCommand: cmd,
|
||||
run: () => {
|
||||
return plug.invoke(name, []);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Completer for CodeMirror
|
||||
public slashCommandCompleter(
|
||||
ctx: CompletionContext
|
||||
): CompletionResult | null {
|
||||
let prefix = ctx.matchBefore(slashCommandRegexp);
|
||||
if (!prefix) {
|
||||
return null;
|
||||
}
|
||||
let options: Completion[] = [];
|
||||
for (let [name, def] of this.slashCommands.entries()) {
|
||||
options.push({
|
||||
label: def.slashCommand.name,
|
||||
detail: name,
|
||||
apply: () => {
|
||||
// Delete slash command part
|
||||
this.editor.editorView?.dispatch({
|
||||
changes: {
|
||||
from: prefix!.from,
|
||||
to: ctx.pos,
|
||||
insert: "",
|
||||
},
|
||||
});
|
||||
// Replace with whatever the completion is
|
||||
safeRun(async () => {
|
||||
await def.run();
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
return {
|
||||
// + 1 because of the '/'
|
||||
from: prefix.from + 1,
|
||||
options: options,
|
||||
};
|
||||
}
|
||||
|
||||
apply(system: System<SlashCommandHookT>): void {
|
||||
this.buildAllCommands(system);
|
||||
system.on({
|
||||
plugLoaded: () => {
|
||||
this.buildAllCommands(system);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
validateManifest(manifest: Manifest<SlashCommandHookT>): string[] {
|
||||
let errors = [];
|
||||
for (const [name, functionDef] of Object.entries(manifest.functions)) {
|
||||
if (!functionDef.slashCommand) {
|
||||
continue;
|
||||
}
|
||||
const cmd = functionDef.slashCommand;
|
||||
if (!cmd.name) {
|
||||
errors.push(`Function ${name} has a command but no name`);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { Space } from "../space";
|
||||
import { SysCallMapping } from "../../plugos/system";
|
||||
import { transportSyscalls } from "../../plugos/syscall/transport";
|
||||
import { transportSyscalls } from "../../plugos/syscalls/transport";
|
||||
|
||||
export default function indexerSyscalls(space: Space): SysCallMapping {
|
||||
return transportSyscalls(
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandDef } from "../common/manifest";
|
||||
import { AppCommand } from "./hooks/command";
|
||||
|
||||
export type PageMeta = {
|
||||
name: string;
|
||||
@ -7,11 +7,6 @@ export type PageMeta = {
|
||||
lastOpened?: number;
|
||||
};
|
||||
|
||||
export type AppCommand = {
|
||||
command: CommandDef;
|
||||
run: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const slashCommandRegexp = /\/[\w\-]*/;
|
||||
|
||||
export type Notification = {
|
||||
|
Loading…
Reference in New Issue
Block a user