Reload plugins on change

This commit is contained in:
Gregory Schier
2024-09-09 11:34:52 -07:00
parent 3bf192953d
commit c0707bb246
18 changed files with 240 additions and 118 deletions

View File

@@ -4,30 +4,35 @@ import { Worker } from 'node:worker_threads';
import { EventChannel } from './EventChannel';
export class PluginHandle {
readonly #worker: Worker;
#worker: Worker;
constructor(
readonly pluginDir: string,
readonly pluginRefId: string,
readonly events: EventChannel,
) {
const workerPath = process.env.YAAK_WORKER_PATH ?? path.join(__dirname, 'index.worker.cjs');
this.#worker = new Worker(workerPath, {
workerData: {
pluginDir,
pluginRefId,
},
});
this.#worker.on('message', (e) => this.events.emit(e));
this.#worker.on('error', this.#handleError.bind(this));
this.#worker.on('exit', this.#handleExit.bind(this));
this.#worker = this.#createWorker();
}
sendToWorker(event: InternalEvent) {
this.#worker.postMessage(event);
}
#createWorker(): Worker {
const workerPath = process.env.YAAK_WORKER_PATH ?? path.join(__dirname, 'index.worker.cjs');
const worker = new Worker(workerPath, {
workerData: { pluginDir: this.pluginDir, pluginRefId: this.pluginRefId },
});
worker.on('message', (e) => this.events.emit(e));
worker.on('error', this.#handleError.bind(this));
worker.on('exit', this.#handleExit.bind(this));
console.log('Created plugin worker for ', this.pluginDir);
return worker;
}
async #handleError(err: Error) {
console.error('Plugin errored', this.pluginDir, err);
}
@@ -36,7 +41,7 @@ export class PluginHandle {
if (code === 0) {
console.log('Plugin exited successfully', this.pluginDir);
} else {
console.log('Plugin exited with error', code, this.pluginDir);
console.log('Plugin exited with status', code, this.pluginDir);
}
}
}

View File

@@ -10,34 +10,31 @@ import {
SendHttpRequestResponse,
TemplateFunction,
} from '@yaakapp/api';
import { HttpRequestActionPlugin } from '@yaakapp/api/lib/plugins/httpRequestAction';
import { HttpRequestActionPlugin } from '@yaakapp/api/lib/plugins/HttpRequestActionPlugin';
import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
import interceptStdout from 'intercept-stdout';
import * as console from 'node:console';
import { readFileSync } from 'node:fs';
import { readFileSync, watch } from 'node:fs';
import path from 'node:path';
import * as util from 'node:util';
import { parentPort, workerData } from 'node:worker_threads';
new Promise<void>(async (resolve, reject) => {
async function initialize() {
const { pluginDir, pluginRefId } = workerData;
const pathPkg = path.join(pluginDir, 'package.json');
// NOTE: Use POSIX join because require() needs forward slash
const pathMod = path.posix.join(pluginDir, 'build', 'index.js');
let pkg: { [x: string]: any };
try {
pkg = JSON.parse(readFileSync(pathPkg, 'utf8'));
} catch (err) {
// TODO: Do something better here
reject(err);
return;
async function importModule() {
const id = require.resolve(pathMod);
delete require.cache[id];
return require(id);
}
const pkg = JSON.parse(readFileSync(pathPkg, 'utf8'));
prefixStdout(`[plugin][${pkg.name}] %s`);
const mod = (await import(pathMod)).default ?? {};
let mod = await importModule();
const capabilities: string[] = [];
if (typeof mod.pluginHookExport === 'function') capabilities.push('export');
@@ -94,6 +91,18 @@ new Promise<void>(async (resolve, reject) => {
return promise as unknown as Promise<T>;
}
async function reloadModule() {
mod = await importModule();
}
// Reload plugin if JS or package.json changes
const cb = async () => {
await reloadModule();
return sendPayload({ type: 'reload_response' }, null);
};
watch(path.join(pathMod), cb);
watch(path.join(pathPkg), cb);
const ctx: Context = {
clipboard: {
async copyText(text) {
@@ -248,6 +257,10 @@ new Promise<void>(async (resolve, reject) => {
return;
}
}
if (payload.type === 'reload_request') {
await reloadModule();
}
} catch (err) {
console.log('Plugin call threw exception', payload.type, err);
// TODO: Return errors to server
@@ -256,9 +269,9 @@ new Promise<void>(async (resolve, reject) => {
// No matches, so send back an empty response so the caller doesn't block forever
sendEmpty(replyId);
});
}
resolve();
}).catch((err) => {
initialize().catch((err) => {
console.log('failed to boot plugin', err);
});