You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	| @@ -59,6 +59,8 @@ packages/app-mobile/locales | ||||
| packages/app-mobile/node_modules | ||||
| packages/app-mobile/pluginAssets/ | ||||
| packages/fork-* | ||||
| packages/default-plugins/plugin-base-repo/ | ||||
| packages/default-plugins/plugin-sources/ | ||||
| packages/htmlpack/dist/ | ||||
| packages/lib/assets/ | ||||
| packages/lib/lib/lib.js | ||||
| @@ -380,10 +382,12 @@ packages/app-desktop/gui/style/StyledTextInput.js | ||||
| packages/app-desktop/gui/utils/NoteListUtils.js | ||||
| packages/app-desktop/gui/utils/convertToScreenCoordinates.js | ||||
| packages/app-desktop/gui/utils/loadScript.js | ||||
| packages/app-desktop/gulpfile.js | ||||
| packages/app-desktop/integration-tests/main.spec.js | ||||
| packages/app-desktop/integration-tests/models/MainScreen.js | ||||
| packages/app-desktop/integration-tests/models/NoteEditorScreen.js | ||||
| packages/app-desktop/integration-tests/models/SettingsScreen.js | ||||
| packages/app-desktop/integration-tests/simpleBackup.spec.js | ||||
| packages/app-desktop/integration-tests/util/activateMainMenuItem.js | ||||
| packages/app-desktop/integration-tests/util/createStartupArgs.js | ||||
| packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js | ||||
| @@ -415,6 +419,7 @@ packages/app-desktop/services/sortOrder/PerFolderSortOrderService.js | ||||
| packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js | ||||
| packages/app-desktop/services/sortOrder/notesSortOrderUtils.js | ||||
| packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js | ||||
| packages/app-desktop/tools/copy7Zip.js | ||||
| packages/app-desktop/tools/notarizeMacApp.js | ||||
| packages/app-desktop/tools/renameReleaseAssets.js | ||||
| packages/app-desktop/utils/checkForUpdatesUtils.test.js | ||||
| @@ -530,6 +535,13 @@ packages/app-mobile/utils/fs-driver/runOnDeviceTests.js | ||||
| packages/app-mobile/utils/setupNotifications.js | ||||
| packages/app-mobile/utils/shareHandler.js | ||||
| packages/app-mobile/utils/types.js | ||||
| packages/default-plugins/build.js | ||||
| packages/default-plugins/buildDefaultPlugins.js | ||||
| packages/default-plugins/commands/buildAll.js | ||||
| packages/default-plugins/commands/editPatch.js | ||||
| packages/default-plugins/utils/getPathToPatchFileFor.js | ||||
| packages/default-plugins/utils/readRepositoryJson.js | ||||
| packages/default-plugins/utils/waitForCliInput.js | ||||
| packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js | ||||
| packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js | ||||
| packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.js | ||||
| @@ -1013,8 +1025,6 @@ packages/tools/build-translation.js | ||||
| packages/tools/build-welcome.js | ||||
| packages/tools/buildServerDocker.test.js | ||||
| packages/tools/buildServerDocker.js | ||||
| packages/tools/bundleDefaultPlugins.test.js | ||||
| packages/tools/bundleDefaultPlugins.js | ||||
| packages/tools/checkIgnoredFiles.js | ||||
| packages/tools/checkLibPaths.test.js | ||||
| packages/tools/checkLibPaths.js | ||||
|   | ||||
							
								
								
									
										12
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -362,10 +362,12 @@ packages/app-desktop/gui/style/StyledTextInput.js | ||||
| packages/app-desktop/gui/utils/NoteListUtils.js | ||||
| packages/app-desktop/gui/utils/convertToScreenCoordinates.js | ||||
| packages/app-desktop/gui/utils/loadScript.js | ||||
| packages/app-desktop/gulpfile.js | ||||
| packages/app-desktop/integration-tests/main.spec.js | ||||
| packages/app-desktop/integration-tests/models/MainScreen.js | ||||
| packages/app-desktop/integration-tests/models/NoteEditorScreen.js | ||||
| packages/app-desktop/integration-tests/models/SettingsScreen.js | ||||
| packages/app-desktop/integration-tests/simpleBackup.spec.js | ||||
| packages/app-desktop/integration-tests/util/activateMainMenuItem.js | ||||
| packages/app-desktop/integration-tests/util/createStartupArgs.js | ||||
| packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js | ||||
| @@ -397,6 +399,7 @@ packages/app-desktop/services/sortOrder/PerFolderSortOrderService.js | ||||
| packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js | ||||
| packages/app-desktop/services/sortOrder/notesSortOrderUtils.js | ||||
| packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js | ||||
| packages/app-desktop/tools/copy7Zip.js | ||||
| packages/app-desktop/tools/notarizeMacApp.js | ||||
| packages/app-desktop/tools/renameReleaseAssets.js | ||||
| packages/app-desktop/utils/checkForUpdatesUtils.test.js | ||||
| @@ -512,6 +515,13 @@ packages/app-mobile/utils/fs-driver/runOnDeviceTests.js | ||||
| packages/app-mobile/utils/setupNotifications.js | ||||
| packages/app-mobile/utils/shareHandler.js | ||||
| packages/app-mobile/utils/types.js | ||||
| packages/default-plugins/build.js | ||||
| packages/default-plugins/buildDefaultPlugins.js | ||||
| packages/default-plugins/commands/buildAll.js | ||||
| packages/default-plugins/commands/editPatch.js | ||||
| packages/default-plugins/utils/getPathToPatchFileFor.js | ||||
| packages/default-plugins/utils/readRepositoryJson.js | ||||
| packages/default-plugins/utils/waitForCliInput.js | ||||
| packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.js | ||||
| packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.js | ||||
| packages/editor/CodeMirror/CodeMirror5Emulation/Decorator.js | ||||
| @@ -995,8 +1005,6 @@ packages/tools/build-translation.js | ||||
| packages/tools/build-welcome.js | ||||
| packages/tools/buildServerDocker.test.js | ||||
| packages/tools/buildServerDocker.js | ||||
| packages/tools/bundleDefaultPlugins.test.js | ||||
| packages/tools/bundleDefaultPlugins.js | ||||
| packages/tools/checkIgnoredFiles.js | ||||
| packages/tools/checkLibPaths.test.js | ||||
| packages/tools/checkLibPaths.js | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| packages/app-clipper/popup/ | ||||
| packages/app-cli/tests/support/plugins/ | ||||
| packages/doc-builder/ | ||||
| packages/doc-builder/ | ||||
| packages/default-plugins/plugin-base-repo/ | ||||
| packages/default-plugins/plugin-sources/ | ||||
| @@ -189,13 +189,11 @@ describe('defaultPluginsUtils', () => { | ||||
|  | ||||
| 		const defaultPluginsInfo: DefaultPluginsInfo = { | ||||
| 			'io.github.jackgruber.backup': { | ||||
| 				version: '1.0.2', | ||||
| 				settings: { | ||||
| 					'path': `${Setting.value('profileDir')}`, | ||||
| 				}, | ||||
| 			}, | ||||
| 			'plugin.calebjohn.rich-markdown': { | ||||
| 				version: '0.8.3', | ||||
| 			}, | ||||
| 		}; | ||||
|  | ||||
| @@ -245,14 +243,12 @@ describe('defaultPluginsUtils', () => { | ||||
|  | ||||
| 		const defaultPluginsInfo: DefaultPluginsInfo = { | ||||
| 			'io.github.jackgruber.backup': { | ||||
| 				version: '1.0.2', | ||||
| 				settings: { | ||||
| 					'path': `${Setting.value('profileDir')}`, | ||||
| 					'missing-key1': 'someValue', | ||||
| 				}, | ||||
| 			}, | ||||
| 			'plugin.calebjohn.rich-markdown': { | ||||
| 				version: '0.8.3', | ||||
| 				settings: { | ||||
| 					'missing-key2': 'someValue', | ||||
| 				}, | ||||
|   | ||||
							
								
								
									
										3
									
								
								packages/app-desktop/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								packages/app-desktop/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -18,3 +18,6 @@ test-results/ | ||||
| playwright-report/ | ||||
| playwright/.cache/ | ||||
| integration-tests/test-profile/ | ||||
| build/defaultPlugins/ | ||||
| build/7zip/7za | ||||
| build/7zip/7za.exe | ||||
|   | ||||
| @@ -29,7 +29,7 @@ import { reg } from '@joplin/lib/registry'; | ||||
| const packageInfo: PackageInfo = require('./packageInfo.js'); | ||||
| import DecryptionWorker from '@joplin/lib/services/DecryptionWorker'; | ||||
| import ClipperServer from '@joplin/lib/ClipperServer'; | ||||
| const { webFrame } = require('electron'); | ||||
| import { ipcRenderer, webFrame } from 'electron'; | ||||
| const Menu = bridge().Menu; | ||||
| const PluginManager = require('@joplin/lib/services/PluginManager'); | ||||
| import RevisionService from '@joplin/lib/services/RevisionService'; | ||||
| @@ -333,6 +333,11 @@ class Application extends BaseApplication { | ||||
| 					type: 'STARTUP_PLUGINS_LOADED', | ||||
| 					value: true, | ||||
| 				}); | ||||
|  | ||||
| 				// Sends an event to the main process -- this is used by the Playwright | ||||
| 				// tests to wait for plugins to load. | ||||
| 				ipcRenderer.send('startup-plugins-loaded'); | ||||
|  | ||||
| 				setSettingsForDefaultPlugins(getDefaultPluginsInfo()); | ||||
| 			} | ||||
| 		}, 500); | ||||
|   | ||||
							
								
								
									
										90
									
								
								packages/app-desktop/build/7zip/license.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								packages/app-desktop/build/7zip/license.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| 7-Zip | ||||
| ~~~~~ | ||||
| License for use and distribution | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
|  | ||||
| 7-Zip Copyright (C) 1999-2023 Igor Pavlov. | ||||
|  | ||||
| The licenses for files are: | ||||
|  | ||||
| 1) 7z.dll: | ||||
| 		- The "GNU LGPL" as main license for most of the code | ||||
| 		- The "GNU LGPL" with "unRAR license restriction" for some code | ||||
| 		- The "BSD 3-clause License" for some code | ||||
| 2) All other files: the "GNU LGPL". | ||||
|  | ||||
| Redistributions in binary form must reproduce related license information from this file. | ||||
|  | ||||
| Note: | ||||
| You can use 7-Zip on any computer, including a computer in a commercial | ||||
| organization. You don't need to register or pay for 7-Zip. | ||||
|  | ||||
|  | ||||
| GNU LGPL information | ||||
| -------------------- | ||||
|  | ||||
| This library is free software; you can redistribute it and/or | ||||
| modify it under the terms of the GNU Lesser General Public | ||||
| License as published by the Free Software Foundation; either | ||||
| version 2.1 of the License, or (at your option) any later version. | ||||
|  | ||||
| This library is distributed in the hope that it will be useful, | ||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | ||||
| Lesser General Public License for more details. | ||||
|  | ||||
| You can receive a copy of the GNU Lesser General Public License from | ||||
| http://www.gnu.org/ | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| BSD 3-clause License | ||||
| -------------------- | ||||
|  | ||||
| The "BSD 3-clause License" is used for the code in 7z.dll that implements LZFSE data decompression. | ||||
| That code was derived from the code in the "LZFSE compression library" developed by Apple Inc, | ||||
| that also uses the "BSD 3-clause License": | ||||
|  | ||||
| ---- | ||||
| Copyright (c) 2015-2016, Apple Inc. All rights reserved. | ||||
|  | ||||
| Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||||
|  | ||||
| 1.  Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | ||||
|  | ||||
| 2.  Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer | ||||
| 	in the documentation and/or other materials provided with the distribution. | ||||
|  | ||||
| 3.  Neither the name of the copyright holder(s) nor the names of any contributors may be used to endorse or promote products derived | ||||
| 	from this software without specific prior written permission. | ||||
|  | ||||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||
| LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE | ||||
| COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||||
| (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | ||||
| HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | ||||
| ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
| ---- | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| unRAR license restriction | ||||
| ------------------------- | ||||
|  | ||||
| The decompression engine for RAR archives was developed using source | ||||
| code of unRAR program. | ||||
| All copyrights to original unRAR code are owned by Alexander Roshal. | ||||
|  | ||||
| The license for original unRAR code has the following restriction: | ||||
|  | ||||
| 	The unRAR sources cannot be used to re-create the RAR compression algorithm, | ||||
| 	which is proprietary. Distribution of modified unRAR sources in separate form | ||||
| 	or as a part of other software is permitted, provided that it is clearly | ||||
| 	stated in the documentation and source comments that the code may | ||||
| 	not be used to develop a RAR (WinRAR) compatible archiver. | ||||
|  | ||||
|  | ||||
| -- | ||||
| Igor Pavlov | ||||
| @@ -60,7 +60,7 @@ const CellRoot = styled.div<{ isCompatible: boolean }>` | ||||
| 	box-sizing: border-box; | ||||
| 	background-color: ${props => props.theme.backgroundColor}; | ||||
| 	flex-direction: column; | ||||
| 	align-items: flex-start; | ||||
| 	align-items: stretch; | ||||
| 	padding: 15px; | ||||
| 	border: 1px solid ${props => props.theme.dividerColor}; | ||||
| 	border-radius: 6px; | ||||
| @@ -96,12 +96,15 @@ const NeedUpgradeMessage = styled.span` | ||||
| 	font-size: ${props => props.theme.fontSize}px; | ||||
| `; | ||||
|  | ||||
| const DevModeLabel = styled.div` | ||||
| const BoxedLabel = styled.div` | ||||
| 	border: 1px solid ${props => props.theme.color}; | ||||
| 	border-radius: 4px; | ||||
| 	padding: 4px 6px; | ||||
| 	font-size: ${props => props.theme.fontSize * 0.75}px; | ||||
| 	color: ${props => props.theme.color}; | ||||
| 	flex-grow: 0; | ||||
| 	height: min-content; | ||||
| 	margin-top: auto; | ||||
| `; | ||||
|  | ||||
| const StyledNameAndVersion = styled.div<{ mb: any }>` | ||||
| @@ -170,7 +173,7 @@ export default function(props: Props) { | ||||
| 		if (!props.onToggle) return null; | ||||
|  | ||||
| 		if (item.devMode) { | ||||
| 			return <DevModeLabel>DEV</DevModeLabel>; | ||||
| 			return <BoxedLabel>DEV</BoxedLabel>; | ||||
| 		} | ||||
|  | ||||
| 		return <ToggleButton | ||||
| @@ -217,6 +220,17 @@ export default function(props: Props) { | ||||
| 		/>; | ||||
| 	} | ||||
|  | ||||
| 	const renderDefaultPluginLabel = () => { | ||||
| 		// Built-in plugins can only be disabled | ||||
| 		if (item.manifest._built_in) { | ||||
| 			return ( | ||||
| 				<BoxedLabel>{_('Built in')}</BoxedLabel> | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		return null; | ||||
| 	}; | ||||
|  | ||||
| 	function renderFooter() { | ||||
| 		if (item.devMode) return null; | ||||
|  | ||||
| @@ -236,6 +250,7 @@ export default function(props: Props) { | ||||
| 				{renderInstallButton()} | ||||
| 				{renderUpdateButton()} | ||||
| 				<div style={{ display: 'flex', flex: 1 }}/> | ||||
| 				{renderDefaultPluginLabel()} | ||||
| 			</CellFooter> | ||||
| 		); | ||||
| 	} | ||||
|   | ||||
| @@ -147,11 +147,18 @@ export default function(props: Props) { | ||||
| 		let cancelled = false; | ||||
|  | ||||
| 		async function fetchPluginIds() { | ||||
| 			const pluginIds = await repoApi().canBeUpdatedPlugins(pluginItems.map(p => p.manifest), pluginService.appVersion); | ||||
| 			// Built-in plugins can't be updated from the main repoApi | ||||
| 			const nonDefaultPlugins = pluginItems | ||||
| 				.map(p => p.manifest) | ||||
| 				.filter(manifest => !manifest._built_in); | ||||
|  | ||||
| 			const pluginIds = await repoApi().canBeUpdatedPlugins(nonDefaultPlugins, pluginService.appVersion); | ||||
| 			if (cancelled) return; | ||||
|  | ||||
| 			const conv: Record<string, boolean> = {}; | ||||
| 			// eslint-disable-next-line github/array-foreach -- Old code before rule was applied | ||||
| 			pluginIds.forEach(id => conv[id] = true); | ||||
| 			for (const id of pluginIds) { | ||||
| 				conv[id] = true; | ||||
| 			} | ||||
| 			setCanBeUpdatedPluginIds(conv); | ||||
| 		} | ||||
|  | ||||
| @@ -281,10 +288,17 @@ export default function(props: Props) { | ||||
| 				</UserPluginsRoot> | ||||
| 			); | ||||
| 		} else { | ||||
| 			const nonDefaultPlugins = pluginItems.filter(item => !item.manifest._built_in); | ||||
| 			const defaultPlugins = pluginItems.filter(item => item.manifest._built_in); | ||||
| 			return ( | ||||
| 				<UserPluginsRoot> | ||||
| 					{renderCells(pluginItems)} | ||||
| 				</UserPluginsRoot> | ||||
| 				<> | ||||
| 					<UserPluginsRoot> | ||||
| 						{renderCells(nonDefaultPlugins)} | ||||
| 					</UserPluginsRoot> | ||||
| 					<UserPluginsRoot> | ||||
| 						{renderCells(defaultPlugins)} | ||||
| 					</UserPluginsRoot> | ||||
| 				</> | ||||
| 			); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -2,6 +2,8 @@ const gulp = require('gulp'); | ||||
| const utils = require('@joplin/tools/gulp/utils'); | ||||
| const compileSass = require('@joplin/tools/compileSass'); | ||||
| const compilePackageInfo = require('@joplin/tools/compilePackageInfo'); | ||||
| import buildDefaultPlugins from '@joplin/default-plugins/commands/buildAll'; | ||||
| import copy7Zip from './tools/copy7Zip'; | ||||
| 
 | ||||
| const tasks = { | ||||
| 	compileScripts: { | ||||
| @@ -24,6 +26,17 @@ const tasks = { | ||||
| 	electronBuilder: { | ||||
| 		fn: require('./tools/electronBuilder.js'), | ||||
| 	}, | ||||
| 	copyDefaultPluginsAssets: { | ||||
| 		fn: async () => { | ||||
| 			await copy7Zip(); | ||||
| 		}, | ||||
| 	}, | ||||
| 	buildDefaultPlugins: { | ||||
| 		fn: async () => { | ||||
| 			const outputDir = `${__dirname}/build/defaultPlugins/`; | ||||
| 			await buildDefaultPlugins(outputDir); | ||||
| 		}, | ||||
| 	}, | ||||
| 	tsc: require('@joplin/tools/gulp/tasks/tsc'), | ||||
| 	updateIgnoredTypeScriptBuild: require('@joplin/tools/gulp/tasks/updateIgnoredTypeScriptBuild'), | ||||
| 	buildCommandIndex: require('@joplin/tools/gulp/tasks/buildCommandIndex'), | ||||
| @@ -39,7 +52,7 @@ const tasks = { | ||||
| 
 | ||||
| utils.registerGulpTasks(gulp, tasks); | ||||
| 
 | ||||
| const buildParallel = [ | ||||
| const buildBeforeStartParallel = [ | ||||
| 	'compileScripts', | ||||
| 	'compilePackageInfo', | ||||
| 	'copyPluginAssets', | ||||
| @@ -49,4 +62,12 @@ const buildParallel = [ | ||||
| 	'compileSass', | ||||
| ]; | ||||
| 
 | ||||
| gulp.task('build', gulp.parallel(...buildParallel)); | ||||
| gulp.task('before-start', gulp.parallel(...buildBeforeStartParallel)); | ||||
| 
 | ||||
| const buildAllSequential = [ | ||||
| 	'before-start', | ||||
| 	'copyDefaultPluginsAssets', | ||||
| 	'buildDefaultPlugins', | ||||
| ]; | ||||
| 
 | ||||
| gulp.task('build', gulp.series(buildAllSequential)); | ||||
| @@ -1,6 +1,5 @@ | ||||
| import { test, expect } from './util/test'; | ||||
| import MainScreen from './models/MainScreen'; | ||||
| import activateMainMenuItem from './util/activateMainMenuItem'; | ||||
| import SettingsScreen from './models/SettingsScreen'; | ||||
| import { _electron as electron } from '@playwright/test'; | ||||
| import { writeFile } from 'fs-extra'; | ||||
| @@ -91,10 +90,7 @@ test.describe('main', () => { | ||||
| 		// Sort order buttons should be visible by default | ||||
| 		await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).toBeVisible(); | ||||
|  | ||||
| 		// Open settings (check both labels so that this works on MacOS) | ||||
| 		expect( | ||||
| 			await activateMainMenuItem(electronApp, 'Preferences...') || await activateMainMenuItem(electronApp, 'Options'), | ||||
| 		).toBe(true); | ||||
| 		await mainScreen.openSettings(electronApp); | ||||
|  | ||||
| 		// Should be on the settings screen | ||||
| 		const settingsScreen = new SettingsScreen(mainWindow); | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Page, Locator } from '@playwright/test'; | ||||
| import { Page, Locator, ElectronApplication } from '@playwright/test'; | ||||
| import NoteEditorScreen from './NoteEditorScreen'; | ||||
| import activateMainMenuItem from '../util/activateMainMenuItem'; | ||||
|  | ||||
| export default class MainScreen { | ||||
| 	public readonly newNoteButton: Locator; | ||||
| @@ -33,4 +34,14 @@ export default class MainScreen { | ||||
|  | ||||
| 		return this.noteEditor; | ||||
| 	} | ||||
|  | ||||
| 	public async openSettings(electronApp: ElectronApplication) { | ||||
| 		// Check both labels so this works on MacOS | ||||
| 		const openedWithPreferences = await activateMainMenuItem(electronApp, 'Preferences...'); | ||||
| 		const openedWithOptions = await activateMainMenuItem(electronApp, 'Options'); | ||||
|  | ||||
| 		if (!openedWithOptions && !openedWithPreferences) { | ||||
| 			throw new Error('Unable to find settings menu item in application menus.'); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -5,9 +5,13 @@ export default class SettingsScreen { | ||||
| 	public readonly okayButton: Locator; | ||||
| 	public readonly appearanceTabButton: Locator; | ||||
|  | ||||
| 	public constructor(page: Page) { | ||||
| 	public constructor(private page: Page) { | ||||
| 		this.okayButton = page.locator('button', { hasText: 'OK' }); | ||||
| 		this.appearanceTabButton = page.getByText('Appearance'); | ||||
| 		this.appearanceTabButton = this.getTabLocator('Appearance'); | ||||
| 	} | ||||
|  | ||||
| 	public getTabLocator(tabName: string) { | ||||
| 		return this.page.locator('a[role="tab"] > span', { hasText: tabName }); | ||||
| 	} | ||||
|  | ||||
| 	public async waitFor() { | ||||
|   | ||||
							
								
								
									
										41
									
								
								packages/app-desktop/integration-tests/simpleBackup.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								packages/app-desktop/integration-tests/simpleBackup.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| import { test, expect } from './util/test'; | ||||
| import MainScreen from './models/MainScreen'; | ||||
| import SettingsScreen from './models/SettingsScreen'; | ||||
| import activateMainMenuItem from './util/activateMainMenuItem'; | ||||
|  | ||||
| test.describe('simpleBackup', () => { | ||||
| 	test('should have a section in settings', async ({ electronApp, startupPluginsLoaded, mainWindow }) => { | ||||
| 		await startupPluginsLoaded; | ||||
|  | ||||
| 		const mainScreen = new MainScreen(mainWindow); | ||||
| 		await mainScreen.waitFor(); | ||||
|  | ||||
| 		// Open settings (check both labels so that this works on MacOS) | ||||
| 		await mainScreen.openSettings(electronApp); | ||||
|  | ||||
| 		// Should be on the settings screen | ||||
| 		const settingsScreen = new SettingsScreen(mainWindow); | ||||
| 		await settingsScreen.waitFor(); | ||||
|  | ||||
| 		const backupTab = settingsScreen.getTabLocator('Backup'); | ||||
| 		await backupTab.waitFor(); | ||||
| 	}); | ||||
|  | ||||
| 	test('should be possible to create a backup', async ({ electronApp, startupPluginsLoaded, mainWindow }) => { | ||||
| 		await startupPluginsLoaded; | ||||
|  | ||||
| 		const mainScreen = new MainScreen(mainWindow); | ||||
| 		await mainScreen.waitFor(); | ||||
|  | ||||
| 		// Backups should work | ||||
| 		expect(await activateMainMenuItem(electronApp, 'Create backup')).toBe(true); | ||||
|  | ||||
| 		const successDialog = mainWindow.locator('iframe[id$=backup-backupDialog]'); | ||||
| 		await successDialog.waitFor(); | ||||
|  | ||||
| 		// Should report success | ||||
| 		const dialogContentLocator = successDialog.frameLocator(':scope'); | ||||
| 		await dialogContentLocator.getByText('Backup completed').waitFor(); | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
| @@ -10,6 +10,7 @@ import firstNonDevToolsWindow from './firstNonDevToolsWindow'; | ||||
| type JoplinFixtures = { | ||||
| 	profileDirectory: string; | ||||
| 	electronApp: ElectronApplication; | ||||
| 	startupPluginsLoaded: Promise<void>; | ||||
| 	mainWindow: Page; | ||||
| }; | ||||
|  | ||||
| @@ -43,6 +44,16 @@ export const test = base.extend<JoplinFixtures>({ | ||||
| 		await electronApp.close(); | ||||
| 	}, | ||||
|  | ||||
| 	startupPluginsLoaded: async ({ electronApp }, use) => { | ||||
| 		const startupPluginsLoadedPromise = electronApp.evaluate(({ ipcMain }) => { | ||||
| 			return new Promise<void>(resolve => { | ||||
| 				ipcMain.once('startup-plugins-loaded', () => resolve()); | ||||
| 			}); | ||||
| 		}); | ||||
|  | ||||
| 		await use(startupPluginsLoadedPromise); | ||||
| 	}, | ||||
|  | ||||
| 	mainWindow: async ({ electronApp }, use) => { | ||||
| 		const mainWindow = await firstNonDevToolsWindow(electronApp); | ||||
| 		await use(mainWindow); | ||||
|   | ||||
| @@ -7,12 +7,11 @@ | ||||
|   "scripts": { | ||||
|     "dist": "yarn run electronRebuild && npx electron-builder", | ||||
|     "build": "gulp build", | ||||
|     "postinstall": "yarn run build", | ||||
|     "electronBuilder": "gulp electronBuilder", | ||||
|     "electronRebuild": "gulp electronRebuild", | ||||
|     "tsc": "tsc --project tsconfig.json", | ||||
|     "watch": "tsc --watch --preserveWatchOutput --project tsconfig.json", | ||||
|     "start": "gulp build && electron . --env dev --log-level debug --open-dev-tools", | ||||
|     "start": "gulp before-start && electron . --env dev --log-level debug --open-dev-tools", | ||||
|     "test": "jest", | ||||
|     "test-ui": "playwright test", | ||||
|     "test-ci": "yarn test && sh ./integration-tests/run-ci.sh", | ||||
| @@ -36,7 +35,8 @@ | ||||
|     "extraResources": [ | ||||
|       "build/icons/**", | ||||
|       "build/images/**", | ||||
|       "build/defaultPlugins/**" | ||||
|       "build/defaultPlugins/**", | ||||
|       "build/7zip/**" | ||||
|     ], | ||||
|     "afterAllArtifactBuild": "./generateSha512.js", | ||||
|     "asar": true, | ||||
| @@ -115,7 +115,9 @@ | ||||
|   }, | ||||
|   "homepage": "https://github.com/laurent22/joplin#readme", | ||||
|   "devDependencies": { | ||||
|     "7zip-bin": "5.2.0", | ||||
|     "@electron/rebuild": "3.3.0", | ||||
|     "@joplin/default-plugins": "~2.13", | ||||
|     "@joplin/tools": "~2.13", | ||||
|     "@playwright/test": "1.39.0", | ||||
|     "@testing-library/react-hooks": "8.0.1", | ||||
| @@ -133,13 +135,9 @@ | ||||
|     "js-sha512": "0.8.0", | ||||
|     "nan": "2.18.0", | ||||
|     "react-test-renderer": "18.2.0", | ||||
|     "ts-node": "10.9.1", | ||||
|     "typescript": "5.2.2" | ||||
|   }, | ||||
|   "optionalDependencies": { | ||||
|     "7zip-bin-linux": "^1.0.1", | ||||
|     "7zip-bin-mac": "^1.0.1", | ||||
|     "7zip-bin-win": "^2.1.1" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@electron/notarize": "2.1.0", | ||||
|     "@electron/remote": "2.0.12", | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| 	// TODO: Not sure if that will work once packaged in Electron | ||||
| 	const sandboxProxy = require('../../vendor/lib/@joplin/lib/services/plugins/sandboxProxy.js'); | ||||
| 	const ipcRenderer = require('electron').ipcRenderer; | ||||
| 	const nodePath = require('path'); | ||||
|  | ||||
| 	const ipcRendererSend = (message, args) => { | ||||
| 		try { | ||||
| @@ -56,7 +57,27 @@ | ||||
| 				return require('../../node_modules/@joplin/lib/node_modules/sqlite3/lib/sqlite3.js'); | ||||
| 			} | ||||
|  | ||||
| 			if (['fs-extra'].includes(modulePath)) return require(modulePath); | ||||
| 			if (modulePath === 'fs-extra') { | ||||
| 				return require('fs-extra'); | ||||
| 			} | ||||
|  | ||||
| 			// 7zip-bin is required by one of the default plugins (simple-backup) | ||||
| 			if (modulePath === '7zip-bin') { | ||||
| 				// 7zip-bin is very large -- return the path to a version of 7zip | ||||
| 				// copied from 7zip-bin. | ||||
| 				const executableName = process.platform === 'win32' ? '7za.exe' : '7za'; | ||||
|  | ||||
| 				let rootDir = nodePath.dirname(nodePath.dirname(__dirname)); | ||||
|  | ||||
| 				// When bundled, __dirname points to a file within app.asar. The build/ directory | ||||
| 				// is outside of app.asar, and thus, we need an extra dirname(...). | ||||
| 				if (nodePath.basename(rootDir).startsWith('app.asar')) { | ||||
| 					rootDir = nodePath.dirname(rootDir); | ||||
| 				} | ||||
|  | ||||
| 				const pathTo7za = nodePath.join(rootDir, 'build', '7zip', executableName); | ||||
| 				return { path7za: nodePath.resolve(pathTo7za) }; | ||||
| 			} | ||||
|  | ||||
| 			throw new Error(`Module not found: ${modulePath}`); | ||||
| 		} | ||||
|   | ||||
							
								
								
									
										40
									
								
								packages/app-desktop/tools/copy7Zip.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								packages/app-desktop/tools/copy7Zip.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
|  | ||||
| import { copy } from 'fs-extra'; | ||||
| import { dirname, join } from 'path'; | ||||
|  | ||||
| const copy7Zip = async () => { | ||||
| 	// We allow buildin for a different architecture/platform with | ||||
| 	// the npm_config_target_arch and npm_config_target_platform environment variables. | ||||
| 	// | ||||
| 	// These are the same environment variables used by yarn when downloading dependencies. | ||||
| 	// | ||||
| 	const targetArch = process.env['npm_config_target_arch'] || process.arch; | ||||
| 	const targetPlatform = process.env['npm_config_target_platform'] || process.platform; | ||||
|  | ||||
| 	console.info('Copying 7zip for platform', targetPlatform, 'and architecture', targetArch); | ||||
|  | ||||
| 	// To use the custom architecture/platform, we copy the relevant files from 7zip-bin | ||||
| 	// directly: | ||||
|  | ||||
| 	const sevenZipBinDirectory = dirname(require.resolve('7zip-bin')); | ||||
| 	const platformToSubdirectory: Record<string, string> = { | ||||
| 		'win32': 'win', | ||||
| 		'darwin': 'mac', | ||||
| 		'linux': 'linux', | ||||
| 	}; | ||||
|  | ||||
| 	if (!(targetPlatform in platformToSubdirectory)) { | ||||
| 		throw new Error(`Invalid target platform ${targetPlatform}. Must be in ${Object.keys(platformToSubdirectory)}`); | ||||
| 	} | ||||
|  | ||||
| 	const fileName = targetPlatform === 'win32' ? '7za.exe' : '7za'; | ||||
| 	const pathTo7za = join( | ||||
| 		sevenZipBinDirectory, platformToSubdirectory[targetPlatform], targetArch, fileName, | ||||
| 	); | ||||
|  | ||||
| 	const rootDir = dirname(__dirname); | ||||
| 	const outputPath = join(rootDir, 'build', '7zip', fileName); | ||||
| 	await copy(pathTo7za, outputPath); | ||||
| }; | ||||
|  | ||||
| export default copy7Zip; | ||||
| @@ -7,5 +7,9 @@ | ||||
| 	"exclude": [ | ||||
| 		"**/node_modules", | ||||
| 		"**/dist", | ||||
|  | ||||
| 		// Exclude gulpfile.ts to prevent Gulp from trying to build from | ||||
| 		// gulpfile.js. | ||||
| 		"gulpfile.ts" | ||||
| 	], | ||||
| } | ||||
							
								
								
									
										2
									
								
								packages/default-plugins/.eslintignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								packages/default-plugins/.eslintignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| plugin-base-repo/ | ||||
| plugin-sources/* | ||||
							
								
								
									
										2
									
								
								packages/default-plugins/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								packages/default-plugins/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| built-plugins/ | ||||
| plugin-sources/* | ||||
							
								
								
									
										31
									
								
								packages/default-plugins/build.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								packages/default-plugins/build.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import buildAll from './commands/buildAll'; | ||||
| import editPatch from './commands/editPatch'; | ||||
| const yargs = require('yargs'); | ||||
|  | ||||
|  | ||||
| const build = () => { | ||||
| 	yargs | ||||
| 		.usage('$0 <cmd> [args]') | ||||
| 		.command('build <outputDir>', 'build all', (yargs: any) => { | ||||
| 			yargs.positional('outputDir', { | ||||
| 				type: 'string', | ||||
| 				describe: 'Path to the parent directory for built output', | ||||
| 			}); | ||||
| 		}, async (args: any) => { | ||||
| 			await buildAll(args.outputDir); | ||||
| 			process.exit(0); | ||||
| 		}) | ||||
| 		.command('patch <plugin>', 'Edit the patch file for the given plugin ID', (yargs: any) => { | ||||
| 			yargs.positional('plugin', { | ||||
| 				type: 'string', | ||||
| 				describe: 'ID of the plugin to patch', | ||||
| 			}); | ||||
| 		}, async (args: any) => { | ||||
| 			await editPatch(args.plugin, null); | ||||
| 			process.exit(0); | ||||
| 		}) | ||||
| 		.help() | ||||
| 		.argv; | ||||
| }; | ||||
|  | ||||
| build(); | ||||
							
								
								
									
										124
									
								
								packages/default-plugins/buildDefaultPlugins.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								packages/default-plugins/buildDefaultPlugins.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
|  | ||||
| /* eslint-disable no-console */ | ||||
|  | ||||
| import { copy, exists, remove, mkdirp, readdir, mkdtemp, readFile, writeFile } from 'fs-extra'; | ||||
| import { join, resolve, basename } from 'path'; | ||||
| import { tmpdir } from 'os'; | ||||
| import { chdir, cwd } from 'process'; | ||||
| import { execCommand } from '@joplin/utils'; | ||||
| import { glob } from 'glob'; | ||||
| import readRepositoryJson from './utils/readRepositoryJson'; | ||||
| import waitForCliInput from './utils/waitForCliInput'; | ||||
| import getPathToPatchFileFor from './utils/getPathToPatchFileFor'; | ||||
|  | ||||
| type BeforeEachInstallCallback = (buildDir: string, pluginName: string)=> Promise<void>; | ||||
|  | ||||
| const buildDefaultPlugins = async (outputParentDir: string|null, beforeInstall: BeforeEachInstallCallback) => { | ||||
| 	const pluginSourcesDir = resolve(join(__dirname, 'plugin-sources')); | ||||
| 	const pluginRepositoryData = await readRepositoryJson(join(__dirname, 'pluginRepositories.json')); | ||||
|  | ||||
| 	const originalDirectory = cwd(); | ||||
|  | ||||
| 	const logStatus = (...message: string[]) => { | ||||
| 		const blue = '\x1b[96m'; | ||||
| 		const reset = '\x1b[0m'; | ||||
| 		console.log(blue, ...message, reset); | ||||
| 	}; | ||||
|  | ||||
| 	for (const pluginId in pluginRepositoryData) { | ||||
| 		const repositoryData = pluginRepositoryData[pluginId]; | ||||
|  | ||||
| 		const buildDir = await mkdtemp(join(tmpdir(), 'default-plugin-build')); | ||||
| 		try { | ||||
| 			logStatus('Building plugin', pluginId, 'at', buildDir); | ||||
| 			const pluginDir = resolve(join(pluginSourcesDir, pluginId)); | ||||
|  | ||||
| 			// Clone the repository if not done yet | ||||
| 			if (!(await exists(pluginDir)) || (await readdir(pluginDir)).length === 0) { | ||||
| 				logStatus(`Cloning from repository ${repositoryData.cloneUrl}`); | ||||
| 				await execCommand(['git', 'clone', '--', repositoryData.cloneUrl, pluginDir]); | ||||
| 				chdir(pluginDir); | ||||
| 			} | ||||
|  | ||||
| 			chdir(pluginDir); | ||||
| 			const currentCommitHash = (await execCommand(['git', 'rev-parse', 'HEAD~'])).trim(); | ||||
| 			const expectedCommitHash = repositoryData.commit; | ||||
|  | ||||
| 			if (currentCommitHash !== expectedCommitHash) { | ||||
| 				logStatus(`Switching to commit ${expectedCommitHash}`); | ||||
| 				await execCommand(['git', 'switch', repositoryData.branch]); | ||||
| 				await execCommand(['git', 'checkout', expectedCommitHash]); | ||||
| 			} | ||||
|  | ||||
| 			logStatus('Copying repository files...'); | ||||
| 			await copy(pluginDir, buildDir, { | ||||
| 				filter: fileName => { | ||||
| 					return basename(fileName) !== '.git'; | ||||
| 				}, | ||||
| 			}); | ||||
|  | ||||
| 			chdir(buildDir); | ||||
|  | ||||
| 			logStatus('Initializing repository.'); | ||||
| 			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.'); | ||||
| 			await execCommand('git add .'); | ||||
| 			await execCommand(['git', 'config', 'user.name', 'Build script']); | ||||
| 			await execCommand(['git', 'config', 'user.email', '']); | ||||
| 			await execCommand(['git', 'commit', '-m', 'Initial commit']); | ||||
|  | ||||
| 			const patchFile = getPathToPatchFileFor(pluginId); | ||||
| 			if (await exists(patchFile)) { | ||||
| 				logStatus('Applying patch.'); | ||||
| 				await execCommand(['git', 'apply', patchFile]); | ||||
| 			} | ||||
|  | ||||
| 			await beforeInstall(buildDir, pluginId); | ||||
|  | ||||
| 			logStatus('Installing dependencies.'); | ||||
| 			await execCommand('npm install'); | ||||
|  | ||||
| 			const jplFiles = await glob('publish/*.jpl'); | ||||
| 			logStatus(`Found built .jpl files: ${JSON.stringify(jplFiles)}`); | ||||
|  | ||||
| 			if (jplFiles.length === 0) { | ||||
| 				throw new Error(`No published files found in ${buildDir}/publish`); | ||||
| 			} | ||||
|  | ||||
| 			if (outputParentDir !== null) { | ||||
| 				logStatus(`Checking output directory in ${outputParentDir}`); | ||||
| 				const outputDirectory = join(outputParentDir, pluginId); | ||||
| 				if (await exists(outputDirectory)) { | ||||
| 					await remove(outputDirectory); | ||||
| 				} | ||||
| 				await mkdirp(outputDirectory); | ||||
|  | ||||
| 				const sourceFile = jplFiles[0]; | ||||
| 				const destFile = join(outputDirectory, 'plugin.jpl'); | ||||
|  | ||||
| 				logStatus(`Copying built file from ${sourceFile} to ${destFile}`); | ||||
| 				await copy(sourceFile, destFile); | ||||
| 			} else { | ||||
| 				console.warn('No output directory specified. Not copying built .jpl files.'); | ||||
| 			} | ||||
| 		} catch (error) { | ||||
| 			console.error(error); | ||||
| 			console.log('Build directory', buildDir); | ||||
| 			await waitForCliInput(); | ||||
| 			throw error; | ||||
| 		} finally { | ||||
| 			chdir(originalDirectory); | ||||
| 			await remove(buildDir); | ||||
| 			logStatus('Removed build directory'); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| export default buildDefaultPlugins; | ||||
							
								
								
									
										7
									
								
								packages/default-plugins/commands/buildAll.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								packages/default-plugins/commands/buildAll.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| import buildDefaultPlugins from '../buildDefaultPlugins'; | ||||
|  | ||||
| const buildAll = (outputDirectory: string) => { | ||||
| 	return buildDefaultPlugins(outputDirectory, async () => { }); | ||||
| }; | ||||
|  | ||||
| export default buildAll; | ||||
							
								
								
									
										31
									
								
								packages/default-plugins/commands/editPatch.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								packages/default-plugins/commands/editPatch.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import { execCommand } from '@joplin/utils'; | ||||
| import waitForCliInput from '../utils/waitForCliInput'; | ||||
| import { copy } from 'fs-extra'; | ||||
| import { join } from 'path'; | ||||
| import buildDefaultPlugins from '../buildDefaultPlugins'; | ||||
| import getPathToPatchFileFor from '../utils/getPathToPatchFileFor'; | ||||
|  | ||||
| const editPatch = async (targetPluginId: string, outputParentDir: string|null) => { | ||||
| 	let patchedPlugin = false; | ||||
|  | ||||
| 	await buildDefaultPlugins(outputParentDir, async (buildDir, pluginId) => { | ||||
| 		if (pluginId !== targetPluginId) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// eslint-disable-next-line no-console | ||||
| 		console.log('Make changes to', buildDir, 'to create a patch.'); | ||||
| 		await waitForCliInput(); | ||||
| 		await execCommand(['sh', '-c', 'git diff -p > diff.diff']); | ||||
|  | ||||
| 		await copy(join(buildDir, './diff.diff'), getPathToPatchFileFor(pluginId)); | ||||
|  | ||||
| 		patchedPlugin = true; | ||||
| 	}); | ||||
|  | ||||
| 	if (!patchedPlugin) { | ||||
| 		throw new Error(`No default plugin with ID ${targetPluginId} found!`); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| export default editPatch; | ||||
							
								
								
									
										25
									
								
								packages/default-plugins/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								packages/default-plugins/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| { | ||||
| 	"name": "@joplin/default-plugins", | ||||
| 	"version": "2.13.0", | ||||
| 	"description": "Default plugins bundler", | ||||
| 	"private": true, | ||||
| 	"scripts": { | ||||
| 		"tsc": "tsc --project tsconfig.json", | ||||
| 		"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json", | ||||
| 		"patch": "ts-node build.ts patch" | ||||
| 	}, | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
| 		"url": "git+https://github.com/laurent22/joplin.git" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"@types/yargs": "17.0.31", | ||||
| 		"ts-node": "10.9.1", | ||||
| 		"typescript": "5.2.2" | ||||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"@joplin/utils": "~2.13", | ||||
| 		"fs-extra": "11.1.1", | ||||
| 		"yargs": "17.7.2" | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,62 @@ | ||||
| diff --git a/src/sevenZip.ts b/src/sevenZip.ts | ||||
| index ef2a527..d98c777 100644 | ||||
| --- a/src/sevenZip.ts | ||||
| +++ b/src/sevenZip.ts | ||||
| @@ -1,21 +1,21 @@ | ||||
|  // https://sevenzip.osdn.jp/chm/cmdline/exit_codes.htm | ||||
|  // https://sevenzip.osdn.jp/chm/cmdline/commands/index.htm | ||||
|  import * as _7z from "node-7z"; | ||||
| -import * as sevenBin from "7zip-bin"; | ||||
| -import * as path from "path"; | ||||
|  import { exec } from "child_process"; | ||||
|  import joplin from "api"; | ||||
| - | ||||
| -export let pathTo7zip = sevenBin.path7za; | ||||
| - | ||||
| -export namespace sevenZip { | ||||
| -  export async function updateBinPath() { | ||||
| -    pathTo7zip = path.join( | ||||
| -      await joplin.plugins.installationDir(), | ||||
| -      "7zip-bin", | ||||
| -      pathTo7zip | ||||
| -    ); | ||||
| -  } | ||||
| +const sevenBin = joplin.require("7zip-bin"); | ||||
| + | ||||
| + export let pathTo7zip = sevenBin.path7za; | ||||
| + | ||||
| + export namespace sevenZip { | ||||
| +   export async function updateBinPath() { | ||||
| +    // Not necessary with 7zip required from Joplin | ||||
| +    // pathTo7zip = path.join( | ||||
| +    //   await joplin.plugins.installationDir(), | ||||
| +    //   "7zip-bin", | ||||
| +    //   pathTo7zip | ||||
| +    // ); | ||||
| +   } | ||||
|   | ||||
|    export async function setExecutionFlag() { | ||||
|      if (process.platform !== "win32") { | ||||
| diff --git a/webpack.config.js b/webpack.config.js | ||||
| index 34a1797..7b2a480 100644 | ||||
| --- a/webpack.config.js | ||||
| +++ b/webpack.config.js | ||||
| @@ -200,15 +200,9 @@ const pluginConfig = { ...baseConfig, entry: './src/index.ts', | ||||
|  		path: distDir, | ||||
|  	}, | ||||
|  	plugins: [ | ||||
| -		new CopyPlugin({ | ||||
| -			patterns: [ | ||||
| -				{ | ||||
| -					from: '**/*', | ||||
| -					context: path.resolve(__dirname, 'node_modules','7zip-bin'), | ||||
| -					to: path.resolve(__dirname, 'dist/7zip-bin/'), | ||||
| -				}, | ||||
| -			] | ||||
| -		}), | ||||
| +		// Removed a CopyPlugin (added by Simple Backup, not necessary when using | ||||
| +		// Joplin's built-in 7zip) | ||||
| + | ||||
|  		new CopyPlugin({ | ||||
|  			patterns: [ | ||||
|  				{ | ||||
							
								
								
									
										7
									
								
								packages/default-plugins/pluginRepositories.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								packages/default-plugins/pluginRepositories.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| { | ||||
| 	"io.github.jackgruber.backup": { | ||||
| 		"cloneUrl": "https://github.com/JackGruber/joplin-plugin-backup.git", | ||||
| 		"branch": "master", | ||||
| 		"commit": "021085cc37ed83a91a7950744e462782e27c04a6" | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										12
									
								
								packages/default-plugins/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								packages/default-plugins/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| { | ||||
| 	"extends": "../../tsconfig.json", | ||||
| 	"include": [ | ||||
| 		"**/*.ts", | ||||
| 		"**/*.tsx", | ||||
| 	], | ||||
| 	"exclude": [ | ||||
| 		"**/node_modules", | ||||
| 		"plugin-sources/", | ||||
| 		"plugin-base-repo/" | ||||
| 	] | ||||
| } | ||||
							
								
								
									
										9
									
								
								packages/default-plugins/utils/getPathToPatchFileFor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								packages/default-plugins/utils/getPathToPatchFileFor.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
|  | ||||
| import { join, dirname } from 'path'; | ||||
|  | ||||
| const getPathToPatchFileFor = (pluginName: string) => { | ||||
| 	const rootDir = dirname(__dirname); | ||||
| 	return join(rootDir, 'plugin-patches', `${pluginName}.diff`); | ||||
| }; | ||||
|  | ||||
| export default getPathToPatchFileFor; | ||||
							
								
								
									
										37
									
								
								packages/default-plugins/utils/readRepositoryJson.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								packages/default-plugins/utils/readRepositoryJson.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| import { readFile } from 'fs-extra'; | ||||
|  | ||||
| export interface RepositoryData { | ||||
| 	cloneUrl: string; | ||||
| 	branch: string; | ||||
| 	commit: string; | ||||
| } | ||||
|  | ||||
| export interface AllRepositoryData { | ||||
| 	[pluginId: string]: RepositoryData; | ||||
| } | ||||
|  | ||||
| const readRepositoryJson = async (repositoryDataFilepath: string): Promise<AllRepositoryData> => { | ||||
| 	const fileContent = await readFile(repositoryDataFilepath, 'utf8'); | ||||
| 	const parsedJson = JSON.parse(fileContent); | ||||
|  | ||||
| 	// Validate | ||||
| 	for (const pluginId in parsedJson) { | ||||
| 		if (typeof parsedJson[pluginId] !== 'object') { | ||||
| 			throw new Error('pluginRepositories should map from plugin IDs to objects.'); | ||||
| 		} | ||||
|  | ||||
| 		const assertPropertyIsString = (propertyName: string) => { | ||||
| 			if (typeof parsedJson[pluginId][propertyName] !== 'string') { | ||||
| 				throw new Error(`Plugin ${pluginId} should have field '${propertyName}' of type string.`); | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| 		assertPropertyIsString('cloneUrl'); | ||||
| 		assertPropertyIsString('branch'); | ||||
| 		assertPropertyIsString('commit'); | ||||
| 	} | ||||
|  | ||||
| 	return parsedJson; | ||||
| }; | ||||
|  | ||||
| export default readRepositoryJson; | ||||
							
								
								
									
										23
									
								
								packages/default-plugins/utils/waitForCliInput.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								packages/default-plugins/utils/waitForCliInput.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
|  | ||||
| const readline = require('readline/promises'); | ||||
|  | ||||
| /* eslint-disable no-console */ | ||||
|  | ||||
| let readlineInterface: any = null; | ||||
| const waitForCliInput = async () => { | ||||
| 	readlineInterface ??= readline.createInterface({ | ||||
| 		input: process.stdin, | ||||
| 		output: process.stdout, | ||||
| 	}); | ||||
| 	if (process.stdin.isTTY) { | ||||
| 		const green = '\x1b[92m'; | ||||
| 		const reset = '\x1b[0m'; | ||||
| 		await readlineInterface.question(`${green}[Press enter to continue]${reset}`); | ||||
|  | ||||
| 		console.log('Continuing...'); | ||||
| 	} else { | ||||
| 		console.warn('Input is not from a TTY -- not waiting for input.'); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| export default waitForCliInput; | ||||
| @@ -33,7 +33,6 @@ export interface SettingAndValue { | ||||
| } | ||||
|  | ||||
| export interface DefaultPluginSettings { | ||||
| 	version: string; | ||||
| 	settings?: SettingAndValue; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -35,7 +35,10 @@ export async function installDefaultPlugins(service: PluginService, defaultPlugi | ||||
| 		const pluginId = pluginStat.path; | ||||
|  | ||||
| 		// 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 (installedPlugins.includes(pluginId) || !defaultPluginsId.includes(pluginId)) continue; | ||||
| 		if (installedPlugins.includes(pluginId) || !defaultPluginsId.includes(pluginId)) { | ||||
| 			logger.debug(`Skipping default plugin ${pluginId}, ${!defaultPluginsId.includes(pluginId) ? '(Not a default)' : ''}`); | ||||
| 			continue; | ||||
| 		} | ||||
| 		const defaultPluginPath: string = path.join(defaultPluginsDir, pluginId, 'plugin.jpl'); | ||||
| 		await service.installPlugin(defaultPluginPath, false); | ||||
|  | ||||
|   | ||||
| @@ -4,14 +4,10 @@ import Setting from '../../../models/Setting'; | ||||
| const getDefaultPluginsInfo = (): DefaultPluginsInfo => { | ||||
| 	const defaultPlugins = { | ||||
| 		'io.github.jackgruber.backup': { | ||||
| 			version: '1.1.1', | ||||
| 			settings: { | ||||
| 				'path': `${Setting.value('profileDir')}`, | ||||
| 			}, | ||||
| 		}, | ||||
| 		'plugin.calebjohn.rich-markdown': { | ||||
| 			version: '0.8.3', | ||||
| 		}, | ||||
| 	}; | ||||
| 	return defaultPlugins; | ||||
| }; | ||||
|   | ||||
| @@ -1,206 +0,0 @@ | ||||
|  | ||||
|  | ||||
|  | ||||
| import { join } from 'path'; | ||||
| import { downloadPlugins, extractPlugins, localPluginsVersion } from './bundleDefaultPlugins'; | ||||
| import { pathExists, readFile, remove } from 'fs-extra'; | ||||
| import Setting from '@joplin/lib/models/Setting'; | ||||
| import { createTempDir, supportDir } from '@joplin/lib/testing/test-utils'; | ||||
| import { rootDir } from './tool-utils'; | ||||
|  | ||||
| const fetch = require('node-fetch'); | ||||
|  | ||||
| jest.mock('node-fetch', ()=>jest.fn()); | ||||
|  | ||||
| const manifests = { | ||||
| 	'io.github.jackgruber.backup': { | ||||
| 		'manifest_version': 1, | ||||
| 		'id': 'io.github.jackgruber.backup', | ||||
| 		'app_min_version': '2.1.3', | ||||
| 		'version': '1.1.0', | ||||
| 		'name': 'Simple Backup', | ||||
| 		'description': 'Plugin to create manual and automatic backups.', | ||||
| 		'author': 'JackGruber', | ||||
| 		'homepage_url': 'https://github.com/JackGruber/joplin-plugin-backup/blob/master/README.md', | ||||
| 		'repository_url': 'https://github.com/JackGruber/joplin-plugin-backup', | ||||
| 		'keywords': [ | ||||
| 			'backup', | ||||
| 			'jex', | ||||
| 			'export', | ||||
| 			'zip', | ||||
| 			'7zip', | ||||
| 			'encrypted', | ||||
| 		], | ||||
| 		'_publish_hash': 'sha256:8d8c6a3bb92fafc587269aea58b623b05242d42c0766a05bbe25c3ba2bbdf8ee', | ||||
| 		'_publish_commit': 'master:00ed52133c659e0f3ac1a55f70b776c42fca0a6d', | ||||
| 		'_npm_package_name': 'joplin-plugin-backup', | ||||
| 	}, | ||||
| 	'plugin.calebjohn.rich-markdown': { | ||||
| 		'manifest_version': 1, | ||||
| 		'id': 'plugin.calebjohn.rich-markdown', | ||||
| 		'app_min_version': '2.7', | ||||
| 		'version': '0.9.0', | ||||
| 		'name': 'Rich Markdown', | ||||
| 		'description': 'Helping you ditch the markdown viewer for good.', | ||||
| 		'author': 'Caleb John', | ||||
| 		'homepage_url': 'https://github.com/CalebJohn/joplin-rich-markdown#readme', | ||||
| 		'repository_url': 'https://github.com/CalebJohn/joplin-rich-markdown', | ||||
| 		'keywords': [ | ||||
| 			'editor', | ||||
| 			'visual', | ||||
| 		], | ||||
| 		'_publish_hash': 'sha256:95337a3868aebdc9bf8c347a37460d0c2753b391ff51a0c72bdccdef9679705f', | ||||
| 		'_publish_commit': 'main:af3493b6ca96c931327ab3bd04906faaed0c782c', | ||||
| 		'_npm_package_name': 'joplin-plugin-rich-markdown', | ||||
| 	}, | ||||
|  | ||||
| }; | ||||
|  | ||||
| const NPM_Response1 = JSON.stringify({ | ||||
| 	'_id': 'joplin-plugin-rich-markdown', | ||||
| 	'name': 'joplin-plugin-rich-markdown', | ||||
| 	'versions': { | ||||
| 		'0.8.2': { | ||||
| 			'name': 'joplin-plugin-rich-markdown', | ||||
| 			'version': '0.8.2', | ||||
| 			'description': 'A plugin that will finally allow you to ditch the markdown viewer, saving space and making your life easier.', | ||||
| 			'_id': 'joplin-plugin-rich-markdown@0.1.0', | ||||
| 			'dist': { | ||||
| 				'tarball': 'no-link-here', | ||||
| 			}, | ||||
| 		}, | ||||
| 		'0.9.0': { | ||||
| 			'name': 'joplin-plugin-rich-markdown', | ||||
| 			'version': '0.9.0', | ||||
| 			'dist': { | ||||
| 				'tarball': 'response-1-link', | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
|  | ||||
| const NPM_Response2 = JSON.stringify({ | ||||
| 	'_id': 'io.github.jackgruber.backup', | ||||
| 	'name': 'joplin-plugin-rich-markdown', | ||||
| 	'versions': { | ||||
| 		'1.0.0': { | ||||
| 			'name': 'joplin-plugin-rich-markdown', | ||||
| 			'version': '1.0.0', | ||||
| 			'description': 'A plugin that will finally allow you to ditch the markdown viewer, saving space and making your life easier.', | ||||
| 			'_id': 'joplin-plugin-rich-markdown@0.1.0', | ||||
| 			'dist': { | ||||
| 				'tarball': 'no-link-here', | ||||
| 			}, | ||||
| 		}, | ||||
| 		'1.1.0': { | ||||
| 			'name': 'joplin-plugin-rich-markdown', | ||||
| 			'version': '1.1.0', | ||||
| 			'dist': { | ||||
| 				'tarball': 'response-2-link', | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
|  | ||||
| async function mockPluginData() { | ||||
| 	const filePath = join(__dirname, '..', 'app-cli', 'tests', 'services', 'plugins', 'mockData', 'mockPlugin.tgz'); | ||||
| 	const tgzData = await readFile(filePath, 'utf8'); | ||||
| 	return tgzData; | ||||
| } | ||||
|  | ||||
| describe('bundleDefaultPlugins', () => { | ||||
|  | ||||
| 	const testDefaultPluginsInfo = { | ||||
| 		'plugin.calebjohn.rich-markdown': { | ||||
| 			version: '0.9.0', | ||||
| 		}, | ||||
| 		'io.github.jackgruber.backup': { | ||||
| 			version: '1.1.0', | ||||
| 			settings: { | ||||
| 				'path': `${Setting.value('profileDir')}`, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}; | ||||
|  | ||||
| 	it('should get local plugin versions', async () => { | ||||
| 		const manifestsPath = join(supportDir, 'pluginRepo', 'plugins'); | ||||
| 		const testDefaultPluginsInfo = { | ||||
| 			'joplin.plugin.ambrt.backlinksToNote': { version: '1.0.4' }, | ||||
| 			'org.joplinapp.plugins.ToggleSidebars': { version: '1.0.2' }, | ||||
| 		}; | ||||
| 		const localPluginsVersions = await localPluginsVersion(manifestsPath, testDefaultPluginsInfo); | ||||
|  | ||||
| 		expect(localPluginsVersions['joplin.plugin.ambrt.backlinksToNote']).toBe('1.0.4'); | ||||
| 		expect(localPluginsVersions['org.joplinapp.plugins.ToggleSidebars']).toBe('1.0.2'); | ||||
| 	}); | ||||
|  | ||||
| 	it('should download plugins folder from GitHub with no initial plugins', async () => { | ||||
|  | ||||
| 		const testCases = [ | ||||
| 			{ | ||||
| 				localVersions: { 'io.github.jackgruber.backup': '0.0.0', 'plugin.calebjohn.rich-markdown': '0.0.0' }, | ||||
| 				downloadedPlugin1: 'joplin-plugin-rich-markdown-0.9.0.tgz', | ||||
| 				downloadedPlugin2: 'joplin-plugin-backup-1.1.0.tgz', | ||||
| 				numberOfCalls: 4, | ||||
| 				calledWith: ['https://registry.npmjs.org/joplin-plugin-rich-markdown', 'response-1-link', 'https://registry.npmjs.org/joplin-plugin-backup', 'response-2-link'], | ||||
| 			}, | ||||
| 			{ | ||||
| 				localVersions: { 'io.github.jackgruber.backup': '1.1.0', 'plugin.calebjohn.rich-markdown': '0.0.0' }, | ||||
| 				downloadedPlugin1: 'joplin-plugin-rich-markdown-0.9.0.tgz', | ||||
| 				downloadedPlugin2: undefined, | ||||
| 				numberOfCalls: 2, | ||||
| 				calledWith: ['https://registry.npmjs.org/joplin-plugin-rich-markdown', 'response-1-link'], | ||||
| 			}, | ||||
| 			{ | ||||
| 				localVersions: { 'io.github.jackgruber.backup': '1.1.0', 'plugin.calebjohn.rich-markdown': '0.9.0' }, | ||||
| 				downloadedPlugin1: undefined, | ||||
| 				downloadedPlugin2: undefined, | ||||
| 				numberOfCalls: 0, | ||||
| 				calledWith: [], | ||||
| 			}, | ||||
| 		]; | ||||
|  | ||||
| 		const tgzData = await mockPluginData(); | ||||
|  | ||||
| 		const mockFetch = fetch as jest.MockedFunction<typeof fetch>; | ||||
|  | ||||
| 		for (const testCase of testCases) { | ||||
|  | ||||
| 			mockFetch.mockResolvedValueOnce({ text: () => Promise.resolve(NPM_Response1), ok: true }) | ||||
| 				.mockResolvedValueOnce({ buffer: () => Promise.resolve(tgzData), ok: true }) | ||||
| 				.mockResolvedValueOnce({ text: () => Promise.resolve(NPM_Response2), ok: true }) | ||||
| 				.mockResolvedValueOnce({ buffer: () => Promise.resolve(tgzData), ok: true }); | ||||
|  | ||||
| 			const downloadedPlugins = await downloadPlugins(testCase.localVersions, testDefaultPluginsInfo, manifests); | ||||
|  | ||||
| 			expect(downloadedPlugins[Object.keys(testDefaultPluginsInfo)[0]]).toBe(testCase.downloadedPlugin1); | ||||
| 			expect(downloadedPlugins[Object.keys(testDefaultPluginsInfo)[1]]).toBe(testCase.downloadedPlugin2); | ||||
|  | ||||
| 			expect(mockFetch).toHaveBeenCalledTimes(testCase.numberOfCalls); | ||||
|  | ||||
| 			// eslint-disable-next-line github/array-foreach -- Old code before rule was applied | ||||
| 			testCase.calledWith.forEach((callValue, index) => expect(mockFetch).toHaveBeenNthCalledWith(index + 1, callValue)); | ||||
|  | ||||
| 			jest.clearAllMocks(); | ||||
| 		} | ||||
|  | ||||
| 		await remove(`${rootDir}/packages/tools/joplin-plugin-backup-1.1.0.tgz`); | ||||
| 		await remove(`${rootDir}/packages/tools/joplin-plugin-rich-markdown-0.9.0.tgz`); | ||||
| 	}); | ||||
|  | ||||
| 	it('should extract plugins files', async () => { | ||||
|  | ||||
| 		const downloadedPluginsNames = { 'plugin.calebjohn.rich-markdown': 'mockPlugin.tgz' }; | ||||
|  | ||||
| 		const filePath = join(__dirname, '..', 'app-cli', 'tests', 'services', 'plugins', 'mockData'); | ||||
| 		const tempDir = await createTempDir(); | ||||
|  | ||||
| 		await extractPlugins(filePath, tempDir, downloadedPluginsNames); | ||||
|  | ||||
| 		expect(await pathExists(join(tempDir, 'plugin.calebjohn.rich-markdown', 'plugin.jpl'))).toBe(true); | ||||
| 		expect(await pathExists(join(tempDir, 'plugin.calebjohn.rich-markdown', 'manifest.json'))).toBe(true); | ||||
|  | ||||
| 		await remove(tempDir); | ||||
| 	}); | ||||
|  | ||||
| }); | ||||
| @@ -1,96 +0,0 @@ | ||||
| import { join } from 'path'; | ||||
| import { pathExists, mkdir, readFile, move, remove, writeFile } from 'fs-extra'; | ||||
| import { DefaultPluginsInfo } from '@joplin/lib/services/plugins/PluginService'; | ||||
| import getDefaultPluginsInfo from '@joplin/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo'; | ||||
| import { execCommand } from '@joplin/utils'; | ||||
| const fetch = require('node-fetch'); | ||||
|  | ||||
| interface PluginAndVersion { | ||||
| 	[pluginId: string]: string; | ||||
| } | ||||
|  | ||||
| interface PluginIdAndName { | ||||
| 	[pluginId: string]: string; | ||||
| } | ||||
|  | ||||
| export const localPluginsVersion = async (defaultPluginDir: string, defaultPluginsInfo: DefaultPluginsInfo): Promise<PluginAndVersion> => { | ||||
| 	if (!await pathExists(join(defaultPluginDir))) await mkdir(defaultPluginDir); | ||||
| 	const localPluginsVersions: PluginAndVersion = {}; | ||||
|  | ||||
| 	for (const pluginId of Object.keys(defaultPluginsInfo)) { | ||||
|  | ||||
| 		if (!await pathExists(join(defaultPluginDir, pluginId))) { | ||||
| 			localPluginsVersions[pluginId] = '0.0.0'; | ||||
| 			continue; | ||||
| 		} | ||||
| 		const data = await readFile(`${defaultPluginDir}/${pluginId}/manifest.json`, 'utf8'); | ||||
| 		const manifest = JSON.parse(data); | ||||
| 		localPluginsVersions[pluginId] = manifest.version; | ||||
| 	} | ||||
| 	return localPluginsVersions; | ||||
| }; | ||||
|  | ||||
| async function downloadFile(url: string, outputPath: string) { | ||||
| 	const response = await fetch(url); | ||||
| 	if (!response.ok) { | ||||
| 		const responseText = await response.text(); | ||||
| 		throw new Error(`Cannot download file from ${url} : ${responseText.substr(0, 500)}`); | ||||
| 	} | ||||
| 	await writeFile(outputPath, await response.buffer()); | ||||
| } | ||||
|  | ||||
| export async function extractPlugins(currentDir: string, defaultPluginDir: string, downloadedPluginsNames: PluginIdAndName): Promise<void> { | ||||
| 	for (const pluginId of Object.keys(downloadedPluginsNames)) { | ||||
| 		await execCommand(`tar xzf ${currentDir}/${downloadedPluginsNames[pluginId]}`, { quiet: true }); | ||||
| 		await move(`package/publish/${pluginId}.jpl`, `${defaultPluginDir}/${pluginId}/plugin.jpl`, { overwrite: true }); | ||||
| 		await move(`package/publish/${pluginId}.json`, `${defaultPluginDir}/${pluginId}/manifest.json`, { overwrite: true }); | ||||
| 		await remove(`${downloadedPluginsNames[pluginId]}`); | ||||
| 		await remove('package'); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| export const downloadPlugins = async (localPluginsVersions: PluginAndVersion, defaultPluginsInfo: DefaultPluginsInfo, manifests: any): Promise<PluginIdAndName> => { | ||||
|  | ||||
| 	const downloadedPluginsNames: PluginIdAndName = {}; | ||||
| 	for (const pluginId of Object.keys(defaultPluginsInfo)) { | ||||
| 		if (localPluginsVersions[pluginId] === defaultPluginsInfo[pluginId].version) continue; | ||||
| 		const response = await fetch(`https://registry.npmjs.org/${manifests[pluginId]._npm_package_name}`); | ||||
|  | ||||
| 		if (!response.ok) { | ||||
| 			const responseText = await response.text(); | ||||
| 			throw new Error(`Cannot fetch ${manifests[pluginId]._npm_package_name} release info from NPM : ${responseText.substr(0, 500)}`); | ||||
| 		} | ||||
| 		const releaseText = await response.text(); | ||||
| 		const release = JSON.parse(releaseText); | ||||
|  | ||||
| 		const pluginUrl = release.versions[defaultPluginsInfo[pluginId].version].dist.tarball; | ||||
|  | ||||
| 		const pluginName = `${manifests[pluginId]._npm_package_name}-${defaultPluginsInfo[pluginId].version}.tgz`; | ||||
| 		await downloadFile(pluginUrl, pluginName); | ||||
|  | ||||
| 		downloadedPluginsNames[pluginId] = pluginName; | ||||
| 	} | ||||
| 	return downloadedPluginsNames; | ||||
| }; | ||||
|  | ||||
| async function start(): Promise<void> { | ||||
| 	const defaultPluginDir = join(__dirname, '..', '..', 'packages', 'app-desktop', 'build', 'defaultPlugins'); | ||||
| 	const defaultPluginsInfo = getDefaultPluginsInfo(); | ||||
|  | ||||
| 	const manifestData = await fetch('https://raw.githubusercontent.com/joplin/plugins/master/manifests.json'); | ||||
| 	const manifests = JSON.parse(await manifestData.text()); | ||||
| 	if (!manifests) throw new Error('Invalid or missing JSON'); | ||||
|  | ||||
| 	const localPluginsVersions = await localPluginsVersion(defaultPluginDir, defaultPluginsInfo); | ||||
| 	const downloadedPluginNames: PluginIdAndName = await downloadPlugins(localPluginsVersions, defaultPluginsInfo, manifests); | ||||
| 	await extractPlugins(__dirname, defaultPluginDir, downloadedPluginNames); | ||||
| } | ||||
|  | ||||
| if (require.main === module) { | ||||
| // eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied | ||||
| 	start().catch((error) => { | ||||
| 		console.error('Fatal error'); | ||||
| 		console.error(error); | ||||
| 		process.exit(1); | ||||
| 	}); | ||||
| } | ||||
| @@ -31,6 +31,7 @@ module.exports = { | ||||
| 				'packages/app-desktop/dist/**', | ||||
| 				'packages/app-mobile/android/**', | ||||
| 				'packages/app-mobile/ios/**', | ||||
| 				'packages/default-plugins/plugin-sources/**', | ||||
| 				'packages/fork-sax/**', | ||||
| 				'packages/lib/plugin_types/**', | ||||
| 				'packages/server/**', | ||||
|   | ||||
| @@ -140,6 +140,7 @@ async function main() { | ||||
| 	await updatePackageVersion(`${rootDir}/packages/server/package.json`, majorMinorVersion, options); | ||||
| 	await updatePackageVersion(`${rootDir}/packages/tools/package.json`, majorMinorVersion, options); | ||||
| 	await updatePackageVersion(`${rootDir}/packages/utils/package.json`, majorMinorVersion, options); | ||||
| 	await updatePackageVersion(`${rootDir}/packages/default-plugins/package.json`, majorMinorVersion, options); | ||||
| 	await updatePackageVersion(`${rootDir}/packages/editor/package.json`, majorMinorVersion, options); | ||||
|  | ||||
| 	if (options.updateVersion) { | ||||
|   | ||||
| @@ -19,6 +19,7 @@ | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "tsc": "tsc --project tsconfig.json", | ||||
|     "build": "yarn run tsc", | ||||
|     "watch": "tsc --watch --preserveWatchOutput --project tsconfig.json", | ||||
|     "test": "jest --verbose=false", | ||||
|     "test-ci": "yarn test" | ||||
|   | ||||
| @@ -9,7 +9,6 @@ To add a new default plugin for desktop: | ||||
| ``` | ||||
| const defaultPlugins = { | ||||
|     'samplePluginId': { | ||||
|         version: '1.0.0', | ||||
|         settings: { | ||||
|             'settingName1': 'setting-value1', | ||||
|             'settingName2': 'setting-value2', | ||||
| @@ -18,13 +17,37 @@ const defaultPlugins = { | ||||
| }; | ||||
| ``` | ||||
|  | ||||
| After this, add the commit, branch, and clone URL to be build from to `pluginRepositories.json`. | ||||
|  | ||||
| For example, | ||||
| ```json | ||||
| { | ||||
| 	"plugin.id.here": { | ||||
| 		"cloneUrl": "https://example.com/plugin-repo/plugin-repo-here.git", | ||||
| 		"branch": "main", | ||||
| 		"commit": "840d2e84b70adf6de961e167dcd27ddad088b286" | ||||
| 	} | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Patching the plugin | ||||
|  | ||||
| Some plugins need patching. To create or update a plugin's patch, run the `patch` command in the `packages/default-plugins/` directory. | ||||
|  | ||||
| For example, | ||||
| ```shell | ||||
| $ cd packages/default-plugins | ||||
| $ yarn run patch plugin.id.here | ||||
| ``` | ||||
|  | ||||
| The script will create a temporary directory in which changes can be made. Do not stage the changes that should appear in the patch. | ||||
|  | ||||
| ## Bundling of default plugins | ||||
|  | ||||
| Script for bundling default plugins is present in [bundleDefaultPlugins.ts](https://github.com/laurent22/joplin/blob/eb7083d7888433ff6ef76ccfb7fb87ba951d513f/packages/tools/bundleDefaultPlugins.ts) | ||||
| Scripts for bundling default plugins are present in `packages/default-plugins/`. | ||||
|  | ||||
| Every time a new desktop release is being built, we compare the local default plugins version with pinned plugin version mentioned in [desktopDefaultPluginsInfo.ts](https://github.com/laurent22/joplin/blob/eb7083d7888433ff6ef76ccfb7fb87ba951d513f/packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.ts) | ||||
| These are run by the `app-desktop` package on a full `build` (e.g. on `postinstall`). | ||||
|  | ||||
| If there is a newer version available, we will pull the `tgz` file of plugin from NPM registry and extract it. We will then move `manifest.json` and `plugin.jpl` to the build folder of desktop. | ||||
|  | ||||
| ## Installing of default plugins | ||||
|  | ||||
|   | ||||
							
								
								
									
										48
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -5,24 +5,10 @@ __metadata: | ||||
|   version: 6 | ||||
|   cacheKey: 8 | ||||
| 
 | ||||
| "7zip-bin-linux@npm:^1.0.1": | ||||
|   version: 1.3.1 | ||||
|   resolution: "7zip-bin-linux@npm:1.3.1" | ||||
|   conditions: os=linux | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
| 
 | ||||
| "7zip-bin-mac@npm:^1.0.1": | ||||
|   version: 1.0.1 | ||||
|   resolution: "7zip-bin-mac@npm:1.0.1" | ||||
|   conditions: os=darwin | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
| 
 | ||||
| "7zip-bin-win@npm:^2.1.1": | ||||
|   version: 2.2.0 | ||||
|   resolution: "7zip-bin-win@npm:2.2.0" | ||||
|   conditions: os=win32 | ||||
| "7zip-bin@npm:5.2.0": | ||||
|   version: 5.2.0 | ||||
|   resolution: "7zip-bin@npm:5.2.0" | ||||
|   checksum: 85d3102275342f1f4ba7d17e778e526dee3dbec0f57d29be7afaa6e3c26687d40a6eccf520e9140143f85a51f3353f6b545f760eff3f776c6ffb30dc5252fb7c | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
| 
 | ||||
| @@ -6411,14 +6397,13 @@ __metadata: | ||||
|   version: 0.0.0-use.local | ||||
|   resolution: "@joplin/app-desktop@workspace:packages/app-desktop" | ||||
|   dependencies: | ||||
|     7zip-bin-linux: ^1.0.1 | ||||
|     7zip-bin-mac: ^1.0.1 | ||||
|     7zip-bin-win: ^2.1.1 | ||||
|     7zip-bin: 5.2.0 | ||||
|     "@electron/notarize": 2.1.0 | ||||
|     "@electron/rebuild": 3.3.0 | ||||
|     "@electron/remote": 2.0.12 | ||||
|     "@fortawesome/fontawesome-free": 5.15.4 | ||||
|     "@joeattardi/emoji-button": 4.6.4 | ||||
|     "@joplin/default-plugins": ~2.13 | ||||
|     "@joplin/editor": ~2.13 | ||||
|     "@joplin/lib": ~2.13 | ||||
|     "@joplin/renderer": ~2.13 | ||||
| @@ -6478,14 +6463,8 @@ __metadata: | ||||
|     styled-system: 5.1.5 | ||||
|     taboverride: 4.0.3 | ||||
|     tinymce: 5.10.6 | ||||
|     ts-node: 10.9.1 | ||||
|     typescript: 5.2.2 | ||||
|   dependenciesMeta: | ||||
|     7zip-bin-linux: | ||||
|       optional: true | ||||
|     7zip-bin-mac: | ||||
|       optional: true | ||||
|     7zip-bin-win: | ||||
|       optional: true | ||||
|   languageName: unknown | ||||
|   linkType: soft | ||||
| 
 | ||||
| @@ -6594,6 +6573,19 @@ __metadata: | ||||
|   languageName: unknown | ||||
|   linkType: soft | ||||
| 
 | ||||
| "@joplin/default-plugins@workspace:packages/default-plugins, @joplin/default-plugins@~2.13": | ||||
|   version: 0.0.0-use.local | ||||
|   resolution: "@joplin/default-plugins@workspace:packages/default-plugins" | ||||
|   dependencies: | ||||
|     "@joplin/utils": ~2.13 | ||||
|     "@types/yargs": 17.0.31 | ||||
|     fs-extra: 11.1.1 | ||||
|     ts-node: 10.9.1 | ||||
|     typescript: 5.2.2 | ||||
|     yargs: 17.7.2 | ||||
|   languageName: unknown | ||||
|   linkType: soft | ||||
| 
 | ||||
| "@joplin/doc-builder@workspace:packages/doc-builder": | ||||
|   version: 0.0.0-use.local | ||||
|   resolution: "@joplin/doc-builder@workspace:packages/doc-builder" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user