1
0

Plug format now changed to YAML

This commit is contained in:
Zef Hemel 2022-03-27 11:26:13 +02:00
parent a382ab3baa
commit 621e55dbcf
20 changed files with 225 additions and 254 deletions

View File

@ -4,8 +4,7 @@ import { CronHook } from "../plugbox/feature/node_cron";
import { EventHook } from "../plugbox/feature/event";
export type CommandDef = {
// Function name to invoke
invoke: string;
name: string;
// Bind to keyboard shortcut
key?: string;
@ -17,9 +16,7 @@ export type CommandDef = {
};
export type SilverBulletHooks = {
commands?: {
[key: string]: CommandDef;
};
command?: CommandDef | CommandDef[];
} & EndpointHook &
CronHook &
EventHook;

View File

@ -14,7 +14,7 @@
"watch": "rm -rf .parcel-cache && parcel watch",
"build": "parcel build",
"clean": "rm -rf dist",
"plugs": "plugbox-bundle -w --dist plugs/dist plugs/core/core.plug.json plugs/git/git.plug.json",
"plugs": "plugbox-bundle -w --dist plugs/dist plugs/*/*.plug.yaml",
"server": "nodemon -w dist/server dist/server/server.js pages",
"test": "jest"
},

View File

@ -8,6 +8,7 @@ import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { Manifest } from "../types";
import { watchFile } from "fs";
import YAML from "yaml";
async function compile(filePath: string, functionName: string, debug: boolean) {
let outFile = "out.js";
@ -48,7 +49,7 @@ export default ${functionName};`
async function bundle(manifestPath: string, sourceMaps: boolean) {
const rootPath = path.dirname(manifestPath);
const manifest = JSON.parse(
const manifest = YAML.parse(
(await readFile(manifestPath)).toString()
) as Manifest<any>;
@ -71,7 +72,12 @@ async function buildManifest(
debug: boolean
) {
let generatedManifest = await bundle(manifestPath, debug);
const outPath = path.join(distPath, path.basename(manifestPath));
const outFile =
manifestPath.substring(
0,
manifestPath.length - path.extname(manifestPath).length
) + ".json";
const outPath = path.join(distPath, path.basename(outFile));
console.log("Emitting bundle to", outPath);
await writeFile(outPath, JSON.stringify(generatedManifest, null, 2));
return { generatedManifest, outPath };
@ -93,7 +99,7 @@ async function run() {
.parse();
if (args._.length === 0) {
console.log(
"Usage: plugbox-bundle [--debug] [--dist <path>] <manifest.plug.json> <manifest2.plug.json> ..."
"Usage: plugbox-bundle [--debug] [--dist <path>] <manifest.plug.yaml> <manifest2.plug.yaml> ..."
);
process.exit(1);
}

View File

@ -16,6 +16,8 @@ import {
storeWriteSyscalls,
} from "../syscall/store.knex_node";
import { fetchSyscalls } from "../syscall/fetch.node";
import { EventFeature, EventHook } from "../feature/event";
import { eventSyscalls } from "../syscall/event";
let args = yargs(hideBin(process.argv))
.option("port", {
@ -33,7 +35,7 @@ const plugPath = args._[0] as string;
const app = express();
type ServerHook = EndpointHook & CronHook;
type ServerHook = EndpointHook & CronHook & EventHook;
const system = new System<ServerHook>("server");
safeRun(async () => {
@ -51,6 +53,9 @@ safeRun(async () => {
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.registerSyscalls("shell", [], shellSyscalls("."));
system.registerSyscalls("fetch", [], fetchSyscalls());

View File

@ -13,6 +13,9 @@ test("Run a plugbox endpoint server", async () => {
{
functions: {
testhandler: {
http: {
path: "/",
},
code: `(() => {
return {
default: (req) => {
@ -23,9 +26,6 @@ test("Run a plugbox endpoint server", async () => {
})()`,
},
},
hooks: {
endpoints: [{ method: "GET", path: "/", handler: "testhandler" }],
},
} as Manifest<EndpointHook>,
createSandbox
);

View File

@ -17,13 +17,12 @@ export type EndpointResponse = {
};
export type EndpointHook = {
endpoints?: EndPointDef[];
http?: EndPointDef | EndPointDef[];
};
export type EndPointDef = {
method: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS";
method?: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "ANY";
path: string;
handler: string; // function name
};
export class EndpointFeature implements Feature<EndpointHook> {
@ -43,35 +42,42 @@ export class EndpointFeature implements Feature<EndpointHook> {
console.log("Endpoint request", req.path);
Promise.resolve()
.then(async () => {
// Iterate over all loaded plugins
for (const [plugName, plug] of system.loadedPlugs.entries()) {
const manifest = plug.manifest;
if (!manifest) {
continue;
}
const endpoints = manifest.hooks?.endpoints;
console.log("Checking plug", plugName, endpoints);
if (endpoints) {
let prefix = `${this.prefix}/${plugName}`;
console.log("Need prefix", prefix, "got", req.path);
if (!req.path.startsWith(prefix)) {
const functions = manifest.functions;
console.log("Checking plug", plugName);
let prefix = `${this.prefix}/${plugName}`;
if (!req.path.startsWith(prefix)) {
continue;
}
for (const [name, functionDef] of Object.entries(functions)) {
if (!functionDef.http) {
continue;
}
for (const { path, method, handler } of endpoints) {
let endpoints = Array.isArray(functionDef.http)
? functionDef.http
: [functionDef.http];
console.log(endpoints);
for (const { path, method } of endpoints) {
let prefixedPath = `${prefix}${path}`;
if (prefixedPath === req.path && method === req.method) {
if (
prefixedPath === req.path &&
((method || "GET") === req.method || method === "ANY")
) {
try {
const response: EndpointResponse = await plug.invoke(
handler,
[
{
path: req.path,
method: req.method,
body: req.body,
query: req.query,
headers: req.headers,
} as EndpointRequest,
]
);
const response: EndpointResponse = await plug.invoke(name, [
{
path: req.path,
method: req.method,
body: req.body,
query: req.query,
headers: req.headers,
} as EndpointRequest,
]);
let resp = res.status(response.status);
if (response.headers) {
for (const [key, value] of Object.entries(
@ -101,21 +107,26 @@ export class EndpointFeature implements Feature<EndpointHook> {
}
validateManifest(manifest: Manifest<EndpointHook>): string[] {
const endpoints = manifest.hooks.endpoints;
let errors = [];
if (endpoints) {
for (let { method, path, handler } of endpoints) {
for (const [name, functionDef] of Object.entries(manifest.functions)) {
if (!functionDef.http) {
continue;
}
let endpoints = Array.isArray(functionDef.http)
? functionDef.http
: [functionDef.http];
for (let { path, method } of endpoints) {
if (!path) {
errors.push("Path not defined for endpoint");
}
if (["GET", "POST", "PUT", "DELETE"].indexOf(method) === -1) {
if (
method &&
["GET", "POST", "PUT", "DELETE", "ANY"].indexOf(method) === -1
) {
errors.push(
`Invalid method ${method} for end point with with ${path}`
);
}
if (!manifest.functions[handler]) {
errors.push(`Endpoint handler function ${handler} not found`);
}
}
}
return errors;

View File

@ -2,29 +2,24 @@ import { Feature, Manifest } from "../types";
import { System } from "../system";
export type EventHook = {
events?: { [key: string]: string[] };
events?: string[];
};
export class EventFeature implements Feature<EventHook> {
private system?: System<EventHook>;
async dispatchEvent(name: string, data?: any): Promise<any[]> {
async dispatchEvent(eventName: 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]));
}
});
for (const [name, functionDef] of Object.entries(
plug.manifest!.functions
)) {
if (functionDef.events && functionDef.events.includes(eventName)) {
promises.push(plug.invoke(name, [data]));
}
}
}
return Promise.all(promises);
@ -32,12 +27,15 @@ export class EventFeature implements Feature<EventHook> {
apply(system: System<EventHook>): void {
this.system = system;
system.on({
plugLoaded: (name, plug) => {},
});
}
validateManifest(manifest: Manifest<EventHook>): string[] {
return [];
let errors = [];
for (const [name, functionDef] of Object.entries(manifest.functions)) {
if (functionDef.events && !Array.isArray(functionDef.events)) {
errors.push("'events' key must be an array of strings");
}
}
return errors;
}
}

View File

@ -4,12 +4,7 @@ import { safeRun } from "../util";
import { System } from "../system";
export type CronHook = {
crons?: CronDef[];
};
export type CronDef = {
cron: string;
handler: string; // function name
cron?: string | string[];
};
export class NodeCronFeature implements Feature<CronHook> {
@ -27,19 +22,28 @@ export class NodeCronFeature implements Feature<CronHook> {
reloadCrons();
function reloadCrons() {
// ts-ignore
tasks.forEach((task) => task.stop());
tasks = [];
for (let plug of system.loadedPlugs.values()) {
const crons = plug.manifest?.hooks?.crons;
if (crons) {
if (!plug.manifest) {
continue;
}
for (const [name, functionDef] of Object.entries(
plug.manifest.functions
)) {
if (!functionDef.cron) {
continue;
}
const crons = Array.isArray(functionDef.cron)
? functionDef.cron
: [functionDef.cron];
for (let cronDef of crons) {
tasks.push(
cron.schedule(cronDef.cron, () => {
console.log("Now acting on cron", cronDef.cron);
cron.schedule(cronDef, () => {
console.log("Now acting on cron", cronDef);
safeRun(async () => {
try {
await plug.invoke(cronDef.handler, []);
await plug.invoke(name, [cronDef]);
} catch (e: any) {
console.error("Execution of cron function failed", e);
}
@ -53,15 +57,17 @@ export class NodeCronFeature implements Feature<CronHook> {
}
validateManifest(manifest: Manifest<CronHook>): string[] {
const crons = manifest.hooks.crons;
let errors = [];
if (crons) {
for (const [name, functionDef] of Object.entries(manifest.functions)) {
if (!functionDef.cron) {
continue;
}
const crons = Array.isArray(functionDef.cron)
? functionDef.cron
: [functionDef.cron];
for (let cronDef of crons) {
if (!cron.validate(cronDef.cron)) {
errors.push(`Invalid cron expression ${cronDef.cron}`);
}
if (!manifest.functions[cronDef.handler]) {
errors.push(`Cron handler function ${cronDef.handler} not found`);
if (!cron.validate(cronDef)) {
errors.push(`Invalid cron expression ${cronDef}`);
}
}
}

View File

@ -82,9 +82,6 @@ test("Run a Node sandbox", async () => {
})()`,
},
},
hooks: {
events: {},
},
},
createSandbox
);

10
plugbox/syscall/event.ts Normal file
View File

@ -0,0 +1,10 @@
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);
},
};
}

View File

@ -3,11 +3,11 @@ import { SysCallMapping } from "../system";
export function fetchSyscalls(): SysCallMapping {
return {
async fetchJson(ctx, url: RequestInfo, init: RequestInit) {
async json(ctx, url: RequestInfo, init: RequestInit) {
let resp = await fetch(url, init);
return resp.json();
},
async fetchText(ctx, url: RequestInfo, init: RequestInit) {
async text(ctx, url: RequestInfo, init: RequestInit) {
let resp = await fetch(url, init);
return resp.text();
},

View File

@ -12,7 +12,6 @@ test("Test store", async () => {
let plug = await system.load(
"test",
{
hooks: {},
functions: {
test1: {
code: `(() => {

View File

@ -28,7 +28,6 @@ test("Test store", async () => {
let plug = await system.load(
"test",
{
hooks: {},
functions: {
test1: {
code: `(() => {

View File

@ -2,17 +2,16 @@ import { System } from "./system";
export interface Manifest<HookT> {
requiredPermissions?: string[];
hooks: HookT;
functions: {
[key: string]: FunctionDef;
[key: string]: FunctionDef<HookT>;
};
}
export interface FunctionDef {
export type FunctionDef<HookT> = {
path?: string;
code?: string;
env?: RuntimeEnvironment;
}
} & HookT;
export type RuntimeEnvironment = "client" | "server";

View File

@ -1,95 +0,0 @@
{
"hooks": {
"commands": {
"Navigate To page": {
"invoke": "linkNavigate",
"key": "Ctrl-Enter",
"mac": "Cmd-Enter"
},
"Insert Current Date": {
"invoke": "insertToday",
"slashCommand": "today"
},
"Toggle : Heading 1": {
"invoke": "toggle_h1",
"mac": "Cmd-1",
"key": "Ctrl-1"
},
"Toggle : Heading 2": {
"invoke": "toggle_h2",
"mac": "Cmd-2",
"key": "Ctrl-2"
},
"Page: Delete": {
"invoke": "deletePage"
},
"Page: Rename": {
"invoke": "renamePage"
},
"Pages: Reindex": {
"invoke": "reindexPages"
},
"Pages: Back Links": {
"invoke": "showBackLinks"
}
},
"events": {
"page:click": ["taskToggle", "clickNavigate"],
"editor:complete": ["pageComplete"],
"page:index": ["indexLinks"],
"load": ["welcome"]
},
"endpoints": [
{
"method": "GET",
"path": "/",
"handler": "endpointTest"
}
]
},
"functions": {
"indexLinks": {
"path": "./page.ts:indexLinks"
},
"deletePage": {
"path": "./page.ts:deletePage"
},
"showBackLinks": {
"path": "./page.ts:showBackLinks"
},
"renamePage": {
"path": "./page.ts:renamePage"
},
"reindexPages": {
"path": "./page.ts:reindex"
},
"pageComplete": {
"path": "./navigate.ts:pageComplete"
},
"linkNavigate": {
"path": "./navigate.ts:linkNavigate"
},
"clickNavigate": {
"path": "./navigate.ts:clickNavigate"
},
"taskToggle": {
"path": "./task.ts:taskToggle"
},
"insertToday": {
"path": "./dates.ts:insertToday"
},
"toggle_h1": {
"path": "./markup.ts:toggleH1"
},
"toggle_h2": {
"path": "./markup.ts:toggleH2"
},
"endpointTest": {
"path": "./server.ts:endpointTest"
},
"welcome": {
"path": "./server.ts:welcome",
"env": "server"
}
}
}

46
plugs/core/core.plug.yaml Normal file
View File

@ -0,0 +1,46 @@
functions:
indexLinks:
path: "./page.ts:indexLinks"
events:
- page:index
deletePage:
path: "./page.ts:deletePage"
command:
name: "Page: Delete"
showBackLinks:
path: "./page.ts:showBackLinks"
command:
name: "Page: Back Links"
renamePage:
path: "./page.ts:renamePage"
command:
name: "Page: Rename"
pageComplete:
path: "./navigate.ts:pageComplete"
events:
- editor:complete
linkNavigate:
path: "./navigate.ts:linkNavigate"
command:
name: Navigate To page
key: Ctrl-Enter
mac: Cmd-Enter
clickNavigate:
path: "./navigate.ts:clickNavigate"
events:
- page:click
taskToggle:
path: "./task.ts:taskToggle"
events:
- page:click
insertToday:
path: "./dates.ts:insertToday"
command:
name: Insert Current Date
slashCommand: today
welcome:
path: "./server.ts:welcome"
events:
- load
env: server

View File

@ -1,37 +0,0 @@
{
"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"
}
}
}

22
plugs/git/git.plug.yaml Normal file
View File

@ -0,0 +1,22 @@
requiredPermissions:
- shell
functions:
snapshotCommand:
path: "./git.ts:snapshotCommand"
env: client
command:
name: "Git: Snapshot"
syncCommand:
path: "./git.ts:syncCommand"
env: client
command:
name: "Git: Sync"
commit:
path: "./git.ts:commit"
env: server
cron:
- "*/15 * * * *"
sync:
path: "./git.ts:sync"
env: server

View File

@ -9,16 +9,16 @@ import {hideBin} from "yargs/helpers";
import {SilverBulletHooks} from "../common/manifest";
import {ExpressServer} from "./express_server";
import {DiskPlugLoader} from "../plugbox/plug_loader";
import { NodeCronFeature } from "../plugbox/feature/node_cron";
import {NodeCronFeature} from "../plugbox/feature/node_cron";
import shellSyscalls from "../plugbox/syscall/shell.node";
import { System } from "../plugbox/system";
import {System} from "../plugbox/system";
let args = yargs(hideBin(process.argv))
.option("port", {
type: "number",
default: 3000,
})
.parse();
.option("port", {
type: "number",
default: 3000,
})
.parse();
if (!args._.length) {
console.error("Usage: silverbullet <path-to-pages>");
@ -50,20 +50,20 @@ socketServer.init().catch((e) => {
const expressServer = new ExpressServer(app, pagesPath, distDir, system);
expressServer
.init()
.then(async () => {
let plugLoader = new DiskPlugLoader(
system,
`${__dirname}/../../plugs/dist`
);
await plugLoader.loadPlugs();
plugLoader.watcher();
system.registerSyscalls("shell", ["shell"], shellSyscalls(pagesPath));
system.addFeature(new NodeCronFeature());
server.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
})
.catch((e) => {
console.error(e);
.init()
.then(async () => {
let plugLoader = new DiskPlugLoader(
system,
`${__dirname}/../../plugs/dist`
);
await plugLoader.loadPlugs();
plugLoader.watcher();
system.registerSyscalls("shell", ["shell"], shellSyscalls(pagesPath));
system.addFeature(new NodeCronFeature());
server.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
})
.catch((e) => {
console.error(e);
});

View File

@ -163,15 +163,23 @@ export class Editor implements AppEventDispatcher {
console.log("Loaded plugs, now updating editor commands");
this.editorCommands.clear();
for (let plug of this.system.loadedPlugs.values()) {
const cmds = plug.manifest!.hooks.commands;
for (let name in cmds) {
let cmd = cmds[name];
this.editorCommands.set(name, {
command: cmd,
run: async (arg): Promise<any> => {
return await plug.invoke(cmd.invoke, [arg]);
},
});
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: async (arg): Promise<any> => {
return await plug.invoke(name, [arg]);
},
});
}
}
}
this.viewDispatch({