diff --git a/packages/plugin-runtime/package.json b/packages/plugin-runtime/package.json index f3d80c5b..39e8aa89 100644 --- a/packages/plugin-runtime/package.json +++ b/packages/plugin-runtime/package.json @@ -3,10 +3,7 @@ "scripts": { "bootstrap": "npm run build", "build": "run-p build:*", - "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" + "build:main": "esbuild src/index.ts --bundle --platform=node --outfile=../../src-tauri/vendored/plugin-runtime/index.cjs" }, "dependencies": { "ws": "^8.18.0" diff --git a/packages/plugin-runtime/src/PluginHandle.ts b/packages/plugin-runtime/src/PluginHandle.ts index b585c40b..18dbbdb8 100644 --- a/packages/plugin-runtime/src/PluginHandle.ts +++ b/packages/plugin-runtime/src/PluginHandle.ts @@ -1,56 +1,28 @@ 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 { PluginWorkerData } from './index.worker'; +import { PluginInstance, PluginWorkerData } from './PluginInstance'; export class PluginHandle { - #worker: Worker; + #instance: PluginInstance; constructor( readonly pluginRefId: string, 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 = { pluginRefId: this.pluginRefId, bootRequest: this.bootRequest, }; - const worker = new Worker(workerPath, { - workerData, - }); - - worker.on('message', (e) => this.events.emit(e)); - worker.on('error', this.#handleError.bind(this)); - worker.on('exit', this.#handleExit.bind(this)); - + this.#instance = new PluginInstance(workerData, pluginToAppEvents); console.log('Created plugin worker for ', this.bootRequest.dir); - - return worker; } - async #handleError(err: Error) { - console.error('Plugin errored', this.bootRequest.dir, err); + sendToWorker(event: InternalEvent) { + this.#instance.postMessage(event); } - async #handleExit(code: number) { - if (code === 0) { - console.log('Plugin exited successfully', this.bootRequest.dir); - } else { - console.log('Plugin exited with status', code, this.bootRequest.dir); - } + terminate() { + this.#instance.terminate(); } } diff --git a/packages/plugin-runtime/src/PluginInstance.ts b/packages/plugin-runtime/src/PluginInstance.ts new file mode 100644 index 00000000..dc9fab32 --- /dev/null +++ b/packages/plugin-runtime/src/PluginInstance.ts @@ -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>( + windowContext: WindowContext, + payload: InternalEventPayload, + ): Promise { + // 1. Build event to send + const eventToSend = this.#buildEventToSend(windowContext, payload, null); + + // 2. Spawn listener in background + const promise = new Promise((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; + } + + #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( + 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( + event.windowContext, + payload, + ); + return httpRequest; + }, + send: async (args) => { + const payload = { + type: 'send_http_request_request', + ...args, + } as const; + const { httpResponse } = await this.#sendAndWaitForReply( + event.windowContext, + payload, + ); + return httpResponse; + }, + render: async (args) => { + const payload = { + type: 'render_http_request_request', + ...args, + } as const; + const { httpRequest } = await this.#sendAndWaitForReply( + 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( + event.windowContext, + payload, + ); + return result.data; + }, + }, + store: { + get: async (key: string) => { + const payload = { type: 'get_key_value_request', key } as const; + const result = await this.#sendAndWaitForReply( + event.windowContext, + payload, + ); + return result.value ? (JSON.parse(result.value) as T) : undefined; + }, + set: async (key: string, value: T) => { + const valueStr = JSON.stringify(value); + const payload: InternalEventPayload = { + type: 'set_key_value_request', + key, + value: valueStr, + }; + await this.#sendAndWaitForReply(event.windowContext, payload); + }, + delete: async (key: string) => { + const payload = { type: 'delete_key_value_request', key } as const; + const result = await this.#sendAndWaitForReply( + 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 = {}; + +/** + * 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(); +// }); +// } diff --git a/packages/plugin-runtime/src/index.ts b/packages/plugin-runtime/src/index.ts index 1611c029..afdd0e22 100644 --- a/packages/plugin-runtime/src/index.ts +++ b/packages/plugin-runtime/src/index.ts @@ -8,7 +8,7 @@ if (!port) { throw new Error('Plugin runtime missing PORT') } -const events = new EventChannel(); +const pluginToAppEvents = new EventChannel(); const plugins: Record = {}; 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)); // Listen for incoming events from plugins -events.listen((e) => { +pluginToAppEvents.listen((e) => { const eventStr = JSON.stringify(e); ws.send(eventStr); }); @@ -34,7 +34,7 @@ async function handleIncoming(msg: string) { const pluginEvent: InternalEvent = JSON.parse(msg); // Handle special event to bootstrap plugin 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; } @@ -46,7 +46,7 @@ async function handleIncoming(msg: string) { } if (pluginEvent.payload.type === 'terminate_request') { - await plugin.terminate(); + plugin.terminate(); console.log('Terminated plugin worker', pluginEvent.pluginRefId); delete plugins[pluginEvent.pluginRefId]; } diff --git a/packages/plugin-runtime/src/index.worker.ts b/packages/plugin-runtime/src/index.worker.ts deleted file mode 100644 index aa888ffe..00000000 --- a/packages/plugin-runtime/src/index.worker.ts +++ /dev/null @@ -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>( - windowContext: WindowContext, - payload: InternalEventPayload, - ): Promise { - // 1. Build event to send - const eventToSend = buildEventToSend(windowContext, payload, null); - - // 2. Spawn listener in background - const promise = new Promise((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; - } - - 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( - 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( - event.windowContext, - payload, - ); - return httpRequest; - }, - async send(args) { - const payload = { - type: 'send_http_request_request', - ...args, - } as const; - const { httpResponse } = await sendAndWaitForReply( - event.windowContext, - payload, - ); - return httpResponse; - }, - async render(args) { - const payload = { - type: 'render_http_request_request', - ...args, - } as const; - const { httpRequest } = await sendAndWaitForReply( - 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( - event.windowContext, - payload, - ); - return result.data; - }, - }, - store: { - async get(key: string) { - const payload = { type: 'get_key_value_request', key } as const; - const result = await sendAndWaitForReply(event.windowContext, payload); - return result.value ? (JSON.parse(result.value) as T) : undefined; - }, - async set(key: string, value: T) { - const valueStr = JSON.stringify(value); - const payload: InternalEventPayload = { - type: 'set_key_value_request', - key, - value: valueStr, - }; - await sendAndWaitForReply(event.windowContext, payload); - }, - async delete(key: string) { - const payload = { type: 'delete_key_value_request', key } as const; - const result = await sendAndWaitForReply( - 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 = {}; - -/** - * 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; - } - } -} diff --git a/src-tauri/src/plugin_events.rs b/src-tauri/src/plugin_events.rs index 2e4ecf2d..bc35e753 100644 --- a/src-tauri/src/plugin_events.rs +++ b/src-tauri/src/plugin_events.rs @@ -30,7 +30,7 @@ pub(crate) async fn handle_plugin_event( event: &InternalEvent, 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 response_event: Option = match event.clone().payload { InternalEventPayload::CopyTextRequest(req) => { diff --git a/src-tauri/vendored/plugins/auth-oauth2/build/index.js b/src-tauri/vendored/plugins/auth-oauth2/build/index.js index cc26357a..f30555ea 100644 --- a/src-tauri/vendored/plugins/auth-oauth2/build/index.js +++ b/src-tauri/vendored/plugins/auth-oauth2/build/index.js @@ -461,17 +461,24 @@ var plugin = { options: grantTypes }, // Always-present fields - { type: "text", name: "clientId", label: "Client ID" }, + { + type: "text", + name: "clientId", + label: "Client ID", + optional: true + }, { type: "text", name: "clientSecret", label: "Client Secret", + optional: true, password: true, dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"]) }, { type: "text", name: "authorizationUrl", + optional: true, label: "Authorization URL", dynamic: hiddenIfNot(["authorization_code", "implicit"]), placeholder: authorizationUrls[0], @@ -480,6 +487,7 @@ var plugin = { { type: "text", name: "accessTokenUrl", + optional: true, label: "Access Token URL", placeholder: accessTokenUrls[0], dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"]), @@ -590,55 +598,55 @@ var plugin = { } ], async onApply(ctx, { values, contextId }) { - const headerPrefix = optionalString(values, "headerPrefix") ?? ""; - const grantType = requiredString(values, "grantType"); + const headerPrefix = stringArg(values, "headerPrefix"); + const grantType = stringArg(values, "grantType"); const credentialsInBody = values.credentials === "body"; let token; if (grantType === "authorization_code") { - const authorizationUrl = requiredString(values, "authorizationUrl"); - const accessTokenUrl = requiredString(values, "accessTokenUrl"); + const authorizationUrl = stringArg(values, "authorizationUrl"); + const accessTokenUrl = stringArg(values, "accessTokenUrl"); token = await getAuthorizationCode(ctx, contextId, { accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`, authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`, - clientId: requiredString(values, "clientId"), - clientSecret: requiredString(values, "clientSecret"), - redirectUri: optionalString(values, "redirectUri"), - scope: optionalString(values, "scope"), - state: optionalString(values, "state"), + clientId: stringArg(values, "clientId"), + clientSecret: stringArg(values, "clientSecret"), + redirectUri: stringArgOrNull(values, "redirectUri"), + scope: stringArgOrNull(values, "scope"), + state: stringArgOrNull(values, "state"), credentialsInBody, pkce: values.usePkce ? { - challengeMethod: requiredString(values, "pkceChallengeMethod"), - codeVerifier: optionalString(values, "pkceCodeVerifier") + challengeMethod: stringArg(values, "pkceChallengeMethod"), + codeVerifier: stringArgOrNull(values, "pkceCodeVerifier") } : null }); } else if (grantType === "implicit") { - const authorizationUrl = requiredString(values, "authorizationUrl"); + const authorizationUrl = stringArg(values, "authorizationUrl"); token = await getImplicit(ctx, contextId, { authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`, - clientId: requiredString(values, "clientId"), - redirectUri: optionalString(values, "redirectUri"), - responseType: requiredString(values, "responseType"), - scope: optionalString(values, "scope"), - state: optionalString(values, "state") + clientId: stringArg(values, "clientId"), + redirectUri: stringArgOrNull(values, "redirectUri"), + responseType: stringArg(values, "responseType"), + scope: stringArgOrNull(values, "scope"), + state: stringArgOrNull(values, "state") }); } else if (grantType === "client_credentials") { - const accessTokenUrl = requiredString(values, "accessTokenUrl"); + const accessTokenUrl = stringArg(values, "accessTokenUrl"); token = await getClientCredentials(ctx, contextId, { accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`, - clientId: requiredString(values, "clientId"), - clientSecret: requiredString(values, "clientSecret"), - scope: optionalString(values, "scope"), + clientId: stringArg(values, "clientId"), + clientSecret: stringArg(values, "clientSecret"), + scope: stringArgOrNull(values, "scope"), credentialsInBody }); } else if (grantType === "password") { - const accessTokenUrl = requiredString(values, "accessTokenUrl"); + const accessTokenUrl = stringArg(values, "accessTokenUrl"); token = await getPassword(ctx, contextId, { accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`, - clientId: requiredString(values, "clientId"), - clientSecret: requiredString(values, "clientSecret"), - username: requiredString(values, "username"), - password: requiredString(values, "password"), - scope: optionalString(values, "scope"), + clientId: stringArg(values, "clientId"), + clientSecret: stringArg(values, "clientSecret"), + username: stringArg(values, "username"), + password: stringArg(values, "password"), + scope: stringArgOrNull(values, "scope"), credentialsInBody }); } else { @@ -654,14 +662,14 @@ var plugin = { } } }; -function optionalString(values, name) { +function stringArgOrNull(values, name) { const arg = values[name]; if (arg == null || arg == "") return null; return `${arg}`; } -function requiredString(values, name) { - const arg = optionalString(values, name); - if (!arg) throw new Error(`Missing required argument ${name}`); +function stringArg(values, name) { + const arg = stringArgOrNull(values, name); + if (!arg) return ""; return arg; } // Annotate the CommonJS export names for ESM import in node: diff --git a/src-tauri/vendored/plugins/exporter-curl/build/index.js b/src-tauri/vendored/plugins/exporter-curl/build/index.js index 971450fb..2965d8b7 100644 --- a/src-tauri/vendored/plugins/exporter-curl/build/index.js +++ b/src-tauri/vendored/plugins/exporter-curl/build/index.js @@ -30,9 +30,13 @@ var plugin = { label: "Copy as Curl", icon: "copy", async onSelect(ctx, args) { + console.log("---------------------------", 0); const rendered_request = await ctx.httpRequest.render({ httpRequest: args.httpRequest, purpose: "preview" }); + console.log("---------------------------", 1); const data = await convertToCurl(rendered_request); + console.log("---------------------------", 2); await ctx.clipboard.copyText(data); + console.log("---------------------------", 3); await ctx.toast.show({ message: "Curl copied to clipboard", icon: "copy", color: "success" }); } }] diff --git a/src-tauri/yaak-plugins/src/manager.rs b/src-tauri/yaak-plugins/src/manager.rs index e86439d2..371c4088 100644 --- a/src-tauri/yaak-plugins/src/manager.rs +++ b/src-tauri/yaak-plugins/src/manager.rs @@ -206,7 +206,7 @@ impl PluginManager { // Boot the plugin let event = timeout( - Duration::from_secs(1), + Duration::from_secs(2), self.send_to_plugin_and_wait( window_context, &plugin_handle, diff --git a/src-tauri/yaak-plugins/src/plugin_handle.rs b/src-tauri/yaak-plugins/src/plugin_handle.rs index 6525fc98..95416b98 100644 --- a/src-tauri/yaak-plugins/src/plugin_handle.rs +++ b/src-tauri/yaak-plugins/src/plugin_handle.rs @@ -74,6 +74,7 @@ impl PluginHandle { } pub async fn set_boot_response(&self, resp: &BootResponse) { + info!("BOOTED PLUGIN {:?}", resp); let mut boot_resp = self.boot_resp.lock().await; *boot_resp = resp.clone(); } diff --git a/src-tauri/yaak-sync/index.ts b/src-tauri/yaak-sync/index.ts index 233812bf..3d5bd4a6 100644 --- a/src-tauri/yaak-sync/index.ts +++ b/src-tauri/yaak-sync/index.ts @@ -77,5 +77,7 @@ function removeWatchKey(key: string) { // On page load, unlisten to all zombie watchers const keys = getWatchKeys(); -console.log('Unsubscribing to zombie file watchers', keys); -keys.forEach(unlistenToWatcher); +if (keys.length > 0) { + console.log('Unsubscribing to zombie file watchers', keys); + keys.forEach(unlistenToWatcher); +}