mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-19 23:31:21 +02:00
Reduce plugin runtime memory
This commit is contained in:
@@ -3,10 +3,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"bootstrap": "npm run build",
|
"bootstrap": "npm run build",
|
||||||
"build": "run-p build:*",
|
"build": "run-p build:*",
|
||||||
"build:main": "esbuild src/index.ts --bundle --platform=node --outfile=../../src-tauri/vendored/plugin-runtime/index.cjs",
|
"build:main": "esbuild src/index.ts --bundle --platform=node --outfile=../../src-tauri/vendored/plugin-runtime/index.cjs"
|
||||||
"build:worker": "esbuild src/index.worker.ts --bundle --platform=node --outfile=../../src-tauri/vendored/plugin-runtime/index.worker.cjs",
|
|
||||||
"build:__main": "esbuild src/index.ts --bundle --platform=node --outfile=../../src-tauri/target/debug/vendored/plugin-runtime/index.cjs",
|
|
||||||
"build:__worker": "esbuild src/index.ts --bundle --platform=node --outfile=../../src-tauri/target/debug/vendored/plugin-runtime/index.worker.cjs"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
|
|||||||
@@ -1,56 +1,28 @@
|
|||||||
import type { BootRequest, InternalEvent } from '@yaakapp/api';
|
import type { BootRequest, InternalEvent } from '@yaakapp/api';
|
||||||
import path from 'node:path';
|
|
||||||
import { Worker } from 'node:worker_threads';
|
|
||||||
import type { EventChannel } from './EventChannel';
|
import type { EventChannel } from './EventChannel';
|
||||||
import type { PluginWorkerData } from './index.worker';
|
import { PluginInstance, PluginWorkerData } from './PluginInstance';
|
||||||
|
|
||||||
export class PluginHandle {
|
export class PluginHandle {
|
||||||
#worker: Worker;
|
#instance: PluginInstance;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly pluginRefId: string,
|
readonly pluginRefId: string,
|
||||||
readonly bootRequest: BootRequest,
|
readonly bootRequest: BootRequest,
|
||||||
readonly events: EventChannel,
|
readonly pluginToAppEvents: EventChannel,
|
||||||
) {
|
) {
|
||||||
this.#worker = this.#createWorker();
|
|
||||||
}
|
|
||||||
|
|
||||||
sendToWorker(event: InternalEvent) {
|
|
||||||
this.#worker.postMessage(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
async terminate() {
|
|
||||||
await this.#worker.terminate();
|
|
||||||
}
|
|
||||||
|
|
||||||
#createWorker(): Worker {
|
|
||||||
const workerPath = process.env.YAAK_WORKER_PATH ?? path.join(__dirname, 'index.worker.cjs');
|
|
||||||
const workerData: PluginWorkerData = {
|
const workerData: PluginWorkerData = {
|
||||||
pluginRefId: this.pluginRefId,
|
pluginRefId: this.pluginRefId,
|
||||||
bootRequest: this.bootRequest,
|
bootRequest: this.bootRequest,
|
||||||
};
|
};
|
||||||
const worker = new Worker(workerPath, {
|
this.#instance = new PluginInstance(workerData, pluginToAppEvents);
|
||||||
workerData,
|
|
||||||
});
|
|
||||||
|
|
||||||
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.bootRequest.dir);
|
console.log('Created plugin worker for ', this.bootRequest.dir);
|
||||||
|
|
||||||
return worker;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async #handleError(err: Error) {
|
sendToWorker(event: InternalEvent) {
|
||||||
console.error('Plugin errored', this.bootRequest.dir, err);
|
this.#instance.postMessage(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #handleExit(code: number) {
|
terminate() {
|
||||||
if (code === 0) {
|
this.#instance.terminate();
|
||||||
console.log('Plugin exited successfully', this.bootRequest.dir);
|
|
||||||
} else {
|
|
||||||
console.log('Plugin exited with status', code, this.bootRequest.dir);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
596
packages/plugin-runtime/src/PluginInstance.ts
Normal file
596
packages/plugin-runtime/src/PluginInstance.ts
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
import type {
|
||||||
|
BootRequest,
|
||||||
|
Context,
|
||||||
|
DeleteKeyValueResponse,
|
||||||
|
FindHttpResponsesResponse,
|
||||||
|
FormInput,
|
||||||
|
GetHttpRequestByIdResponse,
|
||||||
|
GetKeyValueResponse,
|
||||||
|
HttpAuthenticationAction,
|
||||||
|
HttpRequestAction,
|
||||||
|
InternalEvent,
|
||||||
|
InternalEventPayload,
|
||||||
|
JsonPrimitive,
|
||||||
|
PluginDefinition,
|
||||||
|
PromptTextResponse,
|
||||||
|
RenderHttpRequestResponse,
|
||||||
|
SendHttpRequestResponse,
|
||||||
|
TemplateFunction,
|
||||||
|
TemplateRenderResponse,
|
||||||
|
WindowContext,
|
||||||
|
} from '@yaakapp/api';
|
||||||
|
import console from 'node:console';
|
||||||
|
import { readFileSync, type Stats, statSync, watch } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
// import util from 'node:util';
|
||||||
|
import { EventChannel } from './EventChannel';
|
||||||
|
// import { interceptStdout } from './interceptStdout';
|
||||||
|
import { migrateTemplateFunctionSelectOptions } from './migrations';
|
||||||
|
|
||||||
|
export interface PluginWorkerData {
|
||||||
|
bootRequest: BootRequest;
|
||||||
|
pluginRefId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PluginInstance {
|
||||||
|
#workerData: PluginWorkerData;
|
||||||
|
#mod: PluginDefinition;
|
||||||
|
#pkg: { name?: string; version?: string };
|
||||||
|
#pluginToAppEvents: EventChannel;
|
||||||
|
#appToPluginEvents: EventChannel;
|
||||||
|
|
||||||
|
constructor(workerData: PluginWorkerData, pluginEvents: EventChannel) {
|
||||||
|
this.#workerData = workerData;
|
||||||
|
this.#pluginToAppEvents = pluginEvents;
|
||||||
|
this.#appToPluginEvents = new EventChannel();
|
||||||
|
|
||||||
|
// Forward incoming events to onMessage()
|
||||||
|
this.#appToPluginEvents.listen(async event => {
|
||||||
|
await this.#onMessage(event);
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reload plugin if the JS or package.json changes
|
||||||
|
const windowContextNone: WindowContext = { type: 'none' };
|
||||||
|
const fileChangeCallback = async () => {
|
||||||
|
this.#importModule();
|
||||||
|
return this.#sendPayload(windowContextNone, { type: 'reload_response' }, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.#workerData.bootRequest.watch) {
|
||||||
|
watchFile(this.#pathMod(), fileChangeCallback);
|
||||||
|
watchFile(this.#pathPkg(), fileChangeCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#mod = {};
|
||||||
|
this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8'));
|
||||||
|
|
||||||
|
// TODO: Re-implement this now that we're not using workers
|
||||||
|
// prefixStdout(`[plugin][${this.#pkg.name}] %s`);
|
||||||
|
|
||||||
|
this.#importModule();
|
||||||
|
}
|
||||||
|
|
||||||
|
postMessage(event: InternalEvent) {
|
||||||
|
this.#appToPluginEvents.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate() {
|
||||||
|
this.#unimportModule();
|
||||||
|
}
|
||||||
|
|
||||||
|
async #onMessage(event: InternalEvent) {
|
||||||
|
const ctx = this.#newCtx(event);
|
||||||
|
|
||||||
|
const { windowContext, payload, id: replyId } = event;
|
||||||
|
try {
|
||||||
|
if (payload.type === 'boot_request') {
|
||||||
|
// console.log('Plugin initialized', pkg.name, { capabilities, enableWatch });
|
||||||
|
const payload: InternalEventPayload = {
|
||||||
|
type: 'boot_response',
|
||||||
|
name: this.#pkg.name ?? 'unknown',
|
||||||
|
version: this.#pkg.version ?? '0.0.1',
|
||||||
|
};
|
||||||
|
this.#sendPayload(windowContext, payload, replyId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.type === 'terminate_request') {
|
||||||
|
const payload: InternalEventPayload = {
|
||||||
|
type: 'terminate_response',
|
||||||
|
};
|
||||||
|
this.#sendPayload(windowContext, payload, replyId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
payload.type === 'import_request' &&
|
||||||
|
typeof this.#mod?.importer?.onImport === 'function'
|
||||||
|
) {
|
||||||
|
const reply = await this.#mod.importer.onImport(ctx, {
|
||||||
|
text: payload.content,
|
||||||
|
});
|
||||||
|
if (reply != null) {
|
||||||
|
const replyPayload: InternalEventPayload = {
|
||||||
|
type: 'import_response',
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
resources: reply.resources as any,
|
||||||
|
};
|
||||||
|
this.#sendPayload(windowContext, replyPayload, replyId);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Continue, to send back an empty reply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.type === 'filter_request' && typeof this.#mod?.filter?.onFilter === 'function') {
|
||||||
|
const reply = await this.#mod.filter.onFilter(ctx, {
|
||||||
|
filter: payload.filter,
|
||||||
|
payload: payload.content,
|
||||||
|
mimeType: payload.type,
|
||||||
|
});
|
||||||
|
const replyPayload: InternalEventPayload = {
|
||||||
|
type: 'filter_response',
|
||||||
|
content: reply.filtered,
|
||||||
|
};
|
||||||
|
this.#sendPayload(windowContext, replyPayload, replyId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
payload.type === 'get_http_request_actions_request' &&
|
||||||
|
Array.isArray(this.#mod?.httpRequestActions)
|
||||||
|
) {
|
||||||
|
const reply: HttpRequestAction[] = this.#mod.httpRequestActions.map((a) => ({
|
||||||
|
...a,
|
||||||
|
// Add everything except onSelect
|
||||||
|
onSelect: undefined,
|
||||||
|
}));
|
||||||
|
const replyPayload: InternalEventPayload = {
|
||||||
|
type: 'get_http_request_actions_response',
|
||||||
|
pluginRefId: this.#workerData.pluginRefId,
|
||||||
|
actions: reply,
|
||||||
|
};
|
||||||
|
this.#sendPayload(windowContext, replyPayload, replyId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
payload.type === 'get_template_functions_request' &&
|
||||||
|
Array.isArray(this.#mod?.templateFunctions)
|
||||||
|
) {
|
||||||
|
const reply: TemplateFunction[] = this.#mod.templateFunctions.map((templateFunction) => {
|
||||||
|
return {
|
||||||
|
...migrateTemplateFunctionSelectOptions(templateFunction),
|
||||||
|
// Add everything except render
|
||||||
|
onRender: undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const replyPayload: InternalEventPayload = {
|
||||||
|
type: 'get_template_functions_response',
|
||||||
|
pluginRefId: this.#workerData.pluginRefId,
|
||||||
|
functions: reply,
|
||||||
|
};
|
||||||
|
this.#sendPayload(windowContext, replyPayload, replyId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.type === 'get_http_authentication_summary_request' && this.#mod?.authentication) {
|
||||||
|
const { name, shortLabel, label } = this.#mod.authentication;
|
||||||
|
const replyPayload: InternalEventPayload = {
|
||||||
|
type: 'get_http_authentication_summary_response',
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
shortLabel,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#sendPayload(windowContext, replyPayload, replyId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) {
|
||||||
|
const { args, actions } = this.#mod.authentication;
|
||||||
|
const resolvedArgs: FormInput[] = [];
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
let v = args[i];
|
||||||
|
if ('dynamic' in v) {
|
||||||
|
const dynamicAttrs = await v.dynamic(ctx, payload);
|
||||||
|
const { dynamic, ...other } = v;
|
||||||
|
resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput);
|
||||||
|
} else {
|
||||||
|
resolvedArgs.push(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const resolvedActions: HttpAuthenticationAction[] = [];
|
||||||
|
for (const { onSelect, ...action } of actions ?? []) {
|
||||||
|
resolvedActions.push(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
const replyPayload: InternalEventPayload = {
|
||||||
|
type: 'get_http_authentication_config_response',
|
||||||
|
args: resolvedArgs,
|
||||||
|
actions: resolvedActions,
|
||||||
|
pluginRefId: this.#workerData.pluginRefId,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#sendPayload(windowContext, replyPayload, replyId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.type === 'call_http_authentication_request' && this.#mod?.authentication) {
|
||||||
|
const auth = this.#mod.authentication;
|
||||||
|
if (typeof auth?.onApply === 'function') {
|
||||||
|
applyFormInputDefaults(auth.args, payload.values);
|
||||||
|
const result = await auth.onApply(ctx, payload);
|
||||||
|
this.#sendPayload(
|
||||||
|
windowContext,
|
||||||
|
{
|
||||||
|
type: 'call_http_authentication_response',
|
||||||
|
setHeaders: result.setHeaders,
|
||||||
|
},
|
||||||
|
replyId,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
payload.type === 'call_http_authentication_action_request' &&
|
||||||
|
this.#mod.authentication != null
|
||||||
|
) {
|
||||||
|
const action = this.#mod.authentication.actions?.[payload.index];
|
||||||
|
if (typeof action?.onSelect === 'function') {
|
||||||
|
await action.onSelect(ctx, payload.args);
|
||||||
|
this.#sendEmpty(windowContext, replyId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
payload.type === 'call_http_request_action_request' &&
|
||||||
|
Array.isArray(this.#mod.httpRequestActions)
|
||||||
|
) {
|
||||||
|
const action = this.#mod.httpRequestActions[payload.index];
|
||||||
|
if (typeof action?.onSelect === 'function') {
|
||||||
|
await action.onSelect(ctx, payload.args);
|
||||||
|
this.#sendEmpty(windowContext, replyId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
payload.type === 'call_template_function_request' &&
|
||||||
|
Array.isArray(this.#mod?.templateFunctions)
|
||||||
|
) {
|
||||||
|
const action = this.#mod.templateFunctions.find((a) => a.name === payload.name);
|
||||||
|
if (typeof action?.onRender === 'function') {
|
||||||
|
applyFormInputDefaults(action.args, payload.args.values);
|
||||||
|
const result = await action.onRender(ctx, payload.args);
|
||||||
|
this.#sendPayload(
|
||||||
|
windowContext,
|
||||||
|
{
|
||||||
|
type: 'call_template_function_response',
|
||||||
|
value: result ?? null,
|
||||||
|
},
|
||||||
|
replyId,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.type === 'reload_request') {
|
||||||
|
this.#importModule();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Plugin call threw exception', payload.type, err);
|
||||||
|
this.#sendPayload(
|
||||||
|
windowContext,
|
||||||
|
{
|
||||||
|
type: 'error_response',
|
||||||
|
error: `${err}`,
|
||||||
|
},
|
||||||
|
replyId,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No matches, so send back an empty response so the caller doesn't block forever
|
||||||
|
this.#sendEmpty(windowContext, replyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pathMod() {
|
||||||
|
return path.posix.join(this.#workerData.bootRequest.dir, 'build', 'index.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
#pathPkg() {
|
||||||
|
return path.join(this.#workerData.bootRequest.dir, 'package.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
#unimportModule() {
|
||||||
|
const id = require.resolve(this.#pathMod());
|
||||||
|
delete require.cache[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
#importModule() {
|
||||||
|
const id = require.resolve(this.#pathMod());
|
||||||
|
delete require.cache[id];
|
||||||
|
this.#mod = require(id).plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
#buildEventToSend(
|
||||||
|
windowContext: WindowContext,
|
||||||
|
payload: InternalEventPayload,
|
||||||
|
replyId: string | null = null,
|
||||||
|
): InternalEvent {
|
||||||
|
return {
|
||||||
|
pluginRefId: this.#workerData.pluginRefId,
|
||||||
|
pluginName: path.basename(this.#workerData.bootRequest.dir),
|
||||||
|
id: genId(),
|
||||||
|
replyId,
|
||||||
|
payload,
|
||||||
|
windowContext,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#sendPayload(
|
||||||
|
windowContext: WindowContext,
|
||||||
|
payload: InternalEventPayload,
|
||||||
|
replyId: string | null,
|
||||||
|
): string {
|
||||||
|
const event = this.#buildEventToSend(windowContext, payload, replyId);
|
||||||
|
this.#sendEvent(event);
|
||||||
|
return event.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sendEvent(event: InternalEvent) {
|
||||||
|
if (event.payload.type !== 'empty_response') {
|
||||||
|
console.log('Sending event to app', event.id, event.payload.type);
|
||||||
|
}
|
||||||
|
this.#pluginToAppEvents.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sendEmpty(windowContext: WindowContext, replyId: string | null = null): string {
|
||||||
|
return this.#sendPayload(windowContext, { type: 'empty_response' }, replyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
|
||||||
|
windowContext: WindowContext,
|
||||||
|
payload: InternalEventPayload,
|
||||||
|
): Promise<T> {
|
||||||
|
// 1. Build event to send
|
||||||
|
const eventToSend = this.#buildEventToSend(windowContext, payload, null);
|
||||||
|
|
||||||
|
// 2. Spawn listener in background
|
||||||
|
const promise = new Promise<T>((resolve) => {
|
||||||
|
const cb = (event: InternalEvent) => {
|
||||||
|
if (event.replyId === eventToSend.id) {
|
||||||
|
this.#appToPluginEvents.listen(cb); // Unlisten, now that we're done
|
||||||
|
const { type: _, ...payload } = event.payload;
|
||||||
|
resolve(payload as T);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.#appToPluginEvents.listen(cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Send the event after we start listening (to prevent race)
|
||||||
|
console.log("SENDING EVENT FOR REPLY", eventToSend);
|
||||||
|
this.#sendEvent(eventToSend);
|
||||||
|
|
||||||
|
// 4. Return the listener promise
|
||||||
|
return promise as unknown as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sendAndListenForEvents(
|
||||||
|
windowContext: WindowContext,
|
||||||
|
payload: InternalEventPayload,
|
||||||
|
onEvent: (event: InternalEventPayload) => void,
|
||||||
|
): void {
|
||||||
|
// 1. Build event to send
|
||||||
|
const eventToSend = this.#buildEventToSend(windowContext, payload, null);
|
||||||
|
|
||||||
|
// 2. Listen for replies in the background
|
||||||
|
this.#appToPluginEvents.listen((event: InternalEvent) => {
|
||||||
|
if (event.replyId === eventToSend.id) {
|
||||||
|
onEvent(event.payload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Send the event after we start listening (to prevent race)
|
||||||
|
this.#sendEvent(eventToSend);
|
||||||
|
}
|
||||||
|
|
||||||
|
#newCtx(event: InternalEvent): Context {
|
||||||
|
return {
|
||||||
|
clipboard: {
|
||||||
|
copyText: async (text) => {
|
||||||
|
await this.#sendAndWaitForReply(event.windowContext, {
|
||||||
|
type: 'copy_text_request',
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toast: {
|
||||||
|
show: async (args) => {
|
||||||
|
await this.#sendAndWaitForReply(event.windowContext, {
|
||||||
|
type: 'show_toast_request',
|
||||||
|
...args,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
window: {
|
||||||
|
openUrl: async ({ onNavigate, ...args }) => {
|
||||||
|
args.label = args.label || `${Math.random()}`;
|
||||||
|
const payload: InternalEventPayload = { type: 'open_window_request', ...args };
|
||||||
|
const onEvent = (event: InternalEventPayload) => {
|
||||||
|
if (event.type === 'window_navigate_event') {
|
||||||
|
onNavigate?.(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.#sendAndListenForEvents(event.windowContext, payload, onEvent);
|
||||||
|
return {
|
||||||
|
close: () => {
|
||||||
|
const closePayload: InternalEventPayload = {
|
||||||
|
type: 'close_window_request',
|
||||||
|
label: args.label,
|
||||||
|
};
|
||||||
|
this.#sendPayload(event.windowContext, closePayload, null);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prompt: {
|
||||||
|
text: async (args) => {
|
||||||
|
const reply: PromptTextResponse = await this.#sendAndWaitForReply(event.windowContext, {
|
||||||
|
type: 'prompt_text_request',
|
||||||
|
...args,
|
||||||
|
});
|
||||||
|
return reply.value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
httpResponse: {
|
||||||
|
find: async (args) => {
|
||||||
|
const payload = {
|
||||||
|
type: 'find_http_responses_request',
|
||||||
|
...args,
|
||||||
|
} as const;
|
||||||
|
const { httpResponses } = await this.#sendAndWaitForReply<FindHttpResponsesResponse>(
|
||||||
|
event.windowContext,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
return httpResponses;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
httpRequest: {
|
||||||
|
getById: async (args) => {
|
||||||
|
const payload = {
|
||||||
|
type: 'get_http_request_by_id_request',
|
||||||
|
...args,
|
||||||
|
} as const;
|
||||||
|
const { httpRequest } = await this.#sendAndWaitForReply<GetHttpRequestByIdResponse>(
|
||||||
|
event.windowContext,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
return httpRequest;
|
||||||
|
},
|
||||||
|
send: async (args) => {
|
||||||
|
const payload = {
|
||||||
|
type: 'send_http_request_request',
|
||||||
|
...args,
|
||||||
|
} as const;
|
||||||
|
const { httpResponse } = await this.#sendAndWaitForReply<SendHttpRequestResponse>(
|
||||||
|
event.windowContext,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
return httpResponse;
|
||||||
|
},
|
||||||
|
render: async (args) => {
|
||||||
|
const payload = {
|
||||||
|
type: 'render_http_request_request',
|
||||||
|
...args,
|
||||||
|
} as const;
|
||||||
|
const { httpRequest } = await this.#sendAndWaitForReply<RenderHttpRequestResponse>(
|
||||||
|
event.windowContext,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
return httpRequest;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
templates: {
|
||||||
|
/**
|
||||||
|
* Invoke Yaak's template engine to render a value. If the value is a nested type
|
||||||
|
* (eg. object), it will be recursively rendered.
|
||||||
|
*/
|
||||||
|
render: async (args) => {
|
||||||
|
const payload = { type: 'template_render_request', ...args } as const;
|
||||||
|
const result = await this.#sendAndWaitForReply<TemplateRenderResponse>(
|
||||||
|
event.windowContext,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
return result.data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
store: {
|
||||||
|
get: async <T>(key: string) => {
|
||||||
|
const payload = { type: 'get_key_value_request', key } as const;
|
||||||
|
const result = await this.#sendAndWaitForReply<GetKeyValueResponse>(
|
||||||
|
event.windowContext,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
return result.value ? (JSON.parse(result.value) as T) : undefined;
|
||||||
|
},
|
||||||
|
set: async <T>(key: string, value: T) => {
|
||||||
|
const valueStr = JSON.stringify(value);
|
||||||
|
const payload: InternalEventPayload = {
|
||||||
|
type: 'set_key_value_request',
|
||||||
|
key,
|
||||||
|
value: valueStr,
|
||||||
|
};
|
||||||
|
await this.#sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
|
||||||
|
},
|
||||||
|
delete: async (key: string) => {
|
||||||
|
const payload = { type: 'delete_key_value_request', key } as const;
|
||||||
|
const result = await this.#sendAndWaitForReply<DeleteKeyValueResponse>(
|
||||||
|
event.windowContext,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
return result.deleted;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function genId(len = 5): string {
|
||||||
|
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
let id = '';
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
id += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Recursively apply form input defaults to a set of values */
|
||||||
|
function applyFormInputDefaults(
|
||||||
|
inputs: FormInput[],
|
||||||
|
values: { [p: string]: JsonPrimitive | undefined },
|
||||||
|
) {
|
||||||
|
for (const input of inputs) {
|
||||||
|
if ('inputs' in input) {
|
||||||
|
applyFormInputDefaults(input.inputs ?? [], values);
|
||||||
|
} else if ('defaultValue' in input && values[input.name] === undefined) {
|
||||||
|
values[input.name] = input.defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const watchedFiles: Record<string, Stats> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch a file and trigger callback on change.
|
||||||
|
*
|
||||||
|
* We also track the stat for each file because fs.watch() will
|
||||||
|
* trigger a "change" event when the access date changes
|
||||||
|
*/
|
||||||
|
function watchFile(filepath: string, cb: (filepath: string) => void) {
|
||||||
|
watch(filepath, () => {
|
||||||
|
const stat = statSync(filepath);
|
||||||
|
if (stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
|
||||||
|
cb(filepath);
|
||||||
|
}
|
||||||
|
watchedFiles[filepath] = stat;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// function prefixStdout(s: string) {
|
||||||
|
// if (!s.includes('%s')) {
|
||||||
|
// throw new Error('Console prefix must contain a "%s" replacer');
|
||||||
|
// }
|
||||||
|
// interceptStdout((text: string) => {
|
||||||
|
// const lines = text.split(/\n/);
|
||||||
|
// let newText = '';
|
||||||
|
// for (let i = 0; i < lines.length; i++) {
|
||||||
|
// if (lines[i] == '') continue;
|
||||||
|
// newText += util.format(s, lines[i]) + '\n';
|
||||||
|
// }
|
||||||
|
// return newText.trimEnd();
|
||||||
|
// });
|
||||||
|
// }
|
||||||
@@ -8,7 +8,7 @@ if (!port) {
|
|||||||
throw new Error('Plugin runtime missing PORT')
|
throw new Error('Plugin runtime missing PORT')
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = new EventChannel();
|
const pluginToAppEvents = new EventChannel();
|
||||||
const plugins: Record<string, PluginHandle> = {};
|
const plugins: Record<string, PluginHandle> = {};
|
||||||
|
|
||||||
const ws = new WebSocket(`ws://localhost:${port}`);
|
const ws = new WebSocket(`ws://localhost:${port}`);
|
||||||
@@ -25,7 +25,7 @@ ws.on('error', (err: any) => console.error('Plugin runtime websocket error', err
|
|||||||
ws.on('close', (code: number) => console.log('Plugin runtime websocket closed', code));
|
ws.on('close', (code: number) => console.log('Plugin runtime websocket closed', code));
|
||||||
|
|
||||||
// Listen for incoming events from plugins
|
// Listen for incoming events from plugins
|
||||||
events.listen((e) => {
|
pluginToAppEvents.listen((e) => {
|
||||||
const eventStr = JSON.stringify(e);
|
const eventStr = JSON.stringify(e);
|
||||||
ws.send(eventStr);
|
ws.send(eventStr);
|
||||||
});
|
});
|
||||||
@@ -34,7 +34,7 @@ async function handleIncoming(msg: string) {
|
|||||||
const pluginEvent: InternalEvent = JSON.parse(msg);
|
const pluginEvent: InternalEvent = JSON.parse(msg);
|
||||||
// Handle special event to bootstrap plugin
|
// Handle special event to bootstrap plugin
|
||||||
if (pluginEvent.payload.type === 'boot_request') {
|
if (pluginEvent.payload.type === 'boot_request') {
|
||||||
const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.payload, events);
|
const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.payload, pluginToAppEvents);
|
||||||
plugins[pluginEvent.pluginRefId] = plugin;
|
plugins[pluginEvent.pluginRefId] = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ async function handleIncoming(msg: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pluginEvent.payload.type === 'terminate_request') {
|
if (pluginEvent.payload.type === 'terminate_request') {
|
||||||
await plugin.terminate();
|
plugin.terminate();
|
||||||
console.log('Terminated plugin worker', pluginEvent.pluginRefId);
|
console.log('Terminated plugin worker', pluginEvent.pluginRefId);
|
||||||
delete plugins[pluginEvent.pluginRefId];
|
delete plugins[pluginEvent.pluginRefId];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,568 +0,0 @@
|
|||||||
// OAuth 2.0 spec -> https://datatracker.ietf.org/doc/html/rfc6749
|
|
||||||
|
|
||||||
import type {
|
|
||||||
BootRequest,
|
|
||||||
Context,
|
|
||||||
DeleteKeyValueResponse,
|
|
||||||
FindHttpResponsesResponse,
|
|
||||||
FormInput,
|
|
||||||
GetHttpRequestByIdResponse,
|
|
||||||
GetKeyValueResponse,
|
|
||||||
HttpAuthenticationAction,
|
|
||||||
HttpRequestAction,
|
|
||||||
InternalEvent,
|
|
||||||
InternalEventPayload,
|
|
||||||
JsonPrimitive,
|
|
||||||
PluginDefinition,
|
|
||||||
PromptTextResponse,
|
|
||||||
RenderHttpRequestResponse,
|
|
||||||
SendHttpRequestResponse,
|
|
||||||
TemplateFunction,
|
|
||||||
TemplateRenderResponse,
|
|
||||||
WindowContext,
|
|
||||||
} from '@yaakapp/api';
|
|
||||||
import * as console from 'node:console';
|
|
||||||
import type { Stats } from 'node:fs';
|
|
||||||
import { readFileSync, statSync, watch } from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import * as util from 'node:util';
|
|
||||||
import { parentPort as nullableParentPort, workerData } from 'node:worker_threads';
|
|
||||||
import { interceptStdout } from './interceptStdout';
|
|
||||||
import { migrateTemplateFunctionSelectOptions } from './migrations';
|
|
||||||
|
|
||||||
if (nullableParentPort == null) {
|
|
||||||
throw new Error('Worker does not have access to parentPort');
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentPort = nullableParentPort;
|
|
||||||
|
|
||||||
export interface PluginWorkerData {
|
|
||||||
bootRequest: BootRequest;
|
|
||||||
pluginRefId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function initialize(workerData: PluginWorkerData) {
|
|
||||||
const {
|
|
||||||
bootRequest: { dir: pluginDir, watch: enableWatch },
|
|
||||||
pluginRefId,
|
|
||||||
}: PluginWorkerData = workerData;
|
|
||||||
|
|
||||||
const pathPkg = path.join(pluginDir, 'package.json');
|
|
||||||
const pathMod = path.posix.join(pluginDir, 'build', 'index.js');
|
|
||||||
|
|
||||||
const pkg = JSON.parse(readFileSync(pathPkg, 'utf8'));
|
|
||||||
|
|
||||||
prefixStdout(`[plugin][${pkg.name}] %s`);
|
|
||||||
|
|
||||||
function buildEventToSend(
|
|
||||||
windowContext: WindowContext,
|
|
||||||
payload: InternalEventPayload,
|
|
||||||
replyId: string | null = null,
|
|
||||||
): InternalEvent {
|
|
||||||
return {
|
|
||||||
pluginRefId,
|
|
||||||
pluginName: path.basename(pluginDir),
|
|
||||||
id: genId(),
|
|
||||||
replyId,
|
|
||||||
payload,
|
|
||||||
windowContext,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendEmpty(windowContext: WindowContext, replyId: string | null = null): string {
|
|
||||||
return sendPayload(windowContext, { type: 'empty_response' }, replyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendPayload(
|
|
||||||
windowContext: WindowContext,
|
|
||||||
payload: InternalEventPayload,
|
|
||||||
replyId: string | null,
|
|
||||||
): string {
|
|
||||||
const event = buildEventToSend(windowContext, payload, replyId);
|
|
||||||
sendEvent(event);
|
|
||||||
return event.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendEvent(event: InternalEvent) {
|
|
||||||
if (event.payload.type !== 'empty_response') {
|
|
||||||
console.log('Sending event to app', event.id, event.payload.type);
|
|
||||||
}
|
|
||||||
parentPort.postMessage(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
|
|
||||||
windowContext: WindowContext,
|
|
||||||
payload: InternalEventPayload,
|
|
||||||
): Promise<T> {
|
|
||||||
// 1. Build event to send
|
|
||||||
const eventToSend = buildEventToSend(windowContext, payload, null);
|
|
||||||
|
|
||||||
// 2. Spawn listener in background
|
|
||||||
const promise = new Promise<T>((resolve) => {
|
|
||||||
const cb = (event: InternalEvent) => {
|
|
||||||
if (event.replyId === eventToSend.id) {
|
|
||||||
parentPort.off('message', cb); // Unlisten, now that we're done
|
|
||||||
const { type: _, ...payload } = event.payload;
|
|
||||||
resolve(payload as T);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
parentPort.on('message', cb);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Send the event after we start listening (to prevent race)
|
|
||||||
sendEvent(eventToSend);
|
|
||||||
|
|
||||||
// 4. Return the listener promise
|
|
||||||
return promise as unknown as Promise<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendAndListenForEvents(
|
|
||||||
windowContext: WindowContext,
|
|
||||||
payload: InternalEventPayload,
|
|
||||||
onEvent: (event: InternalEventPayload) => void,
|
|
||||||
): void {
|
|
||||||
// 1. Build event to send
|
|
||||||
const eventToSend = buildEventToSend(windowContext, payload, null);
|
|
||||||
|
|
||||||
// 2. Listen for replies in the background
|
|
||||||
parentPort.on('message', (event: InternalEvent) => {
|
|
||||||
if (event.replyId === eventToSend.id) {
|
|
||||||
onEvent(event.payload);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Send the event after we start listening (to prevent race)
|
|
||||||
sendEvent(eventToSend);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload plugin if the JS or package.json changes
|
|
||||||
const windowContextNone: WindowContext = { type: 'none' };
|
|
||||||
const fileChangeCallback = async () => {
|
|
||||||
importModule();
|
|
||||||
return sendPayload(windowContextNone, { type: 'reload_response' }, null);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (enableWatch) {
|
|
||||||
watchFile(pathMod, fileChangeCallback);
|
|
||||||
watchFile(pathPkg, fileChangeCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newCtx = (event: InternalEvent): Context => ({
|
|
||||||
clipboard: {
|
|
||||||
async copyText(text) {
|
|
||||||
await sendAndWaitForReply(event.windowContext, {
|
|
||||||
type: 'copy_text_request',
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
toast: {
|
|
||||||
async show(args) {
|
|
||||||
await sendAndWaitForReply(event.windowContext, {
|
|
||||||
type: 'show_toast_request',
|
|
||||||
...args,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
window: {
|
|
||||||
async openUrl({ onNavigate, ...args }) {
|
|
||||||
args.label = args.label || `${Math.random()}`;
|
|
||||||
const payload: InternalEventPayload = { type: 'open_window_request', ...args };
|
|
||||||
const onEvent = (event: InternalEventPayload) => {
|
|
||||||
if (event.type === 'window_navigate_event') {
|
|
||||||
onNavigate?.(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
sendAndListenForEvents(event.windowContext, payload, onEvent);
|
|
||||||
return {
|
|
||||||
close: () => {
|
|
||||||
const closePayload: InternalEventPayload = {
|
|
||||||
type: 'close_window_request',
|
|
||||||
label: args.label,
|
|
||||||
};
|
|
||||||
sendPayload(event.windowContext, closePayload, null);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
prompt: {
|
|
||||||
async text(args) {
|
|
||||||
const reply: PromptTextResponse = await sendAndWaitForReply(event.windowContext, {
|
|
||||||
type: 'prompt_text_request',
|
|
||||||
...args,
|
|
||||||
});
|
|
||||||
return reply.value;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
httpResponse: {
|
|
||||||
async find(args) {
|
|
||||||
const payload = {
|
|
||||||
type: 'find_http_responses_request',
|
|
||||||
...args,
|
|
||||||
} as const;
|
|
||||||
const { httpResponses } = await sendAndWaitForReply<FindHttpResponsesResponse>(
|
|
||||||
event.windowContext,
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
return httpResponses;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
httpRequest: {
|
|
||||||
async getById(args) {
|
|
||||||
const payload = {
|
|
||||||
type: 'get_http_request_by_id_request',
|
|
||||||
...args,
|
|
||||||
} as const;
|
|
||||||
const { httpRequest } = await sendAndWaitForReply<GetHttpRequestByIdResponse>(
|
|
||||||
event.windowContext,
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
return httpRequest;
|
|
||||||
},
|
|
||||||
async send(args) {
|
|
||||||
const payload = {
|
|
||||||
type: 'send_http_request_request',
|
|
||||||
...args,
|
|
||||||
} as const;
|
|
||||||
const { httpResponse } = await sendAndWaitForReply<SendHttpRequestResponse>(
|
|
||||||
event.windowContext,
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
return httpResponse;
|
|
||||||
},
|
|
||||||
async render(args) {
|
|
||||||
const payload = {
|
|
||||||
type: 'render_http_request_request',
|
|
||||||
...args,
|
|
||||||
} as const;
|
|
||||||
const { httpRequest } = await sendAndWaitForReply<RenderHttpRequestResponse>(
|
|
||||||
event.windowContext,
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
return httpRequest;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
templates: {
|
|
||||||
/**
|
|
||||||
* Invoke Yaak's template engine to render a value. If the value is a nested type
|
|
||||||
* (eg. object), it will be recursively rendered.
|
|
||||||
*/
|
|
||||||
async render(args) {
|
|
||||||
const payload = { type: 'template_render_request', ...args } as const;
|
|
||||||
const result = await sendAndWaitForReply<TemplateRenderResponse>(
|
|
||||||
event.windowContext,
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
return result.data;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
store: {
|
|
||||||
async get<T>(key: string) {
|
|
||||||
const payload = { type: 'get_key_value_request', key } as const;
|
|
||||||
const result = await sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
|
|
||||||
return result.value ? (JSON.parse(result.value) as T) : undefined;
|
|
||||||
},
|
|
||||||
async set<T>(key: string, value: T) {
|
|
||||||
const valueStr = JSON.stringify(value);
|
|
||||||
const payload: InternalEventPayload = {
|
|
||||||
type: 'set_key_value_request',
|
|
||||||
key,
|
|
||||||
value: valueStr,
|
|
||||||
};
|
|
||||||
await sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
|
|
||||||
},
|
|
||||||
async delete(key: string) {
|
|
||||||
const payload = { type: 'delete_key_value_request', key } as const;
|
|
||||||
const result = await sendAndWaitForReply<DeleteKeyValueResponse>(
|
|
||||||
event.windowContext,
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
return result.deleted;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let plug: PluginDefinition | null = null;
|
|
||||||
|
|
||||||
function importModule() {
|
|
||||||
const id = require.resolve(pathMod);
|
|
||||||
delete require.cache[id];
|
|
||||||
plug = require(id).plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
importModule();
|
|
||||||
|
|
||||||
// Message comes into the plugin to be processed
|
|
||||||
parentPort.on('message', async (event: InternalEvent) => {
|
|
||||||
const ctx = newCtx(event);
|
|
||||||
const { windowContext, payload, id: replyId } = event;
|
|
||||||
try {
|
|
||||||
if (payload.type === 'boot_request') {
|
|
||||||
// console.log('Plugin initialized', pkg.name, { capabilities, enableWatch });
|
|
||||||
const payload: InternalEventPayload = {
|
|
||||||
type: 'boot_response',
|
|
||||||
name: pkg.name,
|
|
||||||
version: pkg.version,
|
|
||||||
};
|
|
||||||
sendPayload(windowContext, payload, replyId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.type === 'terminate_request') {
|
|
||||||
const payload: InternalEventPayload = {
|
|
||||||
type: 'terminate_response',
|
|
||||||
};
|
|
||||||
sendPayload(windowContext, payload, replyId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.type === 'import_request' && typeof plug?.importer?.onImport === 'function') {
|
|
||||||
const reply = await plug.importer.onImport(ctx, {
|
|
||||||
text: payload.content,
|
|
||||||
});
|
|
||||||
if (reply != null) {
|
|
||||||
const replyPayload: InternalEventPayload = {
|
|
||||||
type: 'import_response',
|
|
||||||
// deno-lint-ignore no-explicit-any
|
|
||||||
resources: reply.resources as any,
|
|
||||||
};
|
|
||||||
sendPayload(windowContext, replyPayload, replyId);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// Continue, to send back an empty reply
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.type === 'filter_request' && typeof plug?.filter?.onFilter === 'function') {
|
|
||||||
const reply = await plug.filter.onFilter(ctx, {
|
|
||||||
filter: payload.filter,
|
|
||||||
payload: payload.content,
|
|
||||||
mimeType: payload.type,
|
|
||||||
});
|
|
||||||
const replyPayload: InternalEventPayload = {
|
|
||||||
type: 'filter_response',
|
|
||||||
content: reply.filtered,
|
|
||||||
};
|
|
||||||
sendPayload(windowContext, replyPayload, replyId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
payload.type === 'get_http_request_actions_request' &&
|
|
||||||
Array.isArray(plug?.httpRequestActions)
|
|
||||||
) {
|
|
||||||
const reply: HttpRequestAction[] = plug.httpRequestActions.map((a) => ({
|
|
||||||
...a,
|
|
||||||
// Add everything except onSelect
|
|
||||||
onSelect: undefined,
|
|
||||||
}));
|
|
||||||
const replyPayload: InternalEventPayload = {
|
|
||||||
type: 'get_http_request_actions_response',
|
|
||||||
pluginRefId,
|
|
||||||
actions: reply,
|
|
||||||
};
|
|
||||||
sendPayload(windowContext, replyPayload, replyId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
payload.type === 'get_template_functions_request' &&
|
|
||||||
Array.isArray(plug?.templateFunctions)
|
|
||||||
) {
|
|
||||||
const reply: TemplateFunction[] = plug.templateFunctions.map((templateFunction) => {
|
|
||||||
return {
|
|
||||||
...migrateTemplateFunctionSelectOptions(templateFunction),
|
|
||||||
// Add everything except render
|
|
||||||
onRender: undefined,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const replyPayload: InternalEventPayload = {
|
|
||||||
type: 'get_template_functions_response',
|
|
||||||
pluginRefId,
|
|
||||||
functions: reply,
|
|
||||||
};
|
|
||||||
sendPayload(windowContext, replyPayload, replyId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.type === 'get_http_authentication_summary_request' && plug?.authentication) {
|
|
||||||
const { name, shortLabel, label } = plug.authentication;
|
|
||||||
const replyPayload: InternalEventPayload = {
|
|
||||||
type: 'get_http_authentication_summary_response',
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
shortLabel,
|
|
||||||
};
|
|
||||||
|
|
||||||
sendPayload(windowContext, replyPayload, replyId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.type === 'get_http_authentication_config_request' && plug?.authentication) {
|
|
||||||
const { args, actions } = plug.authentication;
|
|
||||||
const resolvedArgs: FormInput[] = [];
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
|
||||||
let v = args[i];
|
|
||||||
if ('dynamic' in v) {
|
|
||||||
const dynamicAttrs = await v.dynamic(ctx, payload);
|
|
||||||
const { dynamic, ...other } = v;
|
|
||||||
resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput);
|
|
||||||
} else {
|
|
||||||
resolvedArgs.push(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const resolvedActions: HttpAuthenticationAction[] = [];
|
|
||||||
for (const { onSelect, ...action } of actions ?? []) {
|
|
||||||
resolvedActions.push(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
const replyPayload: InternalEventPayload = {
|
|
||||||
type: 'get_http_authentication_config_response',
|
|
||||||
args: resolvedArgs,
|
|
||||||
actions: resolvedActions,
|
|
||||||
pluginRefId,
|
|
||||||
};
|
|
||||||
|
|
||||||
sendPayload(windowContext, replyPayload, replyId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.type === 'call_http_authentication_request' && plug?.authentication) {
|
|
||||||
const auth = plug.authentication;
|
|
||||||
if (typeof auth?.onApply === 'function') {
|
|
||||||
applyFormInputDefaults(auth.args, payload.values);
|
|
||||||
const result = await auth.onApply(ctx, payload);
|
|
||||||
sendPayload(
|
|
||||||
windowContext,
|
|
||||||
{
|
|
||||||
type: 'call_http_authentication_response',
|
|
||||||
setHeaders: result.setHeaders,
|
|
||||||
},
|
|
||||||
replyId,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
payload.type === 'call_http_authentication_action_request' &&
|
|
||||||
plug?.authentication != null
|
|
||||||
) {
|
|
||||||
const action = plug.authentication.actions?.[payload.index];
|
|
||||||
if (typeof action?.onSelect === 'function') {
|
|
||||||
await action.onSelect(ctx, payload.args);
|
|
||||||
sendEmpty(windowContext, replyId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
payload.type === 'call_http_request_action_request' &&
|
|
||||||
Array.isArray(plug?.httpRequestActions)
|
|
||||||
) {
|
|
||||||
const action = plug.httpRequestActions[payload.index];
|
|
||||||
if (typeof action?.onSelect === 'function') {
|
|
||||||
await action.onSelect(ctx, payload.args);
|
|
||||||
sendEmpty(windowContext, replyId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
payload.type === 'call_template_function_request' &&
|
|
||||||
Array.isArray(plug?.templateFunctions)
|
|
||||||
) {
|
|
||||||
const action = plug.templateFunctions.find((a) => a.name === payload.name);
|
|
||||||
if (typeof action?.onRender === 'function') {
|
|
||||||
applyFormInputDefaults(action.args, payload.args.values);
|
|
||||||
const result = await action.onRender(ctx, payload.args);
|
|
||||||
sendPayload(
|
|
||||||
windowContext,
|
|
||||||
{
|
|
||||||
type: 'call_template_function_response',
|
|
||||||
value: result ?? null,
|
|
||||||
},
|
|
||||||
replyId,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.type === 'reload_request') {
|
|
||||||
importModule();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log('Plugin call threw exception', payload.type, err);
|
|
||||||
sendPayload(
|
|
||||||
windowContext,
|
|
||||||
{
|
|
||||||
type: 'error_response',
|
|
||||||
error: `${err}`,
|
|
||||||
},
|
|
||||||
replyId,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No matches, so send back an empty response so the caller doesn't block forever
|
|
||||||
sendEmpty(windowContext, replyId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize(workerData);
|
|
||||||
|
|
||||||
function genId(len = 5): string {
|
|
||||||
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
||||||
let id = '';
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
id += alphabet[Math.floor(Math.random() * alphabet.length)];
|
|
||||||
}
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function prefixStdout(s: string) {
|
|
||||||
if (!s.includes('%s')) {
|
|
||||||
throw new Error('Console prefix must contain a "%s" replacer');
|
|
||||||
}
|
|
||||||
interceptStdout((text: string) => {
|
|
||||||
const lines = text.split(/\n/);
|
|
||||||
let newText = '';
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
if (lines[i] == '') continue;
|
|
||||||
newText += util.format(s, lines[i]) + '\n';
|
|
||||||
}
|
|
||||||
return newText.trimEnd();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const watchedFiles: Record<string, Stats> = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Watch a file and trigger callback on change.
|
|
||||||
*
|
|
||||||
* We also track the stat for each file because fs.watch() will
|
|
||||||
* trigger a "change" event when the access date changes
|
|
||||||
*/
|
|
||||||
function watchFile(filepath: string, cb: (filepath: string) => void) {
|
|
||||||
watch(filepath, () => {
|
|
||||||
const stat = statSync(filepath);
|
|
||||||
if (stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
|
|
||||||
cb(filepath);
|
|
||||||
}
|
|
||||||
watchedFiles[filepath] = stat;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Recursively apply form input defaults to a set of values */
|
|
||||||
function applyFormInputDefaults(
|
|
||||||
inputs: FormInput[],
|
|
||||||
values: { [p: string]: JsonPrimitive | undefined },
|
|
||||||
) {
|
|
||||||
for (const input of inputs) {
|
|
||||||
if ('inputs' in input) {
|
|
||||||
applyFormInputDefaults(input.inputs ?? [], values);
|
|
||||||
} else if ('defaultValue' in input && values[input.name] === undefined) {
|
|
||||||
values[input.name] = input.defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
|||||||
event: &InternalEvent,
|
event: &InternalEvent,
|
||||||
plugin_handle: &PluginHandle,
|
plugin_handle: &PluginHandle,
|
||||||
) {
|
) {
|
||||||
// info!("Got event to app {}", event.id);
|
// debug!("Got event to app {event:?}");
|
||||||
let window_context = event.window_context.to_owned();
|
let window_context = event.window_context.to_owned();
|
||||||
let response_event: Option<InternalEventPayload> = match event.clone().payload {
|
let response_event: Option<InternalEventPayload> = match event.clone().payload {
|
||||||
InternalEventPayload::CopyTextRequest(req) => {
|
InternalEventPayload::CopyTextRequest(req) => {
|
||||||
|
|||||||
@@ -461,17 +461,24 @@ var plugin = {
|
|||||||
options: grantTypes
|
options: grantTypes
|
||||||
},
|
},
|
||||||
// Always-present fields
|
// Always-present fields
|
||||||
{ type: "text", name: "clientId", label: "Client ID" },
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "clientId",
|
||||||
|
label: "Client ID",
|
||||||
|
optional: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
name: "clientSecret",
|
name: "clientSecret",
|
||||||
label: "Client Secret",
|
label: "Client Secret",
|
||||||
|
optional: true,
|
||||||
password: true,
|
password: true,
|
||||||
dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"])
|
dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"])
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
name: "authorizationUrl",
|
name: "authorizationUrl",
|
||||||
|
optional: true,
|
||||||
label: "Authorization URL",
|
label: "Authorization URL",
|
||||||
dynamic: hiddenIfNot(["authorization_code", "implicit"]),
|
dynamic: hiddenIfNot(["authorization_code", "implicit"]),
|
||||||
placeholder: authorizationUrls[0],
|
placeholder: authorizationUrls[0],
|
||||||
@@ -480,6 +487,7 @@ var plugin = {
|
|||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
name: "accessTokenUrl",
|
name: "accessTokenUrl",
|
||||||
|
optional: true,
|
||||||
label: "Access Token URL",
|
label: "Access Token URL",
|
||||||
placeholder: accessTokenUrls[0],
|
placeholder: accessTokenUrls[0],
|
||||||
dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"]),
|
dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"]),
|
||||||
@@ -590,55 +598,55 @@ var plugin = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
async onApply(ctx, { values, contextId }) {
|
async onApply(ctx, { values, contextId }) {
|
||||||
const headerPrefix = optionalString(values, "headerPrefix") ?? "";
|
const headerPrefix = stringArg(values, "headerPrefix");
|
||||||
const grantType = requiredString(values, "grantType");
|
const grantType = stringArg(values, "grantType");
|
||||||
const credentialsInBody = values.credentials === "body";
|
const credentialsInBody = values.credentials === "body";
|
||||||
let token;
|
let token;
|
||||||
if (grantType === "authorization_code") {
|
if (grantType === "authorization_code") {
|
||||||
const authorizationUrl = requiredString(values, "authorizationUrl");
|
const authorizationUrl = stringArg(values, "authorizationUrl");
|
||||||
const accessTokenUrl = requiredString(values, "accessTokenUrl");
|
const accessTokenUrl = stringArg(values, "accessTokenUrl");
|
||||||
token = await getAuthorizationCode(ctx, contextId, {
|
token = await getAuthorizationCode(ctx, contextId, {
|
||||||
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
|
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
|
||||||
authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`,
|
authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`,
|
||||||
clientId: requiredString(values, "clientId"),
|
clientId: stringArg(values, "clientId"),
|
||||||
clientSecret: requiredString(values, "clientSecret"),
|
clientSecret: stringArg(values, "clientSecret"),
|
||||||
redirectUri: optionalString(values, "redirectUri"),
|
redirectUri: stringArgOrNull(values, "redirectUri"),
|
||||||
scope: optionalString(values, "scope"),
|
scope: stringArgOrNull(values, "scope"),
|
||||||
state: optionalString(values, "state"),
|
state: stringArgOrNull(values, "state"),
|
||||||
credentialsInBody,
|
credentialsInBody,
|
||||||
pkce: values.usePkce ? {
|
pkce: values.usePkce ? {
|
||||||
challengeMethod: requiredString(values, "pkceChallengeMethod"),
|
challengeMethod: stringArg(values, "pkceChallengeMethod"),
|
||||||
codeVerifier: optionalString(values, "pkceCodeVerifier")
|
codeVerifier: stringArgOrNull(values, "pkceCodeVerifier")
|
||||||
} : null
|
} : null
|
||||||
});
|
});
|
||||||
} else if (grantType === "implicit") {
|
} else if (grantType === "implicit") {
|
||||||
const authorizationUrl = requiredString(values, "authorizationUrl");
|
const authorizationUrl = stringArg(values, "authorizationUrl");
|
||||||
token = await getImplicit(ctx, contextId, {
|
token = await getImplicit(ctx, contextId, {
|
||||||
authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`,
|
authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`,
|
||||||
clientId: requiredString(values, "clientId"),
|
clientId: stringArg(values, "clientId"),
|
||||||
redirectUri: optionalString(values, "redirectUri"),
|
redirectUri: stringArgOrNull(values, "redirectUri"),
|
||||||
responseType: requiredString(values, "responseType"),
|
responseType: stringArg(values, "responseType"),
|
||||||
scope: optionalString(values, "scope"),
|
scope: stringArgOrNull(values, "scope"),
|
||||||
state: optionalString(values, "state")
|
state: stringArgOrNull(values, "state")
|
||||||
});
|
});
|
||||||
} else if (grantType === "client_credentials") {
|
} else if (grantType === "client_credentials") {
|
||||||
const accessTokenUrl = requiredString(values, "accessTokenUrl");
|
const accessTokenUrl = stringArg(values, "accessTokenUrl");
|
||||||
token = await getClientCredentials(ctx, contextId, {
|
token = await getClientCredentials(ctx, contextId, {
|
||||||
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
|
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
|
||||||
clientId: requiredString(values, "clientId"),
|
clientId: stringArg(values, "clientId"),
|
||||||
clientSecret: requiredString(values, "clientSecret"),
|
clientSecret: stringArg(values, "clientSecret"),
|
||||||
scope: optionalString(values, "scope"),
|
scope: stringArgOrNull(values, "scope"),
|
||||||
credentialsInBody
|
credentialsInBody
|
||||||
});
|
});
|
||||||
} else if (grantType === "password") {
|
} else if (grantType === "password") {
|
||||||
const accessTokenUrl = requiredString(values, "accessTokenUrl");
|
const accessTokenUrl = stringArg(values, "accessTokenUrl");
|
||||||
token = await getPassword(ctx, contextId, {
|
token = await getPassword(ctx, contextId, {
|
||||||
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
|
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
|
||||||
clientId: requiredString(values, "clientId"),
|
clientId: stringArg(values, "clientId"),
|
||||||
clientSecret: requiredString(values, "clientSecret"),
|
clientSecret: stringArg(values, "clientSecret"),
|
||||||
username: requiredString(values, "username"),
|
username: stringArg(values, "username"),
|
||||||
password: requiredString(values, "password"),
|
password: stringArg(values, "password"),
|
||||||
scope: optionalString(values, "scope"),
|
scope: stringArgOrNull(values, "scope"),
|
||||||
credentialsInBody
|
credentialsInBody
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -654,14 +662,14 @@ var plugin = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
function optionalString(values, name) {
|
function stringArgOrNull(values, name) {
|
||||||
const arg = values[name];
|
const arg = values[name];
|
||||||
if (arg == null || arg == "") return null;
|
if (arg == null || arg == "") return null;
|
||||||
return `${arg}`;
|
return `${arg}`;
|
||||||
}
|
}
|
||||||
function requiredString(values, name) {
|
function stringArg(values, name) {
|
||||||
const arg = optionalString(values, name);
|
const arg = stringArgOrNull(values, name);
|
||||||
if (!arg) throw new Error(`Missing required argument ${name}`);
|
if (!arg) return "";
|
||||||
return arg;
|
return arg;
|
||||||
}
|
}
|
||||||
// Annotate the CommonJS export names for ESM import in node:
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
|||||||
@@ -30,9 +30,13 @@ var plugin = {
|
|||||||
label: "Copy as Curl",
|
label: "Copy as Curl",
|
||||||
icon: "copy",
|
icon: "copy",
|
||||||
async onSelect(ctx, args) {
|
async onSelect(ctx, args) {
|
||||||
|
console.log("---------------------------", 0);
|
||||||
const rendered_request = await ctx.httpRequest.render({ httpRequest: args.httpRequest, purpose: "preview" });
|
const rendered_request = await ctx.httpRequest.render({ httpRequest: args.httpRequest, purpose: "preview" });
|
||||||
|
console.log("---------------------------", 1);
|
||||||
const data = await convertToCurl(rendered_request);
|
const data = await convertToCurl(rendered_request);
|
||||||
|
console.log("---------------------------", 2);
|
||||||
await ctx.clipboard.copyText(data);
|
await ctx.clipboard.copyText(data);
|
||||||
|
console.log("---------------------------", 3);
|
||||||
await ctx.toast.show({ message: "Curl copied to clipboard", icon: "copy", color: "success" });
|
await ctx.toast.show({ message: "Curl copied to clipboard", icon: "copy", color: "success" });
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ impl PluginManager {
|
|||||||
|
|
||||||
// Boot the plugin
|
// Boot the plugin
|
||||||
let event = timeout(
|
let event = timeout(
|
||||||
Duration::from_secs(1),
|
Duration::from_secs(2),
|
||||||
self.send_to_plugin_and_wait(
|
self.send_to_plugin_and_wait(
|
||||||
window_context,
|
window_context,
|
||||||
&plugin_handle,
|
&plugin_handle,
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ impl PluginHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_boot_response(&self, resp: &BootResponse) {
|
pub async fn set_boot_response(&self, resp: &BootResponse) {
|
||||||
|
info!("BOOTED PLUGIN {:?}", resp);
|
||||||
let mut boot_resp = self.boot_resp.lock().await;
|
let mut boot_resp = self.boot_resp.lock().await;
|
||||||
*boot_resp = resp.clone();
|
*boot_resp = resp.clone();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,5 +77,7 @@ function removeWatchKey(key: string) {
|
|||||||
|
|
||||||
// On page load, unlisten to all zombie watchers
|
// On page load, unlisten to all zombie watchers
|
||||||
const keys = getWatchKeys();
|
const keys = getWatchKeys();
|
||||||
console.log('Unsubscribing to zombie file watchers', keys);
|
if (keys.length > 0) {
|
||||||
keys.forEach(unlistenToWatcher);
|
console.log('Unsubscribing to zombie file watchers', keys);
|
||||||
|
keys.forEach(unlistenToWatcher);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user