Refactoring and got ipc messages to work

plugin_system_types_test
Laurent Cozic 2020-10-01 14:47:38 +01:00
parent 776e7dfe3f
commit 7a79d7ef7e
97 changed files with 620 additions and 416 deletions

View File

@ -91,10 +91,12 @@ CliClient/tests/support/plugins/withExternalModules/src/index.js
CliClient/tests/synchronizer_LockHandler.js
CliClient/tests/synchronizer_MigrationHandler.js
ElectronClient/app.js
ElectronClient/bridge.js
ElectronClient/commands/copyDevCommand.js
ElectronClient/commands/focusElement.js
ElectronClient/commands/startExternalEditing.js
ElectronClient/commands/stopExternalEditing.js
ElectronClient/ElectronAppWrapper.js
ElectronClient/global.d.js
ElectronClient/gui/Button/Button.js
ElectronClient/gui/ConfigScreen/ButtonBar.js
@ -199,9 +201,11 @@ ElectronClient/gui/ToolbarBase.js
ElectronClient/gui/ToolbarButton/styles/index.js
ElectronClient/gui/ToolbarButton/ToolbarButton.js
ElectronClient/gui/utils/NoteListUtils.js
ElectronClient/services/bridge.js
ElectronClient/services/plugins/hooks/useThemeCss.js
ElectronClient/services/plugins/hooks/useViewIsReady.js
ElectronClient/services/plugins/PlatformImplementation.js
ElectronClient/services/plugins/PluginRunner.js
ElectronClient/services/plugins/UserWebview.js
ElectronClient/services/plugins/UserWebviewDialog.js
ElectronClient/services/plugins/UserWebviewDialogButtonBar.js
@ -221,6 +225,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/JoplinServerApi.js
ReactNativeClient/lib/Logger.js
ReactNativeClient/lib/models/Setting.js
ReactNativeClient/lib/ntpDate.js
ReactNativeClient/lib/reducer.js
@ -259,6 +264,7 @@ ReactNativeClient/lib/services/plugins/sandboxProxy.js
ReactNativeClient/lib/services/plugins/ToolbarButtonController.js
ReactNativeClient/lib/services/plugins/utils/executeSandboxCall.js
ReactNativeClient/lib/services/plugins/utils/manifestFromObject.js
ReactNativeClient/lib/services/plugins/utils/mapEventHandlersToIds.js
ReactNativeClient/lib/services/plugins/utils/types.js
ReactNativeClient/lib/services/plugins/ViewController.js
ReactNativeClient/lib/services/plugins/WebviewController.js

6
.gitignore vendored
View File

@ -84,10 +84,12 @@ CliClient/tests/support/plugins/withExternalModules/src/index.js
CliClient/tests/synchronizer_LockHandler.js
CliClient/tests/synchronizer_MigrationHandler.js
ElectronClient/app.js
ElectronClient/bridge.js
ElectronClient/commands/copyDevCommand.js
ElectronClient/commands/focusElement.js
ElectronClient/commands/startExternalEditing.js
ElectronClient/commands/stopExternalEditing.js
ElectronClient/ElectronAppWrapper.js
ElectronClient/global.d.js
ElectronClient/gui/Button/Button.js
ElectronClient/gui/ConfigScreen/ButtonBar.js
@ -192,9 +194,11 @@ ElectronClient/gui/ToolbarBase.js
ElectronClient/gui/ToolbarButton/styles/index.js
ElectronClient/gui/ToolbarButton/ToolbarButton.js
ElectronClient/gui/utils/NoteListUtils.js
ElectronClient/services/bridge.js
ElectronClient/services/plugins/hooks/useThemeCss.js
ElectronClient/services/plugins/hooks/useViewIsReady.js
ElectronClient/services/plugins/PlatformImplementation.js
ElectronClient/services/plugins/PluginRunner.js
ElectronClient/services/plugins/UserWebview.js
ElectronClient/services/plugins/UserWebviewDialog.js
ElectronClient/services/plugins/UserWebviewDialogButtonBar.js
@ -214,6 +218,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/JoplinServerApi.js
ReactNativeClient/lib/Logger.js
ReactNativeClient/lib/models/Setting.js
ReactNativeClient/lib/ntpDate.js
ReactNativeClient/lib/reducer.js
@ -252,6 +257,7 @@ ReactNativeClient/lib/services/plugins/sandboxProxy.js
ReactNativeClient/lib/services/plugins/ToolbarButtonController.js
ReactNativeClient/lib/services/plugins/utils/executeSandboxCall.js
ReactNativeClient/lib/services/plugins/utils/manifestFromObject.js
ReactNativeClient/lib/services/plugins/utils/mapEventHandlersToIds.js
ReactNativeClient/lib/services/plugins/utils/types.js
ReactNativeClient/lib/services/plugins/ViewController.js
ReactNativeClient/lib/services/plugins/WebviewController.js

View File

@ -1,4 +1,4 @@
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const { netUtils } = require('lib/net-utils.js');
const http = require('http');

View File

@ -1,4 +1,4 @@
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const Folder = require('lib/models/Folder.js');
const BaseItem = require('lib/models/BaseItem.js');
const Tag = require('lib/models/Tag.js');

View File

@ -1,7 +1,7 @@
'use strict';
const fs = require('fs-extra');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const { dirname } = require('lib/path-utils.js');
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
const { JoplinDatabase } = require('lib/joplin-database.js');

View File

@ -2,7 +2,7 @@ const yargParser = require('yargs-parser');
const { _ } = require('lib/locale.js');
const { time } = require('lib/time-utils.js');
const stringPadding = require('string-padding');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const cliUtils = {};

View File

@ -1,7 +1,7 @@
const { BaseCommand } = require('./base-command.js');
const { _ } = require('lib/locale.js');
const Setting = require('lib/models/Setting').default;
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const shim = require('lib/shim');
class Command extends BaseCommand {

View File

@ -1,7 +1,7 @@
'use strict';
const { time } = require('lib/time-utils.js');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const Resource = require('lib/models/Resource.js');
const { dirname } = require('lib/path-utils.js');
const { FsDriverNode } = require('./fs-driver-node.js');

View File

@ -23,7 +23,7 @@ const NoteTag = require('lib/models/NoteTag.js');
const MasterKey = require('lib/models/MasterKey');
const Setting = require('lib/models/Setting').default;
const Revision = require('lib/models/Revision.js');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const { FsDriverNode } = require('lib/fs-driver-node.js');
const { shimInit } = require('lib/shim-init-node.js');
const { _ } = require('lib/locale.js');

View File

@ -4,6 +4,7 @@ import sandboxProxy from 'lib/services/plugins/sandboxProxy';
import BasePluginRunner from 'lib/services/plugins/BasePluginRunner';
import executeSandboxCall from 'lib/services/plugins/utils/executeSandboxCall';
import Global from 'lib/services/plugins/api/Global';
import mapEventHandlersToIds, { EventHandlers } from 'lib/services/plugins/utils/mapEventHandlersToIds';
function createConsoleWrapper(pluginId:string) {
const wrapper:any = {};
@ -29,6 +30,8 @@ function createConsoleWrapper(pluginId:string) {
export default class PluginRunner extends BasePluginRunner {
private eventHandlers_:EventHandlers = {};
constructor() {
super();
@ -37,20 +40,12 @@ export default class PluginRunner extends BasePluginRunner {
private async eventHandler(eventHandlerId:string, args:any[]) {
const cb = this.eventHandlers_[eventHandlerId];
delete this.eventHandlers_[eventHandlerId];
return cb(...args);
}
private newSandboxProxy(pluginId:string, sandbox:Global) {
// Note: for desktop, the implementation should be like so:
// In the target, we post an IPC message with the path, args, etc. as well as a callbackId to the host
// The target saves this callbackId and associate it with a Promise that it returns.
// When the host responds back (via IPC), we get the promise back using the callbackId, then call resolve
// with what was sent from the host.
const target = async (path:string, args:any[]) => {
return executeSandboxCall(pluginId, sandbox, `joplin.${path}`, this.mapEventHandlersToIds(args), this.eventHandler);
return executeSandboxCall(pluginId, sandbox, `joplin.${path}`, mapEventHandlersToIds(args, this.eventHandlers_), this.eventHandler);
};
return {

View File

@ -85,7 +85,7 @@ describe('services_PluginService', function() {
// const folder = await Folder.save({ title: 'folder' });
// const folderFromApi = await service.executeSandboxCall(plugin.id, 'joplin.api.get', ['folders/' + folder.id]);
// const folderFromApi = await service.executeSandboxCall(plugin.id, 'joplin.data.get', ['folders/' + folder.id]);
// expect(folder.id).toBe(folderFromApi.id);
// expect(folder.title).toBe(folderFromApi.title);
// }));

View File

@ -1,5 +1,5 @@
joplin.plugins.register({
onStart: async function() {
await joplin.api.post('folders', null, { title: "multi - simple1" });
await joplin.data.post('folders', null, { title: "multi - simple1" });
},
});

View File

@ -1,5 +1,5 @@
joplin.plugins.register({
onStart: async function() {
await joplin.api.post('folders', null, { title: "multi - simple2" });
await joplin.data.post('folders', null, { title: "multi - simple2" });
},
});

View File

@ -11,7 +11,7 @@ joplin.plugins.register({
let parentId = null;
for (const noteId of noteIds) {
const note = await joplin.api.get('notes/' + noteId, { fields: ['title', 'body', 'parent_id']});
const note = await joplin.data.get('notes/' + noteId, { fields: ['title', 'body', 'parent_id']});
newNoteBody.push([
'# ' + note.title,
'',
@ -27,7 +27,7 @@ joplin.plugins.register({
parent_id: parentId,
};
await joplin.api.post('notes', null, newNote);
await joplin.data.post('notes', null, newNote);
},
});

View File

@ -1,6 +1,6 @@
joplin.plugins.register({
onStart: async function() {
const folder = await joplin.api.post('folders', null, { title: "my plugin folder" });
await joplin.api.post('notes', null, { parent_id: folder.id, title: "testing plugin!" });
const folder = await joplin.data.post('folders', null, { title: "my plugin folder" });
await joplin.data.post('notes', null, { parent_id: folder.id, title: "testing plugin!" });
},
});

View File

@ -2,6 +2,6 @@ const testImport = require('./testImport');
joplin.plugins.register({
onStart: async function() {
await joplin.api.post('folders', null, { title: testImport() });
await joplin.data.post('folders', null, { title: testImport() });
},
});

View File

@ -1 +1 @@
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))((function(o,u){function i(e){try{a(r.next(e))}catch(e){u(e)}}function l(e){try{a(r.throw(e))}catch(e){u(e)}}function a(e){var t;e.done?o(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(i,l)}a((r=r.apply(e,t||[])).next())}))},o=this&&this.__generator||function(e,t){var n,r,o,u,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return u={next:l(0),throw:l(1),return:l(2)},"function"==typeof Symbol&&(u[Symbol.iterator]=function(){return this}),u;function l(u){return function(l){return function(u){if(n)throw new TypeError("Generator is already executing.");for(;i;)try{if(n=1,r&&(o=2&u[0]?r.return:u[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,u[1])).done)return o;switch(r=0,o&&(u=[2&u[0],o.value]),u[0]){case 0:case 1:o=u;break;case 4:return i.label++,{value:u[1],done:!1};case 5:i.label++,r=u[1],u=[0];continue;case 7:u=i.ops.pop(),i.trys.pop();continue;default:if(!(o=i.trys,(o=o.length>0&&o[o.length-1])||6!==u[0]&&2!==u[0])){i=0;continue}if(3===u[0]&&(!o||u[1]>o[0]&&u[1]<o[3])){i.label=u[1];break}if(6===u[0]&&i.label<o[1]){i.label=o[1],o=u;break}if(o&&i.label<o[2]){i.label=o[2],i.ops.push(u);break}o[2]&&i.ops.pop(),i.trys.pop();continue}u=t.call(e,i)}catch(e){u=[6,e],r=0}finally{n=o=0}if(5&u[0])throw u[1];return{value:u[0]?u[1]:void 0,done:!0}}([u,l])}}},u=n(1);joplin.plugins.register({onStart:function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){switch(e.label){case 0:return[4,joplin.api.post("folders",null,{title:u("foo",5)})];case 1:return e.sent(),[2]}}))}))}})},function(e,t,n){"use strict";e.exports=function(e,t,n){if((t-=(e+="").length)<=0)return e;n||0===n||(n=" ");if(" "===(n+="")&&t<10)return r[t]+e;var o="";for(;1&t&&(o+=n),t>>=1;)n+=n;return o+e};var r=[""," "," "," "," "," "," "," "," "," "]}]);
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))((function(o,u){function i(e){try{a(r.next(e))}catch(e){u(e)}}function l(e){try{a(r.throw(e))}catch(e){u(e)}}function a(e){var t;e.done?o(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(i,l)}a((r=r.apply(e,t||[])).next())}))},o=this&&this.__generator||function(e,t){var n,r,o,u,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return u={next:l(0),throw:l(1),return:l(2)},"function"==typeof Symbol&&(u[Symbol.iterator]=function(){return this}),u;function l(u){return function(l){return function(u){if(n)throw new TypeError("Generator is already executing.");for(;i;)try{if(n=1,r&&(o=2&u[0]?r.return:u[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,u[1])).done)return o;switch(r=0,o&&(u=[2&u[0],o.value]),u[0]){case 0:case 1:o=u;break;case 4:return i.label++,{value:u[1],done:!1};case 5:i.label++,r=u[1],u=[0];continue;case 7:u=i.ops.pop(),i.trys.pop();continue;default:if(!(o=i.trys,(o=o.length>0&&o[o.length-1])||6!==u[0]&&2!==u[0])){i=0;continue}if(3===u[0]&&(!o||u[1]>o[0]&&u[1]<o[3])){i.label=u[1];break}if(6===u[0]&&i.label<o[1]){i.label=o[1],o=u;break}if(o&&i.label<o[2]){i.label=o[2],i.ops.push(u);break}o[2]&&i.ops.pop(),i.trys.pop();continue}u=t.call(e,i)}catch(e){u=[6,e],r=0}finally{n=o=0}if(5&u[0])throw u[1];return{value:u[0]?u[1]:void 0,done:!0}}([u,l])}}},u=n(1);joplin.plugins.register({onStart:function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){switch(e.label){case 0:return[4,joplin.data.post("folders",null,{title:u("foo",5)})];case 1:return e.sent(),[2]}}))}))}})},function(e,t,n){"use strict";e.exports=function(e,t,n){if((t-=(e+="").length)<=0)return e;n||0===n||(n=" ");if(" "===(n+="")&&t<10)return r[t]+e;var o="";for(;1&t&&(o+=n),t>>=1;)n+=n;return o+e};var r=[""," "," "," "," "," "," "," "," "," "]}]);

View File

@ -2,6 +2,6 @@ const leftPad = require('left-pad');
joplin.plugins.register({
onStart: async function() {
await joplin.api.post('folders', null, { title: leftPad('foo', 5) });
await joplin.data.post('folders', null, { title: leftPad('foo', 5) });
},
});

View File

@ -12,7 +12,7 @@ const Resource = require('lib/models/Resource.js');
const Tag = require('lib/models/Tag.js');
const NoteTag = require('lib/models/NoteTag.js');
const Revision = require('lib/models/Revision.js');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const Setting = require('lib/models/Setting').default;
const MasterKey = require('lib/models/MasterKey');
const BaseItem = require('lib/models/BaseItem.js');

View File

@ -1,3 +1,6 @@
import Logger from "lib/Logger";
import { PluginMessage } from './services/plugins/PluginRunner'
const { BrowserWindow, Tray, screen } = require('electron');
const shim = require('lib/shim');
const url = require('url');
@ -6,31 +9,40 @@ const { dirname } = require('lib/path-utils');
const fs = require('fs-extra');
const { ipcMain } = require('electron');
// const { ipcMain } = require('electron')
// ipcMain.on('pluginMessage', (event, arg) => {
// console.info('PPPPPPPPPPPPPPPPPPPPPP', arg);
// })
interface RendererProcessQuitReply {
canClose: boolean,
}
interface PluginWindows {
[key: string]: any,
}
class ElectronAppWrapper {
export default class ElectronAppWrapper {
constructor(electronApp, env, profilePath, isDebugMode) {
private logger_:Logger = null;
private electronApp_:any;
private env_:string;
private isDebugMode_:boolean;
private profilePath_:string;
private win_:any = null;
private willQuitApp_:boolean = false;
private tray_:any = null;
private buildDir_:string = null;
private rendererProcessQuitReply_:RendererProcessQuitReply = null;
private pluginWindows_:PluginWindows = {};
constructor(electronApp:any, env:string, profilePath:string, isDebugMode:boolean) {
this.electronApp_ = electronApp;
this.env_ = env;
this.isDebugMode_ = isDebugMode;
this.profilePath_ = profilePath;
this.win_ = null;
this.willQuitApp_ = false;
this.tray_ = null;
this.buildDir_ = null;
this.rendererProcessQuitReply_ = null;
}
electronApp() {
return this.electronApp_;
}
setLogger(v) {
setLogger(v:Logger) {
this.logger_ = v;
}
@ -53,7 +65,7 @@ class ElectronAppWrapper {
const windowStateKeeper = require('electron-window-state');
const stateOptions = {
const stateOptions:any = {
defaultWidth: Math.round(0.8 * screen.getPrimaryDisplay().workArea.width),
defaultHeight: Math.round(0.8 * screen.getPrimaryDisplay().workArea.height),
file: `window-state-${this.env_}.json`,
@ -64,7 +76,7 @@ class ElectronAppWrapper {
// Load the previous state with fallback to defaults
const windowState = windowStateKeeper(stateOptions);
const windowOptions = {
const windowOptions:any = {
x: windowState.x,
y: windowState.y,
width: windowState.width,
@ -86,7 +98,7 @@ class ElectronAppWrapper {
if (shim.isLinux()) windowOptions.icon = path.join(__dirname, '..', 'build/icons/128x128.png');
require('electron-context-menu')({
shouldShowMenu: (event, params) => {
shouldShowMenu: (_event:any, params:any) => {
// params.inputFieldType === 'none' when right-clicking the text editor. This is a bit of a hack to detect it because in this
// case we don't want to use the built-in context menu but a custom one.
return params.isEditable && params.inputFieldType !== 'none';
@ -123,7 +135,7 @@ class ElectronAppWrapper {
}, 3000);
}
this.win_.on('close', (event) => {
this.win_.on('close', (event:any) => {
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
// otherwise the window is simply hidden, and will be re-open once the app is "activated" (which happens when the
// user clicks on the icon in the task bar).
@ -171,7 +183,7 @@ class ElectronAppWrapper {
}
});
ipcMain.on('asynchronous-message', (event, message, args) => {
ipcMain.on('asynchronous-message', (_event:any, message:string, args:any) => {
if (message === 'appCloseReply') {
// We got the response from the renderer process:
// save the response and try quit again.
@ -180,10 +192,21 @@ class ElectronAppWrapper {
}
});
ipcMain.on('pluginMessage', (event, data) => {
// Forward message
if (data.target === 'mainWindow') {
this.win_.webContents.send('pluginMessage', data);
// This handler receives IPC messages from a plugin or from the main window,
// and forwards it to the main window or the plugin window.
ipcMain.on('pluginMessage', (_event:any, message:PluginMessage) => {
if (message.target === 'mainWindow') {
this.win_.webContents.send('pluginMessage', message);
}
if (message.target === 'plugin') {
const win = this.pluginWindows_[message.pluginId];
if (!win) {
this.logger().error('Trying to send IPC message to non-existing plugin window: ' + message.pluginId);
return;
}
win.webContents.send('pluginMessage', message);
}
});
@ -200,6 +223,10 @@ class ElectronAppWrapper {
}
}
registerPluginWindow(pluginId:string, window:any) {
this.pluginWindows_[pluginId] = window;
}
async waitForElectronAppReady() {
if (this.electronApp().isReady()) return Promise.resolve();
@ -258,7 +285,7 @@ class ElectronAppWrapper {
}
// Note: this must be called only after the "ready" event of the app has been dispatched
createTray(contextMenu) {
createTray(contextMenu:any) {
try {
this.tray_ = new Tray(`${this.buildDir()}/icons/${this.trayIconFilename_()}`);
this.tray_.setToolTip(this.electronApp_.name);
@ -325,5 +352,3 @@ class ElectronAppWrapper {
}
}
module.exports = { ElectronAppWrapper };

View File

@ -1,5 +1,5 @@
const { _ } = require('lib/locale');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const InteropService = require('lib/services/interop/InteropService').default;
const CommandService = require('lib/services/CommandService').default;
const Setting = require('lib/models/Setting').default;

View File

@ -8,6 +8,8 @@ import { utils as pluginUtils } from 'lib/services/plugins/reducer';
// import SandboxImplementation from './plugins/SandboxImplementation';
import { MenuItemLocation } from 'lib/services/plugins/MenuItemController';
import { defaultState, State } from 'lib/reducer';
import PluginRunner from './services/plugins/PluginRunner';
import PlatformImplementation from './services/plugins/PlatformImplementation';
require('app-module-path').addPath(__dirname);
@ -18,7 +20,7 @@ const shim = require('lib/shim');
const MasterKey = require('lib/models/MasterKey');
const Folder = require('lib/models/Folder');
const { _, setLocale } = require('lib/locale.js');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const fs = require('fs-extra');
const Tag = require('lib/models/Tag.js');
const { reg } = require('lib/registry.js');
@ -31,7 +33,7 @@ const ResourceService = require('lib/services/ResourceService');
const ClipperServer = require('lib/ClipperServer');
const actionApi = require('lib/services/rest/actionApi.desktop').default;
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const { shell, webFrame, clipboard } = require('electron');
const Menu = bridge().Menu;
const PluginManager = require('lib/services/PluginManager');
@ -1357,23 +1359,24 @@ class Application extends BaseApplication {
pluginLogger.addTarget('console', { prefix: 'Plugin Service:' });
pluginLogger.setLevel(Setting.value('env') == 'dev' ? Logger.LEVEL_DEBUG : Logger.LEVEL_INFO);
const pluginRunner = new PluginRunner();
PluginService.instance().setLogger(pluginLogger);
// PluginService.instance().initialize(SandboxImplementation.instance(), this.store());
PluginService.instance().initialize(PlatformImplementation.instance(), pluginRunner, this.store());
// try {
// if (await shim.fsDriver().exists(Setting.value('pluginDir'))) await PluginService.instance().loadAndRunPlugins(Setting.value('pluginDir'));
// } catch (error) {
// this.logger().error(`There was an error loading plugins from ${Setting.value('pluginDir')}:`, error);
// }
try {
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) await PluginService.instance().loadAndRunPlugins(Setting.value('pluginDir'));
} catch (error) {
this.logger().error(`There was an error loading plugins from ${Setting.value('pluginDir')}:`, error);
}
// try {
// if (Setting.value('plugins.devPluginPaths')) {
// const paths = Setting.value('plugins.devPluginPaths').split(',').map((p:string) => p.trim());
// await PluginService.instance().loadAndRunPlugins(paths);
// }
// } catch (error) {
// this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error);
// }
try {
if (Setting.value('plugins.devPluginPaths')) {
const paths = Setting.value('plugins.devPluginPaths').split(',').map((p:string) => p.trim());
await PluginService.instance().loadAndRunPlugins(paths);
}
} catch (error) {
this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error);
}
// const url = require('url');
@ -1395,7 +1398,7 @@ class Application extends BaseApplication {
// const ipcRenderer = require('electron').ipcRenderer;
// ipcRenderer.on('pluginMessage', (event:any, data:any) => {
// console.info('GOT MESSAGE ON MAIN WINDOW', event, data);
// console.info('RENDERER PROCESS got message', data);
// });

View File

@ -1,13 +1,26 @@
import ElectronAppWrapper from "./ElectronAppWrapper";
const { _, setLocale } = require('lib/locale.js');
const shim = require('lib/shim');
const { dirname, toSystemSlashes } = require('lib/path-utils.js');
const { BrowserWindow, nativeTheme } = require('electron');
class Bridge {
interface LastSelectedPath {
file: string,
directory: string,
}
constructor(electronWrapper) {
interface LastSelectedPaths {
[key:string]: LastSelectedPath,
}
export class Bridge {
private electronWrapper_:ElectronAppWrapper;
private lastSelectedPaths_:LastSelectedPaths;
constructor(electronWrapper:ElectronAppWrapper) {
this.electronWrapper_ = electronWrapper;
this.autoUpdateLogger_ = null;
this.lastSelectedPaths_ = {
file: null,
directory: null,
@ -30,11 +43,11 @@ class Bridge {
return this.electronWrapper_.window();
}
showItemInFolder(fullPath) {
showItemInFolder(fullPath:string) {
return require('electron').shell.showItemInFolder(toSystemSlashes(fullPath));
}
newBrowserWindow(options) {
newBrowserWindow(options:any) {
return new BrowserWindow(options);
}
@ -50,7 +63,7 @@ class Bridge {
return { width: s[0], height: s[1] };
}
windowSetSize(width, height) {
windowSetSize(width:number, height:number) {
if (!this.window()) return;
return this.window().setSize(width, height);
}
@ -63,7 +76,7 @@ class Bridge {
return this.window().webContents.closeDevTools();
}
showSaveDialog(options) {
showSaveDialog(options:any) {
const { dialog } = require('electron');
if (!options) options = {};
if (!('defaultPath' in options) && this.lastSelectedPaths_.file) options.defaultPath = this.lastSelectedPaths_.file;
@ -74,7 +87,7 @@ class Bridge {
return filePath;
}
showOpenDialog(options) {
showOpenDialog(options:any) {
const { dialog } = require('electron');
if (!options) options = {};
let fileType = 'file';
@ -89,13 +102,13 @@ class Bridge {
}
// Don't use this directly - call one of the showXxxxxxxMessageBox() instead
showMessageBox_(window, options) {
showMessageBox_(window:any, options:any) {
const { dialog } = require('electron');
if (!window) window = this.window();
return dialog.showMessageBoxSync(window, options);
}
showErrorMessageBox(message) {
showErrorMessageBox(message:string) {
return this.showMessageBox_(this.window(), {
type: 'error',
message: message,
@ -103,7 +116,7 @@ class Bridge {
});
}
showConfirmMessageBox(message, options = null) {
showConfirmMessageBox(message:string, options:any = null) {
if (options === null) options = {};
const result = this.showMessageBox_(this.window(), Object.assign({}, {
@ -117,7 +130,7 @@ class Bridge {
}
/* returns the index of the clicked button */
showMessageBox(message, options = null) {
showMessageBox(message:string, options:any = null) {
if (options === null) options = {};
const result = this.showMessageBox_(this.window(), Object.assign({}, {
@ -129,7 +142,7 @@ class Bridge {
return result;
}
showInfoMessageBox(message, options = {}) {
showInfoMessageBox(message:string, options:any = {}) {
const result = this.showMessageBox_(this.window(), Object.assign({}, {
type: 'info',
message: message,
@ -138,7 +151,7 @@ class Bridge {
return result === 0;
}
setLocale(locale) {
setLocale(locale:string) {
setLocale(locale);
}
@ -150,15 +163,15 @@ class Bridge {
return require('electron').MenuItem;
}
openExternal(url) {
openExternal(url:string) {
return require('electron').shell.openExternal(url);
}
openItem(fullPath) {
openItem(fullPath:string) {
return require('electron').shell.openItem(fullPath);
}
checkForUpdates(inBackground, window, logFilePath, options) {
checkForUpdates(inBackground:boolean, window:any, logFilePath:string, options:any) {
const { checkForUpdates } = require('./checkForUpdates.js');
checkForUpdates(inBackground, window, logFilePath, options);
}
@ -175,7 +188,7 @@ class Bridge {
return nativeTheme.shouldUseDarkColors;
}
addEventListener(name, fn) {
addEventListener(name:string, fn:Function) {
if (name === 'nativeThemeUpdated') {
nativeTheme.on('updated', fn);
} else {
@ -205,17 +218,15 @@ class Bridge {
}
let bridge_ = null;
let bridge_:Bridge = null;
function initBridge(wrapper) {
export function initBridge(wrapper:ElectronAppWrapper) {
if (bridge_) throw new Error('Bridge already initialized');
bridge_ = new Bridge(wrapper);
return bridge_;
}
function bridge() {
export default function bridge() {
if (!bridge_) throw new Error('Bridge not initialized');
return bridge_;
}
module.exports = { bridge, initBridge };

View File

@ -1,6 +1,6 @@
const { dialog } = require('electron');
const shim = require('lib/shim');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const { _ } = require('lib/locale.js');
const fetch = require('node-fetch');
const { fileExtension } = require('lib/path-utils.js');

View File

@ -2,7 +2,7 @@ import { CommandRuntime, CommandDeclaration } from '../lib/services/CommandServi
const { _ } = require('lib/locale');
const Note = require('lib/models/Note');
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
interface Props {
noteId: string

View File

@ -1,6 +1,6 @@
const React = require('react');
const { connect } = require('react-redux');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js');
const ClipperServer = require('lib/ClipperServer');

View File

@ -9,7 +9,7 @@ const pathUtils = require('lib/path-utils.js');
const { _ } = require('lib/locale.js');
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
const shared = require('lib/components/shared/config-shared.js');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const { EncryptionConfigScreen } = require('../EncryptionConfigScreen.min');
const { ClipperConfigScreen } = require('../ClipperConfigScreen.min');
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import ButtonBar from './ConfigScreen/ButtonBar';
const { connect } = require('react-redux');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js');
const Shared = require('lib/components/shared/dropbox-login-shared');

View File

@ -8,7 +8,7 @@ const { time } = require('lib/time-utils.js');
const shim = require('lib/shim');
const dialogs = require('./dialogs');
const shared = require('lib/components/shared/encryption-config-shared.js');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
class EncryptionConfigScreenComponent extends React.Component {
constructor() {

View File

@ -1,5 +1,5 @@
const React = require('react');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const styleSelector = require('./style/ExtensionBadge');
const { _ } = require('lib/locale.js');

View File

@ -8,7 +8,7 @@ import useKeymap from './utils/useKeymap';
import useCommandStatus from './utils/useCommandStatus';
import styles_ from './styles';
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const shim = require('lib/shim');
const { _ } = require('lib/locale');

View File

@ -23,7 +23,7 @@ const Setting = require('lib/models/Setting').default;
const shim = require('lib/shim');
const { themeStyle } = require('lib/theme.js');
const { _ } = require('lib/locale.js');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const PluginManager = require('lib/services/PluginManager');
const EncryptionService = require('lib/services/EncryptionService');
const ipcRenderer = require('electron').ipcRenderer;

View File

@ -2,7 +2,7 @@ import { CommandRuntime, CommandDeclaration } from '../../../lib/services/Comman
const Note = require('lib/models/Note');
const { _ } = require('lib/locale');
const shim = require('lib/shim');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const InteropServiceHelper = require('../../../InteropServiceHelper.js');
export const declaration:CommandDeclaration = {

View File

@ -1,7 +1,7 @@
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
const { _ } = require('lib/locale');
const Folder = require('lib/models/Folder');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
export const declaration:CommandDeclaration = {
name: 'newFolder',

View File

@ -1,6 +1,6 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
const { _ } = require('lib/locale');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
export const declaration:CommandDeclaration = {
name: 'print',

View File

@ -1,7 +1,7 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
const Folder = require('lib/models/Folder');
const { _ } = require('lib/locale');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
export const declaration:CommandDeclaration = {
name: 'renameFolder',

View File

@ -1,7 +1,7 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
const Tag = require('lib/models/Tag');
const { _ } = require('lib/locale');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
export const declaration:CommandDeclaration = {
name: 'renameTag',

View File

@ -3,7 +3,7 @@ import * as React from 'react';
import NoteListUtils from './utils/NoteListUtils';
const { buildStyle } = require('lib/theme');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
interface MultiNoteActionsProps {
themeId: number,

View File

@ -2,7 +2,7 @@ const React = require('react');
const Component = React.Component;
const Setting = require('lib/models/Setting').default;
const { connect } = require('react-redux');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
class NavigatorComponent extends Component {
UNSAFE_componentWillReceiveProps(newProps) {

View File

@ -15,7 +15,7 @@ import usePluginServiceRegistration from '../../utils/usePluginServiceRegistrati
import Setting from 'lib/models/Setting';
// @ts-ignore
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
// @ts-ignore
const Note = require('lib/models/Note.js');
const { clipboard } = require('electron');

View File

@ -35,7 +35,7 @@ const usePrevious = require('lib/hooks/usePrevious').default;
const Setting = require('lib/models/Setting').default;
const { _ } = require('lib/locale');
const Note = require('lib/models/Note.js');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
const NoteRevisionViewer = require('../NoteRevisionViewer.min');
const TagList = require('../TagList.min.js');

View File

@ -1,6 +1,6 @@
import ResourceEditWatcher from '../../../lib/services/ResourceEditWatcher/index';
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const Resource = require('lib/models/Resource.js');

View File

@ -3,7 +3,7 @@ const Note = require('lib/models/Note.js');
const BaseModel = require('lib/BaseModel.js');
const Resource = require('lib/models/Resource.js');
const shim = require('lib/shim');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const ResourceFetcher = require('lib/services/ResourceFetcher.js');
const { reg } = require('lib/registry.js');
const joplinRendererUtils = require('lib/joplin-renderer').utils;

View File

@ -6,7 +6,7 @@ const BaseItem = require('lib/models/BaseItem');
const { _ } = require('lib/locale');
const BaseModel = require('lib/BaseModel.js');
const Resource = require('lib/models/Resource.js');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const { urlDecode } = require('lib/string-utils');
const urlUtils = require('lib/urlUtils');
const ResourceFetcher = require('lib/services/ResourceFetcher.js');

View File

@ -8,7 +8,7 @@ const { time } = require('lib/time-utils.js');
const { themeStyle } = require('lib/theme');
const BaseModel = require('lib/BaseModel');
const { _ } = require('lib/locale.js');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const Note = require('lib/models/Note');
const Setting = require('lib/models/Setting').default;
const NoteListItem = require('../NoteListItem').default;

View File

@ -6,7 +6,7 @@ const DialogButtonRow = require('./DialogButtonRow.min');
const Datetime = require('react-datetime');
const Note = require('lib/models/Note');
const formatcoords = require('formatcoords');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const shim = require('lib/shim');
class NotePropertiesDialog extends React.Component {

View File

@ -14,7 +14,7 @@ const { MarkupToHtml } = require('lib/joplin-renderer');
const { time } = require('lib/time-utils.js');
const ReactTooltip = require('react-tooltip');
const { urlDecode, substrWithEllipsis } = require('lib/string-utils');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const markupLanguageUtils = require('lib/markupLanguageUtils');
class NoteRevisionViewerComponent extends React.PureComponent {

View File

@ -4,7 +4,7 @@ import ButtonBar from './ConfigScreen/ButtonBar';
const { connect } = require('react-redux');
const { reg } = require('lib/registry.js');
const Setting = require('lib/models/Setting').default;
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js');
const { OneDriveApiNodeUtils } = require('lib/onedrive-api-node-utils.js');

View File

@ -4,7 +4,7 @@ import ButtonBar from './ConfigScreen/ButtonBar';
const { connect } = require('react-redux');
const { _ } = require('lib/locale.js');
const { themeStyle } = require('lib/theme');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const prettyBytes = require('pretty-bytes');
const Resource = require('lib/models/Resource.js');

View File

@ -19,7 +19,7 @@ const { ResourceScreen } = require('./ResourceScreen.js');
const { Navigator } = require('./Navigator.min.js');
const WelcomeUtils = require('lib/WelcomeUtils');
const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
interface Props {
themeId: number,

View File

@ -5,7 +5,7 @@ import useSyncTargetUpgrade, { SyncTargetUpgradeResult } from 'lib/services/sync
const { render } = require('react-dom');
const ipcRenderer = require('electron').ipcRenderer;
const Setting = require('lib/models/Setting').default;
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
function useAppCloseHandler(upgradeResult:SyncTargetUpgradeResult) {
useEffect(function() {

View File

@ -15,7 +15,7 @@ const Tag = require('lib/models/Tag.js');
const shim = require('lib/shim');
const { _ } = require('lib/locale.js');
const { themeStyle } = require('lib/theme');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const InteropServiceHelper = require('../../InteropServiceHelper.js');

View File

@ -4,7 +4,7 @@ import ButtonBar from '../ConfigScreen/ButtonBar';
const { connect } = require('react-redux');
const Setting = require('lib/models/Setting').default;
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js');
const { ReportService } = require('lib/services/report.js');

View File

@ -1,5 +1,5 @@
const React = require('react');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
class VerticalResizer extends React.PureComponent {
constructor() {

View File

@ -3,7 +3,7 @@ import { utils as pluginUtils, PluginStates } from 'lib/services/plugins/reducer
const BaseModel = require('lib/BaseModel');
const { _ } = require('lib/locale.js');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const eventManager = require('lib/eventManager').default;

View File

@ -23,11 +23,11 @@ const NoteTag = require('lib/models/NoteTag.js');
const MasterKey = require('lib/models/MasterKey');
const Setting = require('lib/models/Setting').default;
const Revision = require('lib/models/Revision.js');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const { FsDriverNode } = require('lib/fs-driver-node.js');
const { shimInit } = require('lib/shim-init-node.js');
const EncryptionService = require('lib/services/EncryptionService');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
if (bridge().env() === 'dev') {

View File

@ -4,9 +4,9 @@
require('app-module-path').addPath(__dirname);
const electronApp = require('electron').app;
const { ElectronAppWrapper } = require('./ElectronAppWrapper');
const ElectronAppWrapper = require('./ElectronAppWrapper').default;
const { initBridge } = require('./bridge');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const { FsDriverNode } = require('lib/fs-driver-node.js');
const envFromArgs = require('lib/envFromArgs');

View File

@ -0,0 +1,9 @@
// Just a convenient wrapper to get a typed bridge in TypeScript
import { Bridge } from '../bridge';
const remoteBridge = require('electron').remote.require('./bridge').default;
export default function bridge():Bridge {
return remoteBridge();
}

View File

@ -0,0 +1,116 @@
import Plugin from 'lib/services/plugins/Plugin';
import BasePluginRunner from 'lib/services/plugins/BasePluginRunner';
import executeSandboxCall from 'lib/services/plugins/utils/executeSandboxCall';
import Global from 'lib/services/plugins/api/Global';
import bridge from '../bridge';
import Setting from 'lib/models/Setting';
import { EventHandlers } from 'lib/services/plugins/utils/mapEventHandlersToIds';
const shim = require('lib/shim');
const ipcRenderer = require('electron').ipcRenderer;
enum PluginMessageTarget {
MainWindow = 'mainWindow',
Plugin = 'plugin',
}
export interface PluginMessage {
target: PluginMessageTarget,
pluginId: string,
callbackId?: string,
path?: string,
args?: any[],
result?: any,
error?: any,
}
function mapEventIdsToHandlers(pluginId:string, arg:any) {
if (Array.isArray(arg)) {
for (let i = 0; i < arg.length; i++) {
arg[i] = mapEventIdsToHandlers(pluginId, arg[i]);
}
return arg;
} else if (typeof arg === 'string' && arg.indexOf('___plugin_event_') === 0) {
const eventId = arg;
return async (...args:any[]) => {
ipcRenderer.send('pluginMessage', {
target: PluginMessageTarget.Plugin,
pluginId: pluginId,
eventId: eventId,
args: args,
});
};
} else if (arg === null) {
return null;
} else if (arg === undefined) {
return undefined;
} else if (typeof arg === 'object') {
for (const n in arg) {
arg[n] = mapEventIdsToHandlers(pluginId, arg[n]);
}
}
return arg;
}
export default class PluginRunner extends BasePluginRunner {
protected eventHandlers_:EventHandlers = {};
constructor() {
super();
this.eventHandler = this.eventHandler.bind(this);
}
private async eventHandler(eventHandlerId:string, args:any[]) {
const cb = this.eventHandlers_[eventHandlerId];
return cb(...args);
}
async run(plugin:Plugin, pluginApi:Global) {
const scriptPath = `${Setting.value('tempDir')}/plugin_${plugin.id}.js`;
await shim.fsDriver().writeFile(scriptPath, plugin.scriptText, 'utf8');
const pluginWindow = bridge().newBrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
},
});
bridge().electronApp().registerPluginWindow(plugin.id, pluginWindow);
pluginWindow.loadURL(`${require('url').format({
pathname: require('path').join(__dirname, 'plugin_index.html'),
protocol: 'file:',
slashes: true,
})}?pluginId=${plugin.id}&pluginScript=file://${scriptPath}`); // TODO: escape!!
pluginWindow.webContents.openDevTools();
ipcRenderer.on('pluginMessage', async (_event:any, message:PluginMessage) => {
if (message.target !== PluginMessageTarget.MainWindow) return;
if (message.pluginId !== plugin.id) return;
const mappedArgs = mapEventIdsToHandlers(plugin.id, message.args);
let result:any = null;
let error:any = null;
try {
result = await executeSandboxCall(plugin.id, pluginApi, `joplin.${message.path}`, mappedArgs, this.eventHandler);
} catch (e) {
error = e ? e : new Error('Unknown error');
}
ipcRenderer.send('pluginMessage', {
target: PluginMessageTarget.Plugin,
pluginId: plugin.id,
callbackId: message.callbackId,
result: result,
error: error,
});
});
}
}

View File

@ -1,24 +1,24 @@
(function(globalObject) {
// TODO: Not sure if that will work once packaged in Electron
const sandboxProxy = require('../lib/services/plugins/sandboxProxy.js').default;
const sandboxProxy = require('../../lib/services/plugins/sandboxProxy.js').default;
const ipcRenderer = require('electron').ipcRenderer;
const urlParams = new URLSearchParams(window.location.search);
const pluginId = urlParams.get('pluginId');
let callbackId_ = 1;
const callbacks_ = {};
let eventId_ = 1;
const eventHandlers_ = {};
function mapFunctionsToCallbacks(arg) {
function mapEventHandlersToIds(argName, arg) {
if (Array.isArray(arg)) {
for (let i = 0; i < arg.length; i++) {
arg[i] = mapFunctionsToCallbacks(arg[i]);
arg[i] = mapEventHandlersToIds(`${i}`, arg[i]);
}
return arg;
} else if (typeof arg === 'function') {
const id = `__event#${callbackId_}`;
callbackId_++;
callbacks_[id] = arg;
const id = `___plugin_event_${argName}_${eventId_}`;
eventId_++;
eventHandlers_[id] = arg;
return id;
} else if (arg === null) {
return null;
@ -26,16 +26,68 @@
return undefined;
} else if (typeof arg === 'object') {
for (const n in arg) {
arg[n] = mapFunctionsToCallbacks(arg[n]);
arg[n] = mapEventHandlersToIds(n, arg[n]);
}
}
return arg;
}
const callbackPromises = {};
let callbackIndex = 1;
const target = (path, args) => {
ipcRenderer.send('pluginMessage', { target: 'mainWindow', pluginId: pluginId, path: path, args: mapFunctionsToCallbacks(args) });
const callbackId = `cb_${pluginId}_${Date.now()}_${callbackIndex++}`;
const promise = new Promise((resolve, reject) => {
callbackPromises[callbackId] = { resolve, reject };
});
ipcRenderer.send('pluginMessage', {
target: 'mainWindow',
pluginId: pluginId,
callbackId: callbackId,
path: path,
args: mapEventHandlersToIds(null, args),
});
return promise;
};
ipcRenderer.on('pluginMessage', (event, message) => {
if (message.eventId) {
const eventHandler = eventHandlers_[message.eventId];
if (!eventHandler) {
console.error('Got an event ID but no matching event handler: ', message);
return;
}
eventHandler(...message.args);
return;
}
if (message.callbackId) {
const promise = callbackPromises[message.callbackId];
if (!promise) {
console.error('Got a callback without matching promise: ', message);
return;
}
if (message.error) {
promise.reject(message.error);
} else {
promise.resolve(message.result);
}
return;
}
console.warn('Unhandled plugin message:', message);
});
const pluginScriptPath = urlParams.get('pluginScript');
const script = document.createElement('script');
script.src = pluginScriptPath;
document.head.appendChild(script);
globalObject.joplin = sandboxProxy(target);
})(window);

View File

@ -10,7 +10,7 @@ const BaseItem = require('lib/models/BaseItem.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
const Setting = require('lib/models/Setting').default;
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const { splitCommandString } = require('lib/string-utils.js');
const { reg } = require('lib/registry.js');
const { time } = require('lib/time-utils.js');

View File

@ -1,6 +1,6 @@
const urlParser = require('url');
const Setting = require('lib/models/Setting').default;
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const { randomClipperPort, startPort } = require('lib/randomClipperPort');
const enableServerDestroy = require('server-destroy');
const Api = require('lib/services/rest/Api');

View File

@ -1,4 +1,4 @@
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const shim = require('lib/shim');
const JoplinError = require('lib/JoplinError');
const { time } = require('lib/time-utils');

View File

@ -1,4 +1,4 @@
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const shim = require('lib/shim');
const JoplinError = require('lib/JoplinError');
const { rtrimSlashes } = require('lib/path-utils.js');

View File

@ -1,22 +1,52 @@
const moment = require('moment');
const { _ } = require('lib/locale.js');
const { time } = require('lib/time-utils.js');
const { FsDriverDummy } = require('lib/fs-driver-dummy.js');
enum TargetType {
Database = 'database',
File = 'file',
Console = 'console',
}
enum LogLevel {
None = 0,
Error = 10,
Warn = 20,
Info = 30,
Debug = 40,
}
interface Target {
type: TargetType,
level?: LogLevel,
database?: any,
console?: any,
prefix?: string,
path?: string,
source?: string,
}
class Logger {
constructor() {
this.targets_ = [];
this.level_ = Logger.LEVEL_INFO;
this.fileAppendQueue_ = [];
this.lastDbCleanup_ = time.unixMs();
}
// For backward compatibility
public static LEVEL_NONE = LogLevel.None;
public static LEVEL_ERROR = LogLevel.Error;
public static LEVEL_WARN = LogLevel.Warn;
public static LEVEL_INFO = LogLevel.Info;
public static LEVEL_DEBUG = LogLevel.Debug;
public static fsDriver_:any = null;
private targets_:Target[] = [];
private level_:LogLevel = LogLevel.Info;
private lastDbCleanup_:number = time.unixMs();
static fsDriver() {
if (!Logger.fsDriver_) Logger.fsDriver_ = new FsDriverDummy();
return Logger.fsDriver_;
}
setLevel(level) {
setLevel(level:LogLevel) {
this.level_ = level;
}
@ -28,25 +58,22 @@ class Logger {
return this.targets_;
}
clearTargets() {
this.targets_.clear();
}
addTarget(type, options = null) {
addTarget(type:TargetType, options:any = null) {
const target = { type: type };
for (const n in options) {
if (!options.hasOwnProperty(n)) continue;
target[n] = options[n];
(target as any)[n] = options[n];
}
this.targets_.push(target);
}
objectToString(object) {
objectToString(object:any) {
let output = '';
if (typeof object === 'object') {
if (object instanceof Error) {
object = object as any;
output = object.toString();
if (object.code) output += `\nCode: ${object.code}`;
if (object.headers) output += `\nHeader: ${JSON.stringify(object.headers)}`;
@ -62,7 +89,7 @@ class Logger {
return output;
}
objectsToString(...object) {
objectsToString(...object:any[]) {
const output = [];
for (let i = 0; i < object.length; i++) {
output.push(`"${this.objectToString(object[i])}"`);
@ -84,9 +111,9 @@ class Logger {
}
// Only for database at the moment
async lastEntries(limit = 100, options = null) {
async lastEntries(limit:number = 100, options:any = null) {
if (options === null) options = {};
if (!options.levels) options.levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
if (!options.levels) options.levels = [LogLevel.Debug, LogLevel.Info, LogLevel.Warn, LogLevel.Error];
if (!options.levels.length) return [];
for (let i = 0; i < this.targets_.length; i++) {
@ -100,12 +127,12 @@ class Logger {
return [];
}
targetLevel(target) {
targetLevel(target:Target) {
if ('level' in target) return target.level;
return this.level();
}
log(level, ...object) {
log(level:LogLevel, ...object:any[]) {
if (!this.targets_.length) return;
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
@ -118,9 +145,9 @@ class Logger {
if (target.type == 'console') {
let fn = 'log';
if (level == Logger.LEVEL_ERROR) fn = 'error';
if (level == Logger.LEVEL_WARN) fn = 'warn';
if (level == Logger.LEVEL_INFO) fn = 'info';
if (level == LogLevel.Error) fn = 'error';
if (level == LogLevel.Warn) fn = 'warn';
if (level == LogLevel.Info) fn = 'info';
const consoleObj = target.console ? target.console : console;
let items = [moment().format('HH:mm:ss')];
if (target.prefix) {
@ -160,55 +187,41 @@ class Logger {
}
}
error(...object) {
return this.log(Logger.LEVEL_ERROR, ...object);
error(...object:any[]) {
return this.log(LogLevel.Error, ...object);
}
warn(...object) {
return this.log(Logger.LEVEL_WARN, ...object);
warn(...object:any[]) {
return this.log(LogLevel.Warn, ...object);
}
info(...object) {
return this.log(Logger.LEVEL_INFO, ...object);
info(...object:any[]) {
return this.log(LogLevel.Info, ...object);
}
debug(...object) {
return this.log(Logger.LEVEL_DEBUG, ...object);
debug(...object:any[]) {
return this.log(LogLevel.Debug, ...object);
}
static levelStringToId(s) {
if (s == 'none') return Logger.LEVEL_NONE;
if (s == 'error') return Logger.LEVEL_ERROR;
if (s == 'warn') return Logger.LEVEL_WARN;
if (s == 'info') return Logger.LEVEL_INFO;
if (s == 'debug') return Logger.LEVEL_DEBUG;
throw new Error(_('Unknown log level: %s', s));
static levelStringToId(s:string) {
if (s == 'none') return LogLevel.None;
if (s == 'error') return LogLevel.Error;
if (s == 'warn') return LogLevel.Warn;
if (s == 'info') return LogLevel.Info;
if (s == 'debug') return LogLevel.Debug;
throw new Error('Unknown log level: ' + s);
}
static levelIdToString(id) {
if (id == Logger.LEVEL_NONE) return 'none';
if (id == Logger.LEVEL_ERROR) return 'error';
if (id == Logger.LEVEL_WARN) return 'warn';
if (id == Logger.LEVEL_INFO) return 'info';
if (id == Logger.LEVEL_DEBUG) return 'debug';
throw new Error(_('Unknown level ID: %s', id));
static levelIdToString(id:LogLevel) {
if (id == LogLevel.None) return 'none';
if (id == LogLevel.Error) return 'error';
if (id == LogLevel.Warn) return 'warn';
if (id == LogLevel.Info) return 'info';
if (id == LogLevel.Debug) return 'debug';
throw new Error('Unknown level ID: ' + id);
}
static levelIds() {
return [Logger.LEVEL_NONE, Logger.LEVEL_ERROR, Logger.LEVEL_WARN, Logger.LEVEL_INFO, Logger.LEVEL_DEBUG];
return [LogLevel.None, LogLevel.Error, LogLevel.Warn, LogLevel.Info, LogLevel.Debug];
}
static levelEnum() {
const output = {};
const ids = this.levelIds();
for (let i = 0; i < ids.length; i++) {
output[ids[i]] = this.levelIdToString(ids[i]);
}
return output;
}
}
Logger.LEVEL_NONE = 0;
Logger.LEVEL_ERROR = 10;
Logger.LEVEL_WARN = 20;
Logger.LEVEL_INFO = 30;
Logger.LEVEL_DEBUG = 40;
module.exports = { Logger };
export default Logger;

View File

@ -1,6 +1,6 @@
const { time } = require('lib/time-utils.js');
const Setting = require('lib/models/Setting').default;
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
class TaskQueue {
constructor(name) {

View File

@ -1,4 +1,4 @@
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const shim = require('lib/shim');
const parseXmlString = require('xml2js').parseString;
const JoplinError = require('lib/JoplinError');

View File

@ -6,7 +6,7 @@ const { reg } = require('lib/registry.js');
const { ScreenHeader } = require('lib/components/screen-header.js');
const { time } = require('lib/time-utils');
const { themeStyle } = require('lib/components/global-style.js');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const { BaseScreenComponent } = require('lib/components/base-screen.js');
const { _ } = require('lib/locale.js');

View File

@ -1,4 +1,4 @@
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const { time } = require('lib/time-utils.js');
const Mutex = require('async-mutex').Mutex;
const shim = require('lib/shim');

View File

@ -1,5 +1,5 @@
const { isHidden } = require('lib/path-utils.js');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const shim = require('lib/shim');
const BaseItem = require('lib/models/BaseItem.js');
const JoplinError = require('lib/JoplinError');

View File

@ -1,5 +1,5 @@
const ntpClient = require('lib/vendor/ntp-client');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const Mutex = require('async-mutex').Mutex;
let nextSyncTime = 0;

View File

@ -1,7 +1,7 @@
const shim = require('lib/shim');
const { stringify } = require('query-string');
const { time } = require('lib/time-utils.js');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const { _ } = require('lib/locale.js');
class OneDriveApi {

View File

@ -1,4 +1,4 @@
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
class ReactLogger extends Logger {}

View File

@ -1,4 +1,4 @@
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const Setting = require('lib/models/Setting').default;
const shim = require('lib/shim');
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');

View File

@ -1,5 +1,5 @@
const notifier = require('node-notifier');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const shim = require('lib/shim');
class AlarmServiceDriverNode {

View File

@ -1,15 +1,17 @@
import Logger from 'lib/Logger';
export default class BaseService {
static logger_:any = null;
protected instanceLogger_:any = null;
static logger_:Logger = null;
protected instanceLogger_:Logger = null;
logger() {
logger():Logger {
if (this.instanceLogger_) return this.instanceLogger_;
if (!BaseService.logger_) throw new Error('BaseService.logger_ not set!!');
return BaseService.logger_;
}
setLogger(v:any) {
setLogger(v:Logger) {
this.instanceLogger_ = v;
}
}

View File

@ -3,7 +3,7 @@ const BaseModel = require('lib/BaseModel');
const MasterKey = require('lib/models/MasterKey');
const Resource = require('lib/models/Resource');
const ResourceService = require('lib/services/ResourceService');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const EventEmitter = require('events');
const shim = require('lib/shim');

View File

@ -1,5 +1,5 @@
const { padLeft } = require('lib/string-utils.js');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const shim = require('lib/shim');
const Setting = require('lib/models/Setting').default;
const MasterKey = require('lib/models/MasterKey');

View File

@ -1,4 +1,4 @@
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const Note = require('lib/models/Note');
const Setting = require('lib/models/Setting').default;
const shim = require('lib/shim');
@ -7,7 +7,7 @@ const { splitCommandString } = require('lib/string-utils');
const { fileExtension, basename } = require('lib/path-utils');
const spawn = require('child_process').spawn;
const chokidar = require('chokidar');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const { time } = require('lib/time-utils.js');
const { ErrorNotFound } = require('./rest/errors');

View File

@ -1,4 +1,4 @@
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const KeymapService = require('lib/services/KeymapService').default;
class PluginManager {

View File

@ -1,11 +1,11 @@
import AsyncActionQueue from '../../AsyncActionQueue';
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const Setting = require('lib/models/Setting').default;
const Resource = require('lib/models/Resource');
const shim = require('lib/shim');
const EventEmitter = require('events');
const chokidar = require('chokidar');
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
const { _ } = require('lib/locale');
interface WatchedItem {

View File

@ -3,7 +3,7 @@ const Setting = require('lib/models/Setting').default;
const BaseService = require('lib/services/BaseService').default;
const ResourceService = require('lib/services/ResourceService');
const { Dirnames } = require('lib/services/synchronizer/utils/types');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const EventEmitter = require('events');
const shim = require('lib/shim');

View File

@ -2,39 +2,8 @@ import Plugin from './Plugin';
import BaseService from '../BaseService';
import Global from './api/Global';
interface EventHandlers {
[key:string]: Function;
}
export default abstract class BasePluginRunner extends BaseService {
private eventHandlerIndex_:number = 0;
protected eventHandlers_:EventHandlers = {};
protected mapEventHandlersToIds(arg:any) {
if (Array.isArray(arg)) {
for (let i = 0; i < arg.length; i++) {
arg[i] = this.mapEventHandlersToIds(arg[i]);
}
return arg;
} else if (typeof arg === 'function') {
const id = `__event#${this.eventHandlerIndex_}`;
this.eventHandlerIndex_++;
this.eventHandlers_[id] = arg;
return id;
} else if (arg === null) {
return null;
} else if (arg === undefined) {
return undefined;
} else if (typeof arg === 'object') {
for (const n in arg) {
arg[n] = this.mapEventHandlersToIds(arg[n]);
}
}
return arg;
}
async run(plugin:Plugin, sandbox:Global) {
throw new Error(`Not implemented: ${plugin} / ${sandbox}`);
}

View File

@ -1,8 +1,6 @@
import Plugin from 'lib/services/plugins/Plugin';
import manifestFromObject from 'lib/services/plugins/utils/manifestFromObject';
import Global from 'lib/services/plugins/api/Global';
import { SandboxContext } from 'lib/services/plugins/utils/types';
// import sandboxProxy from 'lib/services/plugins/sandboxProxy';
import BasePluginRunner from 'lib/services/plugins/BasePluginRunner';
import BaseService from '../BaseService';
const shim = require('lib/shim');
@ -13,10 +11,6 @@ interface Plugins {
[key:string]: Plugin
}
// interface Sandboxes {
// [key:string]: Sandbox;
// }
function makePluginId(source:string):string {
// https://www.npmjs.com/package/slug#options
return nodeSlug(source, nodeSlug.defaults.modes['rfc3986']).substr(0,32);
@ -37,7 +31,6 @@ export default class PluginService extends BaseService {
private store_:any = null;
private platformImplementation_:any = null;
private plugins_:Plugins = {};
// private sandboxes_:Sandboxes = {};
private runner_:BasePluginRunner = null;
initialize(platformImplementation:any, runner:BasePluginRunner, store:any) {
@ -117,134 +110,10 @@ export default class PluginService extends BaseService {
}
}
// cliSandbox(pluginId:string) {
// let callbackId_ = 1;
// const callbacks_:any = {};
// function mapFunctionsToCallbacks(arg:any) {
// if (Array.isArray(arg)) {
// for (let i = 0; i < arg.length; i++) {
// arg[i] = mapFunctionsToCallbacks(arg[i]);
// }
// return arg;
// } else if (typeof arg === 'function') {
// const id = '__event#' + callbackId_;
// callbackId_++;
// callbacks_[id] = arg;
// return id;
// } else if (arg === null) {
// return null;
// } else if (arg === undefined) {
// return undefined;
// } else if (typeof arg === 'object') {
// for (const n in arg) {
// arg[n] = mapFunctionsToCallbacks(arg[n]);
// }
// }
// return arg;
// }
// const target = (path:string, args:any[]) => {
// console.info('GOT PATH', path, mapFunctionsToCallbacks(args));
// this.executeSandboxCall(pluginId, 'joplin.' + path, mapFunctionsToCallbacks(args));
// };
// return {
// joplin: sandboxProxy(target),
// }
// }
async runPlugin(plugin:Plugin) {
this.plugins_[plugin.id] = plugin;
// Context contains the data that is sent from the plugin to the app
// Currently it only contains the object that's registered when
// the plugin calls `joplin.plugins.register()`
const context:SandboxContext = {
runtime: null,
};
const sandbox = new Global(this.platformImplementation_, plugin, this.store_, context);
// this.sandboxes_[plugin.id] = sandbox;
await this.runner_.run(plugin, sandbox);
// const vmSandbox = vm.createContext(this.cliSandbox(plugin.id));
// try {
// vm.runInContext(plugin.scriptText, vmSandbox);
// } catch (error) {
// this.logger().error(`In plugin ${plugin.id}:`, error);
// return;
// }
if (!context.runtime) {
throw new Error(`Plugin ${plugin.id}: The plugin was not registered! Call joplin.plugins.register({.....}) from within the plugin.`);
}
if (context.runtime.onStart) {
const startTime = Date.now();
this.logger().info(`Starting plugin: ${plugin.id}`);
// We don't use `await` when calling onStart because the plugin might be awaiting
// in that call too (for example, when opening a dialog on startup) so we don't
// want to get stuck here.
context.runtime.onStart({}).catch((error) => {
// For some reason, error thrown from the executed script do not have the type "Error"
// but are instead plain object. So recreate the Error object here so that it can
// be handled correctly by loggers, etc.
const newError:Error = new Error(error.message);
newError.stack = error.stack;
this.logger().error(`In plugin ${plugin.id}:`, newError);
}).then(() => {
this.logger().info(`Finished running onStart handler: ${plugin.id} (Took ${Date.now() - startTime}ms)`);
});
}
// vm.createContext(sandbox);
// try {
// vm.runInContext(plugin.scriptText, sandbox);
// } catch (error) {
// this.logger().error(`In plugin ${plugin.id}:`, error);
// return;
// }
// if (!context.runtime) {
// throw new Error(`Plugin ${plugin.id}: The plugin was not registered! Call joplin.plugins.register({.....}) from within the plugin.`);
// }
// if (context.runtime.onStart) {
// const startTime = Date.now();
// this.logger().info(`Starting plugin: ${plugin.id}`);
// // We don't use `await` when calling onStart because the plugin might be awaiting
// // in that call too (for example, when opening a dialog on startup) so we don't
// // want to get stuck here.
// context.runtime.onStart({}).catch((error) => {
// // For some reason, error thrown from the executed script do not have the type "Error"
// // but are instead plain object. So recreate the Error object here so that it can
// // be handled correctly by loggers, etc.
// const newError:Error = new Error(error.message);
// newError.stack = error.stack;
// this.logger().error(`In plugin ${plugin.id}:`, newError);
// }).then(() => {
// this.logger().info(`Finished running onStart handler: ${plugin.id} (Took ${Date.now() - startTime}ms)`);
// });
// }
const pluginApi = new Global(this.logger(), this.platformImplementation_, plugin, this.store_);
return this.runner_.run(plugin, pluginApi);
}
}

View File

@ -1,6 +1,6 @@
import { SandboxContext } from '../utils/types';
import Plugin from '../Plugin';
import Joplin from './Joplin';
import Logger from 'lib/Logger';
const builtinModules = require('builtin-modules');
const shim = require('lib/shim');
@ -16,14 +16,11 @@ function requireWhiteList():string[] {
export default class Global {
private context: SandboxContext;
private joplin_: Joplin;
private consoleWrapper_:any = null;
// private :string[] = null;
constructor(implementation:any, plugin: Plugin, store: any, context: SandboxContext) {
this.context = context;
this.joplin_ = new Joplin(implementation.joplin, plugin, store, this.context);
constructor(logger:Logger, implementation:any, plugin: Plugin, store: any) {
this.joplin_ = new Joplin(logger, implementation.joplin, plugin, store);
this.consoleWrapper_ = this.createConsoleWrapper(plugin.id);
}

View File

@ -1,4 +1,3 @@
import { SandboxContext } from '../utils/types';
import Plugin from '../Plugin';
import JoplinData from './JoplinData';
import JoplinPlugins from './JoplinPlugins';
@ -9,10 +8,11 @@ import JoplinViews from './JoplinViews';
import JoplinUtils from './JoplinUtils';
import JoplinInterop from './JoplinInterop';
import JoplinSettings from './JoplinSettings';
import Logger from 'lib/Logger';
export default class Joplin {
private api_: JoplinData = null;
private data_: JoplinData = null;
private plugins_: JoplinPlugins = null;
private workspace_: JoplinWorkspace = null;
private filters_: JoplinFilters = null;
@ -22,9 +22,9 @@ export default class Joplin {
private interop_: JoplinInterop = null;
private settings_: JoplinSettings = null;
constructor(implementation:any, plugin: Plugin, store: any, context: SandboxContext) {
this.api_ = new JoplinData();
this.plugins_ = new JoplinPlugins(context);
constructor(logger:Logger, implementation:any, plugin: Plugin, store: any) {
this.data_ = new JoplinData();
this.plugins_ = new JoplinPlugins(logger, plugin);
this.workspace_ = new JoplinWorkspace(implementation.workspace, store);
this.filters_ = new JoplinFilters();
this.commands_ = new JoplinCommands();
@ -34,8 +34,8 @@ export default class Joplin {
this.settings_ = new JoplinSettings(plugin);
}
get api(): JoplinData {
return this.api_;
get data(): JoplinData {
return this.data_;
}
get plugins(): JoplinPlugins {

View File

@ -1,13 +1,35 @@
import { SandboxContext } from '../utils/types';
import Plugin from '../Plugin';
import Logger from 'lib/Logger';
export default class JoplinPlugins {
private context: SandboxContext;
private logger: Logger;
private plugin: Plugin;
constructor(context: SandboxContext) {
this.context = context;
constructor(logger:Logger, plugin:Plugin) {
this.logger = logger;
this.plugin = plugin;
}
register(script: any) {
this.context.runtime = script;
if (script.onStart) {
const startTime = Date.now();
this.logger.info(`Starting plugin: ${this.plugin.id}`);
// We don't use `await` when calling onStart because the plugin might be awaiting
// in that call too (for example, when opening a dialog on startup) so we don't
// want to get stuck here.
script.onStart({}).catch((error:any) => {
// For some reason, error thrown from the executed script do not have the type "Error"
// but are instead plain object. So recreate the Error object here so that it can
// be handled correctly by loggers, etc.
const newError:Error = new Error(error.message);
newError.stack = error.stack;
this.logger.error(`In plugin ${this.plugin.id}:`, newError);
}).then(() => {
this.logger.info(`Finished running onStart handler: ${this.plugin.id} (Took ${Date.now() - startTime}ms)`);
});
}
}
}

View File

@ -8,7 +8,7 @@ function createEventHandlers(arg:any, eventHandler:EventHandler) {
arg[i] = createEventHandlers(arg[i], eventHandler);
}
return arg;
} else if (typeof arg === 'string' && arg.indexOf('__event#') === 0) {
} else if (typeof arg === 'string' && arg.indexOf('___plugin_event_') === 0) {
const callbackId = arg;
return async (...args:any[]) => {
const result = await eventHandler(callbackId, args);
@ -36,6 +36,7 @@ export default async function executeSandboxCall(pluginId:string, sandbox:Global
for (const pathFragment of pathFragments) {
parent = fn;
fn = fn[pathFragment];
if (!fn) throw new Error(`Invalid method call: ${path}`);
}
const convertedArgs = createEventHandlers(args, eventHandler);

View File

@ -0,0 +1,29 @@
let eventHandlerIndex_ = 1;
export interface EventHandlers {
[key:string]: Function;
}
export default function mapEventHandlersToIds(arg:any, eventHandlers:EventHandlers) {
if (Array.isArray(arg)) {
for (let i = 0; i < arg.length; i++) {
arg[i] = mapEventHandlersToIds(arg[i], eventHandlers);
}
return arg;
} else if (typeof arg === 'function') {
const id = `___plugin_event_${eventHandlerIndex_}`;
eventHandlerIndex_++;
eventHandlers[id] = arg;
return id;
} else if (arg === null) {
return null;
} else if (arg === undefined) {
return undefined;
} else if (typeof arg === 'object') {
for (const n in arg) {
arg[n] = mapEventHandlersToIds(arg[n], eventHandlers);
}
}
return arg;
}

View File

@ -10,7 +10,7 @@ const Setting = require('lib/models/Setting').default;
const htmlUtils = require('lib/htmlUtils');
const markupLanguageUtils = require('lib/markupLanguageUtils');
const mimeUtils = require('lib/mime-utils.js').mime;
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const md5 = require('md5');
const shim = require('lib/shim');
const HtmlToMd = require('lib/HtmlToMd');

View File

@ -1,4 +1,4 @@
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const ItemChange = require('lib/models/ItemChange.js');
const Setting = require('lib/models/Setting').default;
const Note = require('lib/models/Note.js');

View File

@ -67,7 +67,7 @@ function shimInit() {
shim.showMessageBox = (message, options = null) => {
if (shim.isElectron()) {
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
return bridge().showMessageBox(message, options);
} else {
throw new Error('Not implemented');
@ -402,7 +402,7 @@ function shimInit() {
shim.Buffer = Buffer;
shim.openUrl = url => {
const { bridge } = require('electron').remote.require('./bridge');
const bridge = require('electron').remote.require('./bridge').default;
// Returns true if it opens the file successfully; returns false if it could
// not find the file.
return bridge().openExternal(url);

View File

@ -9,7 +9,7 @@ const MasterKey = require('lib/models/MasterKey.js');
const BaseModel = require('lib/BaseModel.js');
const { sprintf } = require('sprintf-js');
const { time } = require('lib/time-utils.js');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const { _ } = require('lib/locale.js');
const shim = require('lib/shim');
// const { filename, fileExtension } = require('lib/path-utils');

View File

@ -16,7 +16,7 @@ const { shimInit } = require('lib/shim-init-react.js');
const shim = require('lib/shim');
const { time } = require('lib/time-utils.js');
const { AppNav } = require('lib/components/app-nav.js');
const { Logger } = require('lib/logger.js');
const Logger = require('lib/Logger').default;
const Note = require('lib/models/Note.js');
const Folder = require('lib/models/Folder.js');
const BaseSyncTarget = require('lib/BaseSyncTarget.js');

View File

@ -637,7 +637,13 @@
"ReactNativeClient/lib/services/plugins/api/JoplinViews.js": true,
"ReactNativeClient/lib/services/plugins/api/JoplinWorkspace.js": true,
"ElectronClient/services/plugins/PlatformImplementation.js": true,
"ElectronClient/gui/NoteEditor/utils/usePluginServiceRegistration.js": true
"ElectronClient/gui/NoteEditor/utils/usePluginServiceRegistration.js": true,
"ElectronClient/bridge.js": true,
"ElectronClient/ElectronAppWrapper.js": true,
"ElectronClient/services/bridge.js": true,
"ElectronClient/services/plugins/PluginRunner.js": true,
"ReactNativeClient/lib/Logger.js": true,
"ReactNativeClient/lib/services/plugins/utils/mapEventHandlersToIds.js": true
},
"spellright.language": [
"en"

View File

@ -8,6 +8,7 @@
"watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.dev.json",
"build": "gulp build",
"updateIgnored": "gulp updateIgnoredTypeScriptBuild",
"copyLib": "gulp copyLib",
"buildPluginDoc": "typedoc --name 'Joplin Plugin Documentation' --mode file -theme 'Modules/PluginDocTheme/' --readme 'Modules/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out docs/plugin/api/ ReactNativeClient/lib",
"setupNewRelease": "node ./Tools/setupNewRelease",
"postinstall": "cd Tools && npm i && cd .. && cd ReactNativeClient && npm i && cd .. && cd ElectronClient && npm i && cd .. && cd CliClient && npm i && cd .. && gulp build"

View File

@ -28,4 +28,76 @@ The plugin runner also initialises the sandbox proxy and injects it into the plu
### Plugin API
The plugin API is a light wrapper over Joplin's internal functions and services. All the platforms share some of the plugin API but there can also be some differences. For example, the desktop app exposes the text editor component commands, and so this part of the plugin API is available only on desktop. The difference between platforms is implemented using the PlatformImplementation class, which is injected in the plugin service on startup.
The plugin API is a light wrapper over Joplin's internal functions and services. All the platforms share some of the plugin API but there can also be some differences. For example, the desktop app exposes the text editor component commands, and so this part of the plugin API is available only on desktop. The difference between platforms is implemented using the PlatformImplementation class, which is injected in the plugin service on startup.
## Handling events between the plugin and the host
Handling events in plugins is relatively complicated due to the need to send IPC messages and the limitations of the IPC protocol, which in particular cannot transfer functions.
For example, let's say we define a command in the plugin:
```typescript
joplin.commands.register({
name: 'testCommand1',
label: 'My Test Command 1',
}, {
onExecute: (args:any) => {
alert('Testing plugin command 1');
},
});
```
The "onExecute" event handler needs to be called whenever, for example, a toolbar button associated with this command is clicked. The problem is that it is not possible to send a function via IPC (which can only transfer plain objects), so there has to be a translation layer in between.
The way it is done in Joplin is like so:
In the **sandbox proxy**, the event handlers are converted to string event IDs and the original event handler is stored in a map before being sent to host via IPC. So in the example above, the command would be converted to this plain object:
```typescript
{
name: 'testCommand1',
label: 'My Test Command 1',
}, {
onExecute: '___event_handler_123',
}
```
Then, still in the sandbox proxy, we'll have a map called something like `eventHandlers`, which now will have this content:
```typescript
eventHandlers['___event_handler_123'] = (args:any) => {
alert('Testing plugin command 1');
}
```
In the **plugin runner** (Host side), all the event IDs are converted to functions again, but instead of performing the action directly, it posts an IPC message back to the sandbox proxy using the provided event ID.
So in the host, the command will now look like this:
```typescript
{
name: 'testCommand1',
label: 'My Test Command 1',
}, {
onExecute: (args:any) => {
postMessage('pluginMessage', { eventId: '___event_handler_123', args: args });
};
}
```
At this point, any code in the Joplin application can call the `onExecute` function as normal without having to know about the IPC translation layer.
When the function onExecute is eventually called, the IPC message is sent back to the sandbox proxy, which will decode it and execute it.
So on the **sandbox proxy**, we'll have something like this:
```typescript
window.addEventListener('message', ((event) => {
const eventId = getEventId(event); // Get back the event ID (implementation might be different)
const eventArgs = getEventArgs(event); // Get back the args (implementation might be different)
if (eventId) {
// And call the event handler
eventHandlers[eventId](...eventArgs);
}
}));
```