You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-16 00:14:34 +02:00
Desktop: Resolves #7934: Don't create an extra copy of default plugins (load directly from the app bundle) (#9508)
This commit is contained in:
@ -1,11 +1,9 @@
|
|||||||
import { installDefaultPlugins, getDefaultPluginsInstallState, setSettingsForDefaultPlugins, checkPreInstalledDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
|
import { afterDefaultPluginsLoaded, getDefaultPluginPathsAndSettings } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
|
||||||
import PluginRunner from '../../../app/services/plugins/PluginRunner';
|
import PluginRunner from '../../../app/services/plugins/PluginRunner';
|
||||||
import { pathExists } from 'fs-extra';
|
|
||||||
import { checkThrow, setupDatabaseAndSynchronizer, supportDir, switchClient } from '@joplin/lib/testing/test-utils';
|
import { checkThrow, setupDatabaseAndSynchronizer, supportDir, switchClient } from '@joplin/lib/testing/test-utils';
|
||||||
import PluginService, { defaultPluginSetting, DefaultPluginsInfo, PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
import PluginService, { defaultPluginSetting, DefaultPluginsInfo } from '@joplin/lib/services/plugins/PluginService';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
|
||||||
const testPluginDir = `${supportDir}/plugins`;
|
|
||||||
|
|
||||||
function newPluginService(appVersion = '1.4') {
|
function newPluginService(appVersion = '1.4') {
|
||||||
const runner = new PluginRunner();
|
const runner = new PluginRunner();
|
||||||
@ -27,13 +25,17 @@ function newPluginService(appVersion = '1.4') {
|
|||||||
describe('defaultPluginsUtils', () => {
|
describe('defaultPluginsUtils', () => {
|
||||||
|
|
||||||
const pluginsId = ['joplin.plugin.ambrt.backlinksToNote', 'org.joplinapp.plugins.ToggleSidebars'];
|
const pluginsId = ['joplin.plugin.ambrt.backlinksToNote', 'org.joplinapp.plugins.ToggleSidebars'];
|
||||||
|
const defaultPluginsInfo = {
|
||||||
|
'joplin.plugin.ambrt.backlinksToNote': {},
|
||||||
|
'org.joplinapp.plugins.ToggleSidebars': {},
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await setupDatabaseAndSynchronizer(1);
|
await setupDatabaseAndSynchronizer(1);
|
||||||
await switchClient(1);
|
await switchClient(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should install default plugins with no previous default plugins installed', (async () => {
|
it('should load default plugins when nor previously installed', (async () => {
|
||||||
const testPluginDir = `${supportDir}/pluginRepo/plugins`;
|
const testPluginDir = `${supportDir}/pluginRepo/plugins`;
|
||||||
Setting.setValue('installedDefaultPlugins', []);
|
Setting.setValue('installedDefaultPlugins', []);
|
||||||
|
|
||||||
@ -41,151 +43,69 @@ describe('defaultPluginsUtils', () => {
|
|||||||
|
|
||||||
const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
|
const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
|
||||||
|
|
||||||
const newPluginsSettings = await installDefaultPlugins(service, testPluginDir, pluginsId, pluginSettings);
|
for (const pluginId of pluginsId) {
|
||||||
|
expect(pluginSettings[pluginId]).toBeFalsy();
|
||||||
|
}
|
||||||
|
|
||||||
const installedPluginPath1 = `${Setting.value('pluginDir')}/${pluginsId[0]}.jpl`;
|
const pluginPathsAndNewSettings = await getDefaultPluginPathsAndSettings(testPluginDir, defaultPluginsInfo, pluginSettings);
|
||||||
const installedPluginPath2 = `${Setting.value('pluginDir')}/${pluginsId[1]}.jpl`;
|
|
||||||
|
|
||||||
expect(await pathExists(installedPluginPath1)).toBe(true);
|
|
||||||
expect(await pathExists(installedPluginPath2)).toBe(true);
|
|
||||||
|
|
||||||
expect(newPluginsSettings[pluginsId[0]]).toMatchObject(defaultPluginSetting());
|
|
||||||
expect(newPluginsSettings[pluginsId[1]]).toMatchObject(defaultPluginSetting());
|
|
||||||
|
|
||||||
|
for (const pluginId of pluginsId) {
|
||||||
|
expect(
|
||||||
|
pluginPathsAndNewSettings.pluginSettings[pluginId],
|
||||||
|
).toMatchObject(defaultPluginSetting());
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should install default plugins with previous default plugins installed', (async () => {
|
it('should keep already created default plugins disabled with previous default plugins installed', (async () => {
|
||||||
|
|
||||||
const testPluginDir = `${supportDir}/pluginRepo/plugins`;
|
const testPluginDir = `${supportDir}/pluginRepo/plugins`;
|
||||||
Setting.setValue('installedDefaultPlugins', ['org.joplinapp.plugins.ToggleSidebars']);
|
Setting.setValue('installedDefaultPlugins', ['org.joplinapp.plugins.ToggleSidebars']);
|
||||||
|
Setting.setValue('plugins.states', {
|
||||||
|
'org.joplinapp.plugins.ToggleSidebars': { ...defaultPluginSetting(), enabled: false },
|
||||||
|
});
|
||||||
|
|
||||||
const service = newPluginService('2.1');
|
const service = newPluginService('2.1');
|
||||||
|
|
||||||
const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
|
const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
|
||||||
|
const pluginPathsAndNewSettings = await getDefaultPluginPathsAndSettings(testPluginDir, defaultPluginsInfo, pluginSettings);
|
||||||
|
|
||||||
const newPluginsSettings = await installDefaultPlugins(service, testPluginDir, pluginsId, pluginSettings);
|
// Should still be disabled
|
||||||
|
expect(
|
||||||
const installedPluginPath1 = `${Setting.value('pluginDir')}/${pluginsId[0]}.jpl`;
|
pluginPathsAndNewSettings.pluginSettings['org.joplinapp.plugins.ToggleSidebars'].enabled,
|
||||||
const installedPluginPath2 = `${Setting.value('pluginDir')}/${pluginsId[1]}.jpl`;
|
).toBe(false);
|
||||||
|
|
||||||
expect(await pathExists(installedPluginPath1)).toBe(true);
|
|
||||||
expect(await pathExists(installedPluginPath2)).toBe(false);
|
|
||||||
|
|
||||||
expect(newPluginsSettings[pluginsId[0]]).toMatchObject(defaultPluginSetting());
|
|
||||||
expect(newPluginsSettings[pluginsId[1]]).toBeUndefined();
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should get default plugins install state', (async () => {
|
const sampleJsBundlePlugin = `
|
||||||
const testCases = [
|
/* joplin-manifest:
|
||||||
{
|
{
|
||||||
'installedDefaultPlugins': [''],
|
"id": "io.github.jackgruber.backup",
|
||||||
'loadingPlugins': [`${testPluginDir}/simple`, `${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`],
|
"manifest_version": 1,
|
||||||
'plugin1DefaultState': defaultPluginSetting(),
|
"app_min_version": "1.4",
|
||||||
'plugin2DefaultState': defaultPluginSetting(),
|
"name": "JS Bundle test",
|
||||||
'installedDefaultPlugins1': true,
|
"version": "1.0.0"
|
||||||
'installedDefaultPlugins2': true,
|
}
|
||||||
},
|
*/
|
||||||
{
|
joplin.plugins.register({
|
||||||
'installedDefaultPlugins': [''],
|
onStart: async function() {
|
||||||
'loadingPlugins': [`${testPluginDir}/simple`],
|
await joplin.settings.registerSettings({
|
||||||
'plugin1DefaultState': defaultPluginSetting(),
|
path: {
|
||||||
'plugin2DefaultState': undefined,
|
value: "initial-path",
|
||||||
'installedDefaultPlugins1': true,
|
type: 2,
|
||||||
'installedDefaultPlugins2': false,
|
section: "backupSection",
|
||||||
},
|
public: true,
|
||||||
{
|
label: "Backup path",
|
||||||
'installedDefaultPlugins': ['org.joplinapp.plugins.Simple'],
|
},
|
||||||
'loadingPlugins': [`${testPluginDir}/simple`, `${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`],
|
})
|
||||||
'plugin1DefaultState': undefined,
|
},
|
||||||
'plugin2DefaultState': defaultPluginSetting(),
|
});`;
|
||||||
'installedDefaultPlugins1': true,
|
|
||||||
'installedDefaultPlugins2': true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'installedDefaultPlugins': ['org.joplinapp.plugins.Simple'],
|
|
||||||
'loadingPlugins': [`${testPluginDir}/simple`],
|
|
||||||
'plugin1DefaultState': undefined,
|
|
||||||
'plugin2DefaultState': undefined,
|
|
||||||
'installedDefaultPlugins1': true,
|
|
||||||
'installedDefaultPlugins2': false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const testCase of testCases) {
|
|
||||||
const service = newPluginService();
|
|
||||||
const pluginsId = ['org.joplinapp.plugins.Simple', 'org.joplinapp.FirstJplPlugin'];
|
|
||||||
|
|
||||||
Setting.setValue('installedDefaultPlugins', testCase.installedDefaultPlugins);
|
|
||||||
await service.loadAndRunPlugins(testCase.loadingPlugins, {});
|
|
||||||
|
|
||||||
// setting installedDefaultPlugins state
|
|
||||||
const defaultInstallStates: PluginSettings = getDefaultPluginsInstallState(service, pluginsId);
|
|
||||||
|
|
||||||
expect(defaultInstallStates[pluginsId[0]]).toStrictEqual(testCase.plugin1DefaultState);
|
|
||||||
expect(defaultInstallStates[pluginsId[1]]).toStrictEqual(testCase.plugin2DefaultState);
|
|
||||||
|
|
||||||
|
|
||||||
const installedDefaultPlugins = Setting.value('installedDefaultPlugins');
|
|
||||||
expect(installedDefaultPlugins.includes(pluginsId[0])).toBe(testCase.installedDefaultPlugins1);
|
|
||||||
expect(installedDefaultPlugins.includes(pluginsId[1])).toBe(testCase.installedDefaultPlugins2);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should check pre-installed default plugins', (async () => {
|
|
||||||
// with previous pre-installed default plugins
|
|
||||||
Setting.setValue('installedDefaultPlugins', ['']);
|
|
||||||
let pluginSettings, installedDefaultPlugins;
|
|
||||||
|
|
||||||
pluginSettings = { [pluginsId[0]]: defaultPluginSetting() };
|
|
||||||
checkPreInstalledDefaultPlugins(pluginsId, pluginSettings);
|
|
||||||
|
|
||||||
installedDefaultPlugins = Setting.value('installedDefaultPlugins');
|
|
||||||
expect(installedDefaultPlugins.includes(pluginsId[0])).toBe(true);
|
|
||||||
expect(installedDefaultPlugins.includes(pluginsId[1])).toBe(false);
|
|
||||||
|
|
||||||
|
|
||||||
// with no previous pre-installed default plugins
|
|
||||||
Setting.setValue('installedDefaultPlugins', ['not-a-default-plugin']);
|
|
||||||
pluginSettings = {};
|
|
||||||
checkPreInstalledDefaultPlugins(pluginsId, pluginSettings);
|
|
||||||
|
|
||||||
installedDefaultPlugins = Setting.value('installedDefaultPlugins');
|
|
||||||
expect(installedDefaultPlugins.includes(pluginsId[0])).toBe(false);
|
|
||||||
expect(installedDefaultPlugins.includes(pluginsId[1])).toBe(false);
|
|
||||||
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should set initial settings for default plugins', async () => {
|
it('should set initial settings for default plugins', async () => {
|
||||||
const service = newPluginService();
|
const service = newPluginService();
|
||||||
|
|
||||||
const pluginScript = `
|
|
||||||
/* joplin-manifest:
|
|
||||||
{
|
|
||||||
"id": "io.github.jackgruber.backup",
|
|
||||||
"manifest_version": 1,
|
|
||||||
"app_min_version": "1.4",
|
|
||||||
"name": "JS Bundle test",
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
joplin.plugins.register({
|
|
||||||
onStart: async function() {
|
|
||||||
await joplin.settings.registerSettings({
|
|
||||||
path: {
|
|
||||||
value: "initial-path",
|
|
||||||
type: 2,
|
|
||||||
section: "backupSection",
|
|
||||||
public: true,
|
|
||||||
label: "Backup path",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
});`;
|
|
||||||
|
|
||||||
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
|
const plugin = await service.loadPluginFromJsBundle('', sampleJsBundlePlugin);
|
||||||
|
plugin.builtIn = true;
|
||||||
await service.runPlugin(plugin);
|
await service.runPlugin(plugin);
|
||||||
|
const runningPlugins = { 'io.github.jackgruber.backup': plugin };
|
||||||
|
|
||||||
const defaultPluginsInfo: DefaultPluginsInfo = {
|
const defaultPluginsInfo: DefaultPluginsInfo = {
|
||||||
'io.github.jackgruber.backup': {
|
'io.github.jackgruber.backup': {
|
||||||
@ -199,46 +119,64 @@ describe('defaultPluginsUtils', () => {
|
|||||||
|
|
||||||
// with pre-installed default plugin
|
// with pre-installed default plugin
|
||||||
Setting.setValue('installedDefaultPlugins', ['io.github.jackgruber.backup']);
|
Setting.setValue('installedDefaultPlugins', ['io.github.jackgruber.backup']);
|
||||||
setSettingsForDefaultPlugins(defaultPluginsInfo);
|
const pluginSettings = { 'io.github.jackgruber.backup': defaultPluginSetting() };
|
||||||
|
|
||||||
|
await afterDefaultPluginsLoaded(
|
||||||
|
runningPlugins,
|
||||||
|
defaultPluginsInfo,
|
||||||
|
pluginSettings,
|
||||||
|
);
|
||||||
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe('initial-path');
|
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe('initial-path');
|
||||||
await service.destroy();
|
|
||||||
|
|
||||||
// with no pre-installed default plugin
|
// with no pre-installed default plugin
|
||||||
Setting.setValue('installedDefaultPlugins', ['']);
|
Setting.setValue('installedDefaultPlugins', ['']);
|
||||||
setSettingsForDefaultPlugins(defaultPluginsInfo);
|
await afterDefaultPluginsLoaded(
|
||||||
|
runningPlugins,
|
||||||
|
defaultPluginsInfo,
|
||||||
|
pluginSettings,
|
||||||
|
);
|
||||||
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe(`${Setting.value('profileDir')}`);
|
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe(`${Setting.value('profileDir')}`);
|
||||||
await service.destroy();
|
await service.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not overwrite existing settings for a user-installed version of a built-in plugin', async () => {
|
||||||
|
const service = newPluginService();
|
||||||
|
|
||||||
|
const plugin = await service.loadPluginFromJsBundle('', sampleJsBundlePlugin);
|
||||||
|
plugin.builtIn = false;
|
||||||
|
await service.runPlugin(plugin);
|
||||||
|
|
||||||
|
const defaultPluginsInfo: DefaultPluginsInfo = {
|
||||||
|
'io.github.jackgruber.backup': {
|
||||||
|
settings: {
|
||||||
|
'path': 'overwrite?',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// No pre-installed default plugins
|
||||||
|
Setting.setValue('installedDefaultPlugins', []);
|
||||||
|
|
||||||
|
// The plugin is running and enabled
|
||||||
|
const runningPlugins = { 'io.github.jackgruber.backup': plugin };
|
||||||
|
const pluginSettings = { 'io.github.jackgruber.backup': defaultPluginSetting() };
|
||||||
|
|
||||||
|
await afterDefaultPluginsLoaded(
|
||||||
|
runningPlugins,
|
||||||
|
defaultPluginsInfo,
|
||||||
|
pluginSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should not overwrite
|
||||||
|
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe('initial-path');
|
||||||
|
});
|
||||||
|
|
||||||
it('should not throw error on missing setting key', async () => {
|
it('should not throw error on missing setting key', async () => {
|
||||||
|
|
||||||
const service = newPluginService();
|
const service = newPluginService();
|
||||||
|
|
||||||
const pluginScript = `
|
const plugin = await service.loadPluginFromJsBundle('', sampleJsBundlePlugin);
|
||||||
/* joplin-manifest:
|
plugin.builtIn = true;
|
||||||
{
|
|
||||||
"id": "io.github.jackgruber.backup",
|
|
||||||
"manifest_version": 1,
|
|
||||||
"app_min_version": "1.4",
|
|
||||||
"name": "JS Bundle test",
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
joplin.plugins.register({
|
|
||||||
onStart: async function() {
|
|
||||||
await joplin.settings.registerSettings({
|
|
||||||
path: {
|
|
||||||
value: "initial-path",
|
|
||||||
type: 2,
|
|
||||||
section: "backupSection",
|
|
||||||
public: true,
|
|
||||||
label: "Backup path",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
});`;
|
|
||||||
|
|
||||||
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
|
|
||||||
await service.runPlugin(plugin);
|
await service.runPlugin(plugin);
|
||||||
|
|
||||||
const defaultPluginsInfo: DefaultPluginsInfo = {
|
const defaultPluginsInfo: DefaultPluginsInfo = {
|
||||||
@ -256,7 +194,10 @@ describe('defaultPluginsUtils', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Setting.setValue('installedDefaultPlugins', ['']);
|
Setting.setValue('installedDefaultPlugins', ['']);
|
||||||
expect(checkThrow(() => setSettingsForDefaultPlugins(defaultPluginsInfo))).toBe(false);
|
const pluginSettings = { 'io.github.jackgruber.backup': defaultPluginSetting() };
|
||||||
|
const runningPlugins = { 'io.github.jackgruber.backup': plugin };
|
||||||
|
|
||||||
|
expect(checkThrow(() => afterDefaultPluginsLoaded(runningPlugins, defaultPluginsInfo, pluginSettings))).toBe(false);
|
||||||
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe(`${Setting.value('profileDir')}`);
|
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe(`${Setting.value('profileDir')}`);
|
||||||
await service.destroy();
|
await service.destroy();
|
||||||
});
|
});
|
||||||
|
@ -65,7 +65,7 @@ import { AppState } from './app.reducer';
|
|||||||
import syncDebugLog from '@joplin/lib/services/synchronizer/syncDebugLog';
|
import syncDebugLog from '@joplin/lib/services/synchronizer/syncDebugLog';
|
||||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||||
import path = require('path');
|
import path = require('path');
|
||||||
import { checkPreInstalledDefaultPlugins, installDefaultPlugins, setSettingsForDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
|
import { afterDefaultPluginsLoaded, loadAndRunDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
|
||||||
import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher';
|
import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher';
|
||||||
import { parseNotesParent } from '@joplin/lib/reducer';
|
import { parseNotesParent } from '@joplin/lib/reducer';
|
||||||
import OcrService from '@joplin/lib/services/ocr/OcrService';
|
import OcrService from '@joplin/lib/services/ocr/OcrService';
|
||||||
@ -277,7 +277,6 @@ class Application extends BaseApplication {
|
|||||||
const pluginRunner = new PluginRunner();
|
const pluginRunner = new PluginRunner();
|
||||||
service.initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store());
|
service.initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store());
|
||||||
service.isSafeMode = Setting.value('isSafeMode');
|
service.isSafeMode = Setting.value('isSafeMode');
|
||||||
const defaultPluginsId = Object.keys(getDefaultPluginsInfo());
|
|
||||||
|
|
||||||
let pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
|
let pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
|
||||||
{
|
{
|
||||||
@ -285,15 +284,11 @@ class Application extends BaseApplication {
|
|||||||
// time, however we only effectively uninstall the plugin the next
|
// time, however we only effectively uninstall the plugin the next
|
||||||
// time the app is started. What plugin should be uninstalled is
|
// time the app is started. What plugin should be uninstalled is
|
||||||
// stored in the settings.
|
// stored in the settings.
|
||||||
const newSettings = service.clearUpdateState(await service.uninstallPlugins(pluginSettings));
|
pluginSettings = service.clearUpdateState(await service.uninstallPlugins(pluginSettings));
|
||||||
Setting.setValue('plugins.states', newSettings);
|
Setting.setValue('plugins.states', pluginSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkPreInstalledDefaultPlugins(defaultPluginsId, pluginSettings);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const defaultPluginsDir = path.join(bridge().buildDir(), 'defaultPlugins');
|
|
||||||
pluginSettings = await installDefaultPlugins(service, defaultPluginsDir, defaultPluginsId, pluginSettings);
|
|
||||||
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) {
|
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) {
|
||||||
await service.loadAndRunPlugins(Setting.value('pluginDir'), pluginSettings);
|
await service.loadAndRunPlugins(Setting.value('pluginDir'), pluginSettings);
|
||||||
}
|
}
|
||||||
@ -302,19 +297,31 @@ class Application extends BaseApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const devPluginOptions = { devMode: true, builtIn: false };
|
||||||
|
|
||||||
if (Setting.value('plugins.devPluginPaths')) {
|
if (Setting.value('plugins.devPluginPaths')) {
|
||||||
const paths = Setting.value('plugins.devPluginPaths').split(',').map((p: string) => p.trim());
|
const paths = Setting.value('plugins.devPluginPaths').split(',').map((p: string) => p.trim());
|
||||||
await service.loadAndRunPlugins(paths, pluginSettings, true);
|
await service.loadAndRunPlugins(paths, pluginSettings, devPluginOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also load dev plugins that have passed via command line arguments
|
// Also load dev plugins that have passed via command line arguments
|
||||||
if (Setting.value('startupDevPlugins')) {
|
if (Setting.value('startupDevPlugins')) {
|
||||||
await service.loadAndRunPlugins(Setting.value('startupDevPlugins'), pluginSettings, true);
|
await service.loadAndRunPlugins(Setting.value('startupDevPlugins'), pluginSettings, devPluginOptions);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error);
|
this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load default plugins after loading other plugins -- this allows users
|
||||||
|
// to override built-in plugins with development versions with the same
|
||||||
|
// ID.
|
||||||
|
const defaultPluginsDir = path.join(bridge().buildDir(), 'defaultPlugins');
|
||||||
|
try {
|
||||||
|
pluginSettings = await loadAndRunDefaultPlugins(service, defaultPluginsDir, getDefaultPluginsInfo(), pluginSettings);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger().error(`There was an error loading plugins from ${defaultPluginsDir}:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Users can potentially delete files from /plugins or even delete
|
// Users can potentially delete files from /plugins or even delete
|
||||||
// the complete folder. When that happens, we still have the plugin
|
// the complete folder. When that happens, we still have the plugin
|
||||||
@ -322,7 +329,7 @@ class Application extends BaseApplication {
|
|||||||
// out we remove from the state any plugin that has *not* been loaded
|
// out we remove from the state any plugin that has *not* been loaded
|
||||||
// above (meaning the file was missing).
|
// above (meaning the file was missing).
|
||||||
// https://github.com/laurent22/joplin/issues/5253
|
// https://github.com/laurent22/joplin/issues/5253
|
||||||
const oldSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
|
const oldSettings = pluginSettings;
|
||||||
const newSettings: PluginSettings = {};
|
const newSettings: PluginSettings = {};
|
||||||
for (const pluginId of Object.keys(oldSettings)) {
|
for (const pluginId of Object.keys(oldSettings)) {
|
||||||
if (!service.pluginIds.includes(pluginId)) {
|
if (!service.pluginIds.includes(pluginId)) {
|
||||||
@ -332,6 +339,7 @@ class Application extends BaseApplication {
|
|||||||
newSettings[pluginId] = oldSettings[pluginId];
|
newSettings[pluginId] = oldSettings[pluginId];
|
||||||
}
|
}
|
||||||
Setting.setValue('plugins.states', newSettings);
|
Setting.setValue('plugins.states', newSettings);
|
||||||
|
pluginSettings = newSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkAllPluginStartedIID_ = setInterval(() => {
|
this.checkAllPluginStartedIID_ = setInterval(() => {
|
||||||
@ -346,7 +354,7 @@ class Application extends BaseApplication {
|
|||||||
// tests to wait for plugins to load.
|
// tests to wait for plugins to load.
|
||||||
ipcRenderer.send('startup-plugins-loaded');
|
ipcRenderer.send('startup-plugins-loaded');
|
||||||
|
|
||||||
setSettingsForDefaultPlugins(getDefaultPluginsInfo());
|
void afterDefaultPluginsLoaded(service.plugins, getDefaultPluginsInfo(), pluginSettings);
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,6 @@ import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
|||||||
import * as shared from '@joplin/lib/components/shared/config/config-shared.js';
|
import * as shared from '@joplin/lib/components/shared/config/config-shared.js';
|
||||||
import ClipperConfigScreen from '../ClipperConfigScreen';
|
import ClipperConfigScreen from '../ClipperConfigScreen';
|
||||||
import restart from '../../services/restart';
|
import restart from '../../services/restart';
|
||||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
|
||||||
import { getDefaultPluginsInstallState, updateDefaultPluginsInstallState } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
|
|
||||||
import getDefaultPluginsInfo from '@joplin/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo';
|
|
||||||
import JoplinCloudConfigScreen from '../JoplinCloudConfigScreen';
|
import JoplinCloudConfigScreen from '../JoplinCloudConfigScreen';
|
||||||
import ToggleAdvancedSettingsButton from './controls/ToggleAdvancedSettingsButton';
|
import ToggleAdvancedSettingsButton from './controls/ToggleAdvancedSettingsButton';
|
||||||
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
|
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
|
||||||
@ -74,7 +71,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
|||||||
this.switchSection(this.props.defaultSection);
|
this.switchSection(this.props.defaultSection);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
updateDefaultPluginsInstallState(getDefaultPluginsInstallState(PluginService.instance(), Object.keys(getDefaultPluginsInfo())), this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleSettingButton(key: string) {
|
private async handleSettingButton(key: string) {
|
||||||
|
@ -43,6 +43,7 @@ function manifestToItem(manifest: PluginManifest): PluginItem {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
devMode: false,
|
devMode: false,
|
||||||
|
builtIn: false,
|
||||||
hasBeenUpdated: false,
|
hasBeenUpdated: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -52,6 +53,7 @@ export interface PluginItem {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
deleted: boolean;
|
deleted: boolean;
|
||||||
devMode: boolean;
|
devMode: boolean;
|
||||||
|
builtIn: boolean;
|
||||||
hasBeenUpdated: boolean;
|
hasBeenUpdated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +186,10 @@ export default function(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderDeleteButton() {
|
function renderDeleteButton() {
|
||||||
|
// Built-in plugins can only be disabled
|
||||||
|
if (item.builtIn) return null;
|
||||||
if (!props.onDelete) return null;
|
if (!props.onDelete) return null;
|
||||||
|
|
||||||
return <Button level={ButtonLevel.Secondary} onClick={() => props.onDelete({ item })} title={_('Delete')}/>;
|
return <Button level={ButtonLevel.Secondary} onClick={() => props.onDelete({ item })} title={_('Delete')}/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,8 +226,7 @@ export default function(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderDefaultPluginLabel = () => {
|
const renderDefaultPluginLabel = () => {
|
||||||
// Built-in plugins can only be disabled
|
if (item.builtIn) {
|
||||||
if (item.manifest._built_in) {
|
|
||||||
return (
|
return (
|
||||||
<BoxedLabel>{_('Built in')}</BoxedLabel>
|
<BoxedLabel>{_('Built in')}</BoxedLabel>
|
||||||
);
|
);
|
||||||
|
@ -81,6 +81,7 @@ function usePluginItems(plugins: Plugins, settings: PluginSettings): PluginItem[
|
|||||||
enabled: setting.enabled,
|
enabled: setting.enabled,
|
||||||
deleted: setting.deleted,
|
deleted: setting.deleted,
|
||||||
devMode: plugin.devMode,
|
devMode: plugin.devMode,
|
||||||
|
builtIn: plugin.builtIn,
|
||||||
hasBeenUpdated: setting.hasBeenUpdated,
|
hasBeenUpdated: setting.hasBeenUpdated,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -149,8 +150,8 @@ export default function(props: Props) {
|
|||||||
async function fetchPluginIds() {
|
async function fetchPluginIds() {
|
||||||
// Built-in plugins can't be updated from the main repoApi
|
// Built-in plugins can't be updated from the main repoApi
|
||||||
const nonDefaultPlugins = pluginItems
|
const nonDefaultPlugins = pluginItems
|
||||||
.map(p => p.manifest)
|
.filter(plugin => !plugin.builtIn)
|
||||||
.filter(manifest => !manifest._built_in);
|
.map(p => p.manifest);
|
||||||
|
|
||||||
const pluginIds = await repoApi().canBeUpdatedPlugins(nonDefaultPlugins, pluginService.appVersion);
|
const pluginIds = await repoApi().canBeUpdatedPlugins(nonDefaultPlugins, pluginService.appVersion);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
@ -288,8 +289,8 @@ export default function(props: Props) {
|
|||||||
</UserPluginsRoot>
|
</UserPluginsRoot>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const nonDefaultPlugins = pluginItems.filter(item => !item.manifest._built_in);
|
const nonDefaultPlugins = pluginItems.filter(item => !item.builtIn);
|
||||||
const defaultPlugins = pluginItems.filter(item => item.manifest._built_in);
|
const defaultPlugins = pluginItems.filter(item => item.builtIn);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserPluginsRoot>
|
<UserPluginsRoot>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
import { copy, exists, remove, mkdirp, readdir, mkdtemp, readFile, writeFile } from 'fs-extra';
|
import { copy, exists, remove, mkdirp, readdir, mkdtemp } from 'fs-extra';
|
||||||
import { join, resolve, basename } from 'path';
|
import { join, resolve, basename } from 'path';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import { chdir, cwd } from 'process';
|
import { chdir, cwd } from 'process';
|
||||||
@ -62,12 +62,6 @@ const buildDefaultPlugins = async (outputParentDir: string|null, beforeInstall:
|
|||||||
logStatus('Initializing repository.');
|
logStatus('Initializing repository.');
|
||||||
await execCommand('git init . -b main');
|
await execCommand('git init . -b main');
|
||||||
|
|
||||||
logStatus('Marking manifest as built-in');
|
|
||||||
const manifestFile = './src/manifest.json';
|
|
||||||
const manifest = JSON.parse(await readFile(manifestFile, 'utf8'));
|
|
||||||
manifest._built_in = true;
|
|
||||||
await writeFile(manifestFile, JSON.stringify(manifest, undefined, '\t'));
|
|
||||||
|
|
||||||
logStatus('Creating initial commit.');
|
logStatus('Creating initial commit.');
|
||||||
await execCommand('git add .');
|
await execCommand('git add .');
|
||||||
await execCommand(['git', 'config', 'user.name', 'Build script']);
|
await execCommand(['git', 'config', 'user.name', 'Build script']);
|
||||||
|
@ -32,6 +32,7 @@ export default class Plugin {
|
|||||||
private dispatch_: Function;
|
private dispatch_: Function;
|
||||||
private eventEmitter_: any;
|
private eventEmitter_: any;
|
||||||
private devMode_ = false;
|
private devMode_ = false;
|
||||||
|
private builtIn_ = false;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
private messageListener_: Function = null;
|
private messageListener_: Function = null;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
@ -61,6 +62,14 @@ export default class Plugin {
|
|||||||
this.devMode_ = v;
|
this.devMode_ = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get builtIn(): boolean {
|
||||||
|
return this.builtIn_;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set builtIn(builtIn: boolean) {
|
||||||
|
this.builtIn_ = builtIn;
|
||||||
|
}
|
||||||
|
|
||||||
public get manifest(): PluginManifest {
|
public get manifest(): PluginManifest {
|
||||||
return this.manifest_;
|
return this.manifest_;
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,11 @@ export interface PluginSettings {
|
|||||||
[pluginId: string]: PluginSetting;
|
[pluginId: string]: PluginSetting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PluginLoadOptions {
|
||||||
|
devMode: boolean;
|
||||||
|
builtIn: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
function makePluginId(source: string): string {
|
function makePluginId(source: string): string {
|
||||||
// https://www.npmjs.com/package/slug#options
|
// https://www.npmjs.com/package/slug#options
|
||||||
return uslug(source).substr(0, 32);
|
return uslug(source).substr(0, 32);
|
||||||
@ -340,7 +345,14 @@ export default class PluginService extends BaseService {
|
|||||||
return this.runner_.callStatsSummary(pluginId, duration);
|
return this.runner_.callStatsSummary(pluginId, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async loadAndRunPlugins(pluginDirOrPaths: string | string[], settings: PluginSettings, devMode = false) {
|
public async loadAndRunPlugins(
|
||||||
|
pluginDirOrPaths: string | string[], settings: PluginSettings, options?: PluginLoadOptions,
|
||||||
|
) {
|
||||||
|
options ??= {
|
||||||
|
builtIn: false,
|
||||||
|
devMode: false,
|
||||||
|
};
|
||||||
|
|
||||||
let pluginPaths = [];
|
let pluginPaths = [];
|
||||||
|
|
||||||
if (Array.isArray(pluginDirOrPaths)) {
|
if (Array.isArray(pluginDirOrPaths)) {
|
||||||
@ -370,6 +382,10 @@ export default class PluginService extends BaseService {
|
|||||||
// such folders but to keep things sane we disallow it.
|
// such folders but to keep things sane we disallow it.
|
||||||
if (this.plugins_[plugin.id]) throw new Error(`There is already a plugin with this ID: ${plugin.id}`);
|
if (this.plugins_[plugin.id]) throw new Error(`There is already a plugin with this ID: ${plugin.id}`);
|
||||||
|
|
||||||
|
// We mark the plugin as built-in even if not enabled (being built-in affects
|
||||||
|
// update UI).
|
||||||
|
plugin.builtIn = options.builtIn;
|
||||||
|
|
||||||
this.setPluginAt(plugin.id, plugin);
|
this.setPluginAt(plugin.id, plugin);
|
||||||
|
|
||||||
if (!this.pluginEnabled(settings, plugin.id)) {
|
if (!this.pluginEnabled(settings, plugin.id)) {
|
||||||
@ -377,7 +393,7 @@ export default class PluginService extends BaseService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin.devMode = devMode;
|
plugin.devMode = options.devMode;
|
||||||
|
|
||||||
await this.runPlugin(plugin);
|
await this.runPlugin(plugin);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,90 +1,111 @@
|
|||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import path = require('path');
|
|
||||||
import Setting from '../../../models/Setting';
|
import Setting from '../../../models/Setting';
|
||||||
import shim from '../../../shim';
|
import shim from '../../../shim';
|
||||||
import PluginService, { defaultPluginSetting, DefaultPluginsInfo, PluginSettings } from '../PluginService';
|
import PluginService, { defaultPluginSetting, DefaultPluginsInfo, Plugins, PluginSettings } from '../PluginService';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
import * as React from 'react';
|
import { join } from 'path';
|
||||||
const shared = require('../../../components/shared/config/config-shared.js');
|
|
||||||
|
|
||||||
const logger = Logger.create('defaultPluginsUtils');
|
const logger = Logger.create('defaultPluginsUtils');
|
||||||
|
|
||||||
export function checkPreInstalledDefaultPlugins(defaultPluginsId: string[], pluginSettings: PluginSettings) {
|
|
||||||
const installedDefaultPlugins: string[] = Setting.value('installedDefaultPlugins');
|
|
||||||
for (const pluginId of defaultPluginsId) {
|
|
||||||
// if pluginId is present in pluginSettings and not in installedDefaultPlugins array,
|
|
||||||
// then its either pre-installed by user or just uninstalled
|
|
||||||
if (pluginSettings[pluginId] && !installedDefaultPlugins.includes(pluginId)) Setting.setArrayValue('installedDefaultPlugins', pluginId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function installDefaultPlugins(service: PluginService, defaultPluginsDir: string, defaultPluginsId: string[], pluginSettings: PluginSettings): Promise<PluginSettings> {
|
// Use loadAndRunDefaultPlugins
|
||||||
|
// Exported for testing.
|
||||||
|
export const getDefaultPluginPathsAndSettings = async (
|
||||||
|
defaultPluginsDir: string, defaultPluginsInfo: DefaultPluginsInfo, pluginSettings: PluginSettings,
|
||||||
|
) => {
|
||||||
|
const pluginPaths: string[] = [];
|
||||||
|
|
||||||
if (!await shim.fsDriver().exists(defaultPluginsDir)) {
|
if (!await shim.fsDriver().exists(defaultPluginsDir)) {
|
||||||
logger.info(`Could not find default plugins' directory: ${defaultPluginsDir} - skipping installation.`);
|
logger.info(`Could not find default plugins' directory: ${defaultPluginsDir} - skipping installation.`);
|
||||||
return pluginSettings;
|
return { pluginPaths, pluginSettings };
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultPluginsPaths = await shim.fsDriver().readDirStats(defaultPluginsDir);
|
const defaultPluginsPaths = await shim.fsDriver().readDirStats(defaultPluginsDir);
|
||||||
if (defaultPluginsPaths.length <= 0) {
|
if (defaultPluginsPaths.length <= 0) {
|
||||||
logger.info(`Default plugins' directory is empty: ${defaultPluginsDir} - skipping installation.`);
|
logger.info(`Default plugins' directory is empty: ${defaultPluginsDir} - no default plugins will be installed.`);
|
||||||
return pluginSettings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const installedPlugins = Setting.value('installedDefaultPlugins');
|
|
||||||
|
|
||||||
for (const pluginStat of defaultPluginsPaths) {
|
for (const pluginStat of defaultPluginsPaths) {
|
||||||
const pluginId = pluginStat.path;
|
// Each plugin should be within a folder with the same ID as the plugin
|
||||||
|
const pluginFolderName = pluginStat.path;
|
||||||
|
const pluginId = pluginFolderName;
|
||||||
|
|
||||||
// if pluginId is present in 'installedDefaultPlugins' array or it doesn't have default plugin ID, then we won't install it again as default plugin
|
if (!defaultPluginsInfo.hasOwnProperty(pluginId)) {
|
||||||
if (installedPlugins.includes(pluginId) || !defaultPluginsId.includes(pluginId)) {
|
logger.warn(`Default plugin ${pluginId} is missing in defaultPluginsInfo. Not loading.`);
|
||||||
logger.debug(`Skipping default plugin ${pluginId}, ${!defaultPluginsId.includes(pluginId) ? '(Not a default)' : ''}`);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const defaultPluginPath: string = path.join(defaultPluginsDir, pluginId, 'plugin.jpl');
|
|
||||||
await service.installPlugin(defaultPluginPath, false);
|
pluginPaths.push(join(defaultPluginsDir, pluginFolderName, 'plugin.jpl'));
|
||||||
|
|
||||||
pluginSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
pluginSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
||||||
draft[pluginId] = defaultPluginSetting();
|
// Default plugins can be overridden but not uninstalled (as they're part of
|
||||||
|
// the app bundle). When overriding and unoverriding a default plugin, the plugin's
|
||||||
|
// state may be deleted.
|
||||||
|
// As such, we recreate the plugin state if necessary.
|
||||||
|
if (!draft[pluginId]) {
|
||||||
|
draft[pluginId] = defaultPluginSetting();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { pluginSettings, pluginPaths };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadAndRunDefaultPlugins = async (
|
||||||
|
service: PluginService,
|
||||||
|
defaultPluginsDir: string,
|
||||||
|
defaultPluginsInfo: DefaultPluginsInfo,
|
||||||
|
originalPluginSettings: PluginSettings,
|
||||||
|
): Promise<PluginSettings> => {
|
||||||
|
const { pluginPaths, pluginSettings } = await getDefaultPluginPathsAndSettings(
|
||||||
|
defaultPluginsDir, defaultPluginsInfo, originalPluginSettings,
|
||||||
|
) ?? { pluginPaths: [], pluginSettings: originalPluginSettings };
|
||||||
|
|
||||||
|
await service.loadAndRunPlugins(pluginPaths, pluginSettings, { builtIn: true, devMode: false });
|
||||||
return pluginSettings;
|
return pluginSettings;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function setSettingsForDefaultPlugins(defaultPluginsInfo: DefaultPluginsInfo) {
|
// Applies setting overrides and marks default plugins as installed.
|
||||||
const installedDefaultPlugins = Setting.value('installedDefaultPlugins');
|
// Should be called after plugins have finished loading.
|
||||||
|
export const afterDefaultPluginsLoaded = async (
|
||||||
|
allLoadedPlugins: Plugins,
|
||||||
|
defaultPluginsInfo: DefaultPluginsInfo,
|
||||||
|
pluginSettings: PluginSettings,
|
||||||
|
) => {
|
||||||
|
const installedDefaultPlugins: string[] = Setting.value('installedDefaultPlugins');
|
||||||
|
const allDefaultPlugins = Object.keys(defaultPluginsInfo);
|
||||||
|
|
||||||
// only set initial settings if the plugin is not present in installedDefaultPlugins array
|
const isFirstLoadOfDefaultPlugin = (pluginId: string) => {
|
||||||
for (const pluginId of Object.keys(defaultPluginsInfo)) {
|
// Not installed?
|
||||||
if (!defaultPluginsInfo[pluginId].settings) continue;
|
if (!pluginSettings[pluginId]) {
|
||||||
for (const settingName of Object.keys(defaultPluginsInfo[pluginId].settings)) {
|
return false;
|
||||||
if (!installedDefaultPlugins.includes(pluginId) && Setting.keyExists(`plugin-${pluginId}.${settingName}`)) {
|
|
||||||
Setting.setValue(`plugin-${pluginId}.${settingName}`, defaultPluginsInfo[pluginId].settings[settingName]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDefaultPluginsInstallState(service: PluginService, defaultPluginsId: string[]): PluginSettings {
|
// Not the first load
|
||||||
const settings: PluginSettings = {};
|
if (installedDefaultPlugins.includes(pluginId)) {
|
||||||
for (const pluginId of defaultPluginsId) {
|
return false;
|
||||||
if (!service.pluginIds.includes(pluginId)) continue;
|
|
||||||
if (!Setting.setArrayValue('installedDefaultPlugins', pluginId)) {
|
|
||||||
settings[pluginId] = defaultPluginSetting();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateDefaultPluginsInstallState(newPluginStates: PluginSettings, ConfigScreen: React.Component<any, any>) {
|
// Return true only if the plugin is built-in (and not a user-installed
|
||||||
if (Object.keys(newPluginStates).length === 0) return;
|
// copy).
|
||||||
const key = 'plugins.states';
|
//
|
||||||
const md = Setting.settingMetadata(key);
|
// This avoids overriding existing user-set settings.
|
||||||
let newValue = Setting.value('plugins.states');
|
return allLoadedPlugins[pluginId]?.builtIn ?? false;
|
||||||
newValue = {
|
|
||||||
...newValue, ...newPluginStates,
|
|
||||||
};
|
};
|
||||||
shared.updateSettingValue(ConfigScreen, key, newValue);
|
|
||||||
|
|
||||||
if (md.autoSave) {
|
for (const pluginId of allDefaultPlugins) {
|
||||||
shared.scheduleSaveSettings(ConfigScreen);
|
// if pluginId is present in pluginSettings and not in installedDefaultPlugins array,
|
||||||
|
// then it's a new default plugin and needs overrides applied.
|
||||||
|
if (isFirstLoadOfDefaultPlugin(pluginId)) {
|
||||||
|
// Postprocess: Apply setting overrides
|
||||||
|
for (const settingName of Object.keys(defaultPluginsInfo[pluginId].settings ?? {})) {
|
||||||
|
if (!installedDefaultPlugins.includes(pluginId) && Setting.keyExists(`plugin-${pluginId}.${settingName}`)) {
|
||||||
|
Setting.setValue(`plugin-${pluginId}.${settingName}`, defaultPluginsInfo[pluginId].settings[settingName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the plugin as installed so that postprocessing won't be done again.
|
||||||
|
Setting.setArrayValue('installedDefaultPlugins', pluginId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
@ -64,7 +64,6 @@ export default function manifestFromObject(o: any): PluginManifest {
|
|||||||
icons: getIcons(),
|
icons: getIcons(),
|
||||||
|
|
||||||
_recommended: getBoolean('_recommended', false, false),
|
_recommended: getBoolean('_recommended', false, false),
|
||||||
_built_in: getBoolean('_built_in', false, false),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
validatePluginId(manifest.id);
|
validatePluginId(manifest.id);
|
||||||
|
@ -37,5 +37,4 @@ export interface PluginManifest {
|
|||||||
_npm_package_name?: string;
|
_npm_package_name?: string;
|
||||||
_obsolete?: boolean;
|
_obsolete?: boolean;
|
||||||
_recommended?: boolean;
|
_recommended?: boolean;
|
||||||
_built_in?: boolean;
|
|
||||||
}
|
}
|
||||||
|
@ -13,16 +13,11 @@ const validateUntrustedManifest = (manifest: any, existingManifests: any) => {
|
|||||||
validatePluginId(manifest.id);
|
validatePluginId(manifest.id);
|
||||||
validatePluginVersion(manifest.version);
|
validatePluginVersion(manifest.version);
|
||||||
|
|
||||||
// This prevents a plugin author from marking their own plugin as _recommended
|
// This prevents a plugin author from marking their own plugin as _recommended.
|
||||||
// or _built_in.
|
|
||||||
if (typeof manifest._recommended !== 'undefined') {
|
if (typeof manifest._recommended !== 'undefined') {
|
||||||
throw new Error(`Plugin ${manifest.id} cannot mark itself as recommended.`);
|
throw new Error(`Plugin ${manifest.id} cannot mark itself as recommended.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof manifest._built_in !== 'undefined') {
|
|
||||||
throw new Error(`Plugin ${manifest.id} cannot mark itself as built-in.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
checkIfPluginCanBeAdded(existingManifests, manifest);
|
checkIfPluginCanBeAdded(existingManifests, manifest);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -52,6 +52,7 @@ These are run by the `app-desktop` package on a full `build` (e.g. on `postinsta
|
|||||||
## Installing of default plugins
|
## Installing of default plugins
|
||||||
|
|
||||||
- All the functions related to default plugins are located in [defaultPluginsUtils.ts](https://github.com/laurent22/joplin/blob/eb7083d7888433ff6ef76ccfb7fb87ba951d513f/packages/lib/services/plugins/defaultPlugins/defaultPluginsUtils.ts)
|
- All the functions related to default plugins are located in [defaultPluginsUtils.ts](https://github.com/laurent22/joplin/blob/eb7083d7888433ff6ef76ccfb7fb87ba951d513f/packages/lib/services/plugins/defaultPlugins/defaultPluginsUtils.ts)
|
||||||
- On every startup, we check if there are new plugins available in build folder that have not been installed yet. After installing the new plugin, we update the `installedDefaultPlugins` array in `Setting.ts` with respective plugin ID for future reference.
|
- Default plugins are bundled with the app (included in the `build/` directory) and loaded from this directory.
|
||||||
- After installing is complete, we apply the default settings for each default plugin. Default settings are located in [desktopDefaultPluginsInfo.ts](https://github.com/laurent22/joplin/blob/eb7083d7888433ff6ef76ccfb7fb87ba951d513f/packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.ts)
|
- To allow loading `dev` and NPM versions of the plugin, default plugins are loaded after non-default plugins. The plugin service refuses to load additional copies of already-loaded plugins. As such, non-default plugins take precedence over default plugins.
|
||||||
|
- After loading is complete, we apply the default settings for each default plugin. Default settings are located in [desktopDefaultPluginsInfo.ts](https://github.com/laurent22/joplin/blob/eb7083d7888433ff6ef76ccfb7fb87ba951d513f/packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.ts). The `installedDefaultPlugins` setting is used to ensure that settnigs are only overridden once.
|
||||||
- If the plugin is already installed by the user, then we don't apply default settings to avoid overriding user's settings.
|
- If the plugin is already installed by the user, then we don't apply default settings to avoid overriding user's settings.
|
Reference in New Issue
Block a user