Desktop: Performance: Faster startup and smaller application size (#12366)

This commit is contained in:
Henry Heino 2025-06-06 02:10:49 -07:00 committed by GitHub
parent a527a278a9
commit 86ee95a8d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 694 additions and 180 deletions

View File

@ -578,6 +578,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/bundleJs.js
packages/app-desktop/tools/copy7Zip.js
packages/app-desktop/tools/generateLatestArm64Yml.js
packages/app-desktop/tools/githubReleasesUtils.js
@ -592,6 +593,7 @@ packages/app-desktop/utils/customProtocols/constants.js
packages/app-desktop/utils/customProtocols/handleCustomProtocols.test.js
packages/app-desktop/utils/customProtocols/handleCustomProtocols.js
packages/app-desktop/utils/customProtocols/registerCustomProtocols.js
packages/app-desktop/utils/getAssetPath.js
packages/app-desktop/utils/initializeCommandService.js
packages/app-desktop/utils/isSafeToOpen.test.js
packages/app-desktop/utils/isSafeToOpen.js

View File

@ -23,6 +23,7 @@ module.exports = {
'FileSystemCreateWritableOptions': 'readonly',
'FileSystemHandle': 'readonly',
'IDBTransactionMode': 'readonly',
'globalThis': 'readonly',
// ServiceWorker
'ExtendableEvent': 'readonly',

3
.gitignore vendored
View File

@ -46,6 +46,7 @@ sync_staging.sh
TODO.md
packages/tools/commit_hook.txt
packages/tools/github_oauth_token.txt
packages/app-desktop/main-html-out.js
lerna-debug.log
.env
docs/**/*.mustache
@ -552,6 +553,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/bundleJs.js
packages/app-desktop/tools/copy7Zip.js
packages/app-desktop/tools/generateLatestArm64Yml.js
packages/app-desktop/tools/githubReleasesUtils.js
@ -566,6 +568,7 @@ packages/app-desktop/utils/customProtocols/constants.js
packages/app-desktop/utils/customProtocols/handleCustomProtocols.test.js
packages/app-desktop/utils/customProtocols/handleCustomProtocols.js
packages/app-desktop/utils/customProtocols/registerCustomProtocols.js
packages/app-desktop/utils/getAssetPath.js
packages/app-desktop/utils/initializeCommandService.js
packages/app-desktop/utils/isSafeToOpen.test.js
packages/app-desktop/utils/isSafeToOpen.js

View File

@ -1,6 +1,22 @@
# We remove the `canvas` optional dependency because electron-rebuild fails to build it, and
# the `canvas` API is already part of Electron
diff --git a/build/pdf.js b/build/pdf.js
index 4acf16b1d6f9351bda1a98649ea4f926618fe617..f63dbc6050ca63ca8e8ed982edea134103fa15dd 100644
--- a/build/pdf.js
+++ b/build/pdf.js
@@ -6244,8 +6244,9 @@ class NodeFilterFactory extends _base_factory.BaseFilterFactory {}
exports.NodeFilterFactory = NodeFilterFactory;
class NodeCanvasFactory extends _base_factory.BaseCanvasFactory {
_createCanvas(width, height) {
- const Canvas = require("canvas");
- return Canvas.createCanvas(width, height);
+ throw new Error('Node canvas disabled');
+ // const Canvas = require("canvas");
+ // return Canvas.createCanvas(width, height);
}
}
exports.NodeCanvasFactory = NodeCanvasFactory;
diff --git a/package.json b/package.json
index 105811f53d508486e08a60dc1b6e437cd24d7427..dea6a4e6612c4a4006cc482e46ff5270dcfda1e5 100644
--- a/package.json

View File

@ -15,9 +15,9 @@
"scripts": {
"buildApiDoc": "yarn workspace joplin start apidoc ../../readme/api/references/rest_api.md",
"buildScriptIndexes": "node packages/tools/gulp/tasks/buildScriptIndexesRun.js",
"buildParallel": "yarn workspaces foreach --verbose --interlaced --parallel --jobs 2 --topological run build && yarn tsc",
"buildParallel": "yarn workspaces foreach --verbose --interlaced --parallel --jobs 2 --topological-dev run build && yarn tsc",
"buildPluginDoc": "cd packages/generate-plugin-doc && yarn buildPluginDoc_",
"buildSequential": "yarn workspaces foreach --verbose --interlaced --topological run build && yarn tsc",
"buildSequential": "yarn workspaces foreach --verbose --interlaced --topological-dev run build && yarn tsc",
"buildServerDocker": "node packages/tools/buildServerDocker.js",
"buildSettingJsonSchema": "yarn workspace joplin start settingschema ../../../joplin-website/docs/schema/settings.json",
"buildTranslations": "node packages/tools/build-translation.js",
@ -110,6 +110,9 @@
"app-builder-lib@24.13.3": "patch:app-builder-lib@npm%3A24.13.3#./.yarn/patches/app-builder-lib-npm-24.13.3-86a66c0bf3.patch",
"react-native-sqlite-storage@6.0.1": "patch:react-native-sqlite-storage@npm%3A6.0.1#./.yarn/patches/react-native-sqlite-storage-npm-6.0.1-8369d747bd.patch",
"react-native-paper@5.13.1": "patch:react-native-paper@npm%3A5.13.1#./.yarn/patches/react-native-paper-npm-5.13.1-f153e542e2.patch",
"react-native-popup-menu@0.17.0": "patch:react-native-popup-menu@npm%3A0.17.0#./.yarn/patches/react-native-popup-menu-npm-0.17.0-8b745d88dd.patch"
"react-native-popup-menu@0.17.0": "patch:react-native-popup-menu@npm%3A0.17.0#./.yarn/patches/react-native-popup-menu-npm-0.17.0-8b745d88dd.patch",
"pdfjs-dist@2.16.105": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
"pdfjs-dist@*": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
"pdfjs-dist@3.11.174": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch"
}
}

View File

@ -8,7 +8,7 @@ import Note from '@joplin/lib/models/Note';
import Tag from '@joplin/lib/models/Tag';
import Setting from '@joplin/lib/models/Setting';
import { reg } from '@joplin/lib/registry.js';
import { fileExtension } from '@joplin/lib/path-utils';
import { dirname, fileExtension } from '@joplin/lib/path-utils';
import { splitCommandString } from '@joplin/utils';
import { _ } from '@joplin/lib/locale';
import { pathExists, readFile, readdirSync } from 'fs-extra';
@ -397,8 +397,12 @@ class Application extends BaseApplication {
}
public async start(argv: string[]) {
const keychainEnabled = this.checkIfKeychainEnabled(argv);
// TODO: Currently, `pluginAssetDir` needs to be set differently for each platform and requires
// a call to Setting.setConstant. Ideally, this would be done in a way that requires users to
// set this constant on startup.
Setting.setConstant('pluginAssetDir', `${dirname(require.resolve('@joplin/renderer'))}/assets`);
const keychainEnabled = this.checkIfKeychainEnabled(argv);
argv = await super.start(argv, { keychainEnabled });
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied

View File

@ -25,3 +25,7 @@ build/7zip/7za
build/7zip/7za.exe
sentry.properties
downloads/
# Bundler output
*.js.meta.json
*.bundle.js

View File

@ -246,7 +246,7 @@ export class Bridge {
// version of electron-context-menu.
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
public setupContextMenu(_spellCheckerMenuItemsHandler: Function) {
require('electron-context-menu')({
require('./services/electron-context-menu')({
allWindows: [this.mainWindow()],
electronApp: this.electronApp(),

View File

@ -3,7 +3,7 @@ import { _ } from '@joplin/lib/locale';
import { stateUtils } from '@joplin/lib/reducer';
import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher';
import Note from '@joplin/lib/models/Note';
const bridge = require('@electron/remote').require('./bridge').default;
import bridge from '../services/bridge';
export const declaration: CommandDeclaration = {
name: 'startExternalEditing',

View File

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

View File

@ -1,3 +1,4 @@
import * as React from 'react';
import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
import EmojiBox from './EmojiBox';

View File

@ -4,7 +4,6 @@ import ButtonBar from './ConfigScreen/ButtonBar';
import { _ } from '@joplin/lib/locale';
import { clipboard } from 'electron';
import Button, { ButtonLevel } from './Button/Button';
const bridge = require('@electron/remote').require('./bridge').default;
import { uuidgen } from '@joplin/lib/uuid';
import { Dispatch } from 'redux';
import { reducer, defaultState, generateApplicationConfirmUrl, checkIfLoginWasSuccessful } from '@joplin/lib/services/joplinCloudUtils';
@ -12,6 +11,7 @@ import { AppState } from '../app.reducer';
import Logger from '@joplin/utils/Logger';
import { reg } from '@joplin/lib/registry';
import JoplinCloudSignUpCallToAction from './JoplinCloudSignUpCallToAction';
import bridge from '../services/bridge';
const logger = Logger.create('JoplinCloudLoginScreen');
const { connect } = require('react-redux');
@ -62,7 +62,7 @@ const JoplinCloudScreenComponent = (props: Props) => {
const onAuthorizeClicked = async () => {
const url = await generateApplicationConfirmUrl(confirmUrl(applicationAuthId));
bridge().openExternal(url);
void bridge().openExternal(url);
onButtonUsed();
};

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import { useEffect, useImperativeHandle, useState, useRef, useCallback, forwardRef } from 'react';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import * as CodeMirror from 'codemirror';
import CodeMirror from 'codemirror';
import 'codemirror/addon/comment/comment';
import 'codemirror/addon/dialog/dialog';
@ -32,54 +32,41 @@ import Setting from '@joplin/lib/models/Setting';
// import eventManager from '@joplin/lib/eventManager';
import { reg } from '@joplin/lib/registry';
import { focus } from '@joplin/lib/utils/focusHandler';
// Based on http://pypl.github.io/PYPL.html
const topLanguages = [
'python',
'clike',
'javascript',
'jsx',
'php',
'r',
'swift',
'go',
'vb',
'vbscript',
'ruby',
'rust',
'dart',
'lua',
'groovy',
'perl',
'cobol',
'julia',
'haskell',
'pascal',
'css',
import 'codemirror/mode/python/python';
import 'codemirror/mode/clike/clike';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/jsx/jsx';
import 'codemirror/mode/php/php';
import 'codemirror/mode/r/r';
import 'codemirror/mode/swift/swift';
import 'codemirror/mode/go/go';
import 'codemirror/mode/vb/vb';
import 'codemirror/mode/vbscript/vbscript';
import 'codemirror/mode/ruby/ruby';
import 'codemirror/mode/rust/rust';
import 'codemirror/mode/dart/dart';
import 'codemirror/mode/lua/lua';
import 'codemirror/mode/groovy/groovy';
import 'codemirror/mode/perl/perl';
import 'codemirror/mode/cobol/cobol';
import 'codemirror/mode/julia/julia';
import 'codemirror/mode/haskell/haskell';
import 'codemirror/mode/pascal/pascal';
import 'codemirror/mode/css/css';
// Additional languages, not in the PYPL list
'xml', // For HTML too
'markdown',
'yaml',
'shell',
'dockerfile',
'diff',
'erlang',
'sql',
];
// Load Top Modes
for (let i = 0; i < topLanguages.length; i++) {
const mode = topLanguages[i];
// Additional languages, not in the PYPL lis;
import 'codemirror/mode/xml/xml'; // For HTML too
import 'codemirror/mode/markdown/markdown';
import 'codemirror/mode/yaml/yaml';
import 'codemirror/mode/shell/shell';
import 'codemirror/mode/dockerfile/dockerfile';
import 'codemirror/mode/diff/diff';
import 'codemirror/mode/erlang/erlang';
import 'codemirror/mode/sql/sql';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
if (CodeMirror.modeInfo.find((m: any) => m.mode === mode)) {
require(`codemirror/mode/${mode}/${mode}`);
} else {
reg.logger().error('Cannot find CodeMirror mode: ', mode);
}
}
export interface EditorProps {
value: string;

View File

@ -2,7 +2,6 @@ import shim from '@joplin/lib/shim';
import Setting from '@joplin/lib/models/Setting';
import Note from '@joplin/lib/models/Note';
import Resource from '@joplin/lib/models/Resource';
const bridge = require('@electron/remote').require('./bridge').default;
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
import htmlUtils from '@joplin/lib/htmlUtils';
import rendererHtmlUtils, { extractHtmlBody, removeWrappingParagraphAndTrailingEmptyElements } from '@joplin/renderer/htmlUtils';
@ -15,6 +14,7 @@ import { fileExtension, filename, safeFileExtension, safeFilename } from '@jopli
const joplinRendererUtils = require('@joplin/renderer').utils;
const { clipboard } = require('electron');
import * as mimeUtils from '@joplin/lib/mime-utils';
import bridge from '../../../services/bridge';
const md5 = require('md5');
const path = require('path');

View File

@ -7,6 +7,8 @@ import { ForwardedRef, forwardRef, RefObject, useContext, useEffect, useImperati
import { WindowIdContext } from './NewWindowOrIFrame';
import useDocument from './hooks/useDocument';
import { _ } from '@joplin/lib/locale';
import getAssetPath from '../utils/getAssetPath';
import { toForwardSlashes } from '@joplin/utils/path';
interface Props {
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
@ -240,7 +242,7 @@ const NoteTextViewer = forwardRef((props: Props, ref: ForwardedRef<NoteViewerCon
allow='clipboard-write=(self) fullscreen=(self) autoplay=(self) local-fonts=(self) encrypted-media=(self)'
allowFullScreen={true}
aria-label={_('Note viewer')}
src={`joplin-content://note-viewer/${__dirname}/note-viewer/index.html`}
src={`joplin-content://note-viewer/${toForwardSlashes(getAssetPath('gui/note-viewer/index.html'))}`}
></iframe>
);
});

View File

@ -5,7 +5,7 @@ import { _ } from '@joplin/lib/locale';
const { connect } = require('react-redux');
import { reg } from '@joplin/lib/registry';
import Setting from '@joplin/lib/models/Setting';
const bridge = require('@electron/remote').require('./bridge').default;
import bridge from '../services/bridge';
const { themeStyle } = require('@joplin/lib/theme');
const { OneDriveApiNodeUtils } = require('@joplin/lib/onedrive-api-node-utils.js');
@ -66,7 +66,7 @@ class OneDriveLoginScreenComponent extends React.Component<any, any> {
const logComps = [];
for (const l of this.state.authLog) {
if (l.text.indexOf('http:') === 0) {
logComps.push(<a key={l.key} style={theme.urlStyle} href="#" onClick={() => { bridge().openExternal(l.text); }}>{l.text}</a>);
logComps.push(<a key={l.key} style={theme.urlStyle} href="#" onClick={() => { void bridge().openExternal(l.text); }}>{l.text}</a>);
} else {
logComps.push(<p key={l.key} style={theme.textStyle}>{l.text}</p>);
}

View File

@ -10,7 +10,7 @@ import MoveButtons, { MoveButtonClickEvent } from './MoveButtons';
import { StyledWrapperRoot, StyledMoveOverlay, MoveModeRootMessage } from './utils/style';
import type { ResizeCallback, ResizeStartCallback } from 're-resizable';
import Dialog from '../Dialog';
import * as EventEmitter from 'events';
import EventEmitter = require('events');
import LayoutItemContainer from './LayoutItemContainer';
interface OnResizeEvent {

View File

@ -343,4 +343,4 @@ const mapStateToProps = (state: any) => ({
const ResourceScreen = connect(mapStateToProps)(ResourceScreenComponent);
module.exports = { ResourceScreen };
export default ResourceScreen;

View File

@ -21,7 +21,7 @@ import DialogButtonRow, { ButtonSpec, ClickEvent, ClickEventHandler } from './Di
import Dialog from './Dialog';
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
import ImportScreen from './ImportScreen';
const { ResourceScreen } = require('./ResourceScreen.js');
import ResourceScreen from './ResourceScreen';
import Navigator from './Navigator';
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
import JoplinCloudLoginScreen from './JoplinCloudLoginScreen';

View File

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

View File

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

View File

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

View File

@ -4,9 +4,18 @@ 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';
import bundleJs from './tools/bundleJs';
import { remove } from 'fs-extra';
const tasks = {
bundle: {
fn: () => bundleJs(false),
},
// Bundles and computes additional information that can be analysed with
// locally or with https://esbuild.github.io/analyze/.
bundleWithStats: {
fn: () => bundleJs(true),
},
compileScripts: {
fn: require('./tools/compileScripts'),
},
@ -54,7 +63,7 @@ const tasks = {
utils.registerGulpTasks(gulp, tasks);
const buildBeforeStartParallel = [
const buildBeforeStartParallel = gulp.parallel(
'compileScripts',
'compilePackageInfo',
'copyPluginAssets',
@ -62,14 +71,21 @@ const buildBeforeStartParallel = [
'updateIgnoredTypeScriptBuild',
'buildScriptIndexes',
'compileSass',
];
);
const buildRequiresTsc = gulp.series('bundle');
gulp.task('before-start', gulp.parallel(...buildBeforeStartParallel));
gulp.task('before-start', gulp.series(
buildRequiresTsc,
buildBeforeStartParallel,
));
gulp.task('before-dist', buildRequiresTsc);
const buildAllSequential = [
'before-start',
// Since "build" runs before "tsc", exclude tasks that require
// other packages to be built (i.e. don't include buildRequiresTsc).
const buildSequential = [
buildBeforeStartParallel,
'copyDefaultPluginsAssets',
'buildDefaultPlugins',
];
gulp.task('build', gulp.series(buildAllSequential));
gulp.task('build', gulp.series(buildSequential));

View File

@ -12,11 +12,11 @@
<link rel="stylesheet" href="style.min.css">
<script src="vendor/lib/smalltalk/dist/smalltalk.min.js"></script>
<script src="./node_modules/tesseract.js/dist/tesseract.min.js"></script>
<script src="vendor/lib/tesseract.js/dist/tesseract.min.js"></script>
</head>
<body>
<div id="react-root"></div>
<script src="./utils/window/eventHandlerOverrides.js"></script>
<script src="main-html.js"></script>
<script src="./main-html.bundle.js"></script>
</body>
</html>

View File

@ -2,6 +2,7 @@
import { ElectronApplication, expect, Locator, Page } from '@playwright/test';
import MainScreen from './MainScreen';
import activateMainMenuItem from '../util/activateMainMenuItem';
import { msleep } from '@joplin/utils/time';
export default class GoToAnything {
public readonly containerLocator: Locator;
@ -38,6 +39,8 @@ export default class GoToAnything {
// This expect.poll retries the search if it initially fails.
await expect.poll(async () => {
await this.inputLocator.clear();
// Pause to help ensure that the change in the search input is detected
await msleep(300);
await this.inputLocator.fill(query);
try {
await expect(resultLocator).toBeVisible({ timeout: 1000 });
@ -47,7 +50,7 @@ export default class GoToAnything {
return error;
}
return true;
}, { timeout: 10_000 }).toBe(true);
}, { timeout: 15_000 }).toBe(true);
}
public async expectToBeClosed() {

View File

@ -3,7 +3,7 @@ import { dirname, resolve } from 'path';
const createStartupArgs = (profileDirectory: string) => {
// Input paths need to be absolute when running from VSCode
const baseDirectory = dirname(dirname(__dirname));
const mainPath = resolve(baseDirectory, 'main.js');
const mainPath = resolve(baseDirectory, 'main.bundle.js');
// We need to run with --env dev to disable the single instance check.
return [

View File

@ -19,6 +19,7 @@ jest.mock('@electron/remote', () => {
default: {},
};
},
getGlobal: () => ({}),
};
});

View File

@ -3,7 +3,7 @@
// Disable React message in console "Download the React DevTools for a better development experience"
// https://stackoverflow.com/questions/42196819/disable-hide-download-the-react-devtools#42196820
// eslint-disable-next-line no-undef
__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
supportsFiber: true,
inject: function() {},
onCommitFiberRoot: function() {},
@ -22,9 +22,9 @@ const Setting = require('@joplin/lib/models/Setting').default;
const Revision = require('@joplin/lib/models/Revision').default;
const Logger = require('@joplin/utils/Logger').default;
const FsDriverNode = require('@joplin/lib/fs-driver-node').default;
const bridge = require('./services/bridge').default;
const shim = require('@joplin/lib/shim').default;
const { shimInit } = require('@joplin/lib/shim-init-node.js');
const bridge = require('@electron/remote').require('./bridge').default;
const EncryptionService = require('@joplin/lib/services/e2ee/EncryptionService').default;
const FileApiDriverLocal = require('@joplin/lib/file-api-driver-local').default;
const React = require('react');
@ -34,6 +34,9 @@ const pdfJs = require('pdfjs-dist');
const { isAppleSilicon } = require('is-apple-silicon');
require('@sentry/electron/renderer');
// Allows components to use React as a global
window.React = React;
const main = async () => {
if (bridge().env() === 'dev') {
@ -83,6 +86,7 @@ const main = async () => {
Setting.setConstant('appId', bridge().appId());
Setting.setConstant('appType', 'desktop');
Setting.setConstant('pluginAssetDir', `${__dirname}/pluginAssets`);
// eslint-disable-next-line no-console
console.info(`appId: ${Setting.value('appId')}`);

View File

@ -11,7 +11,7 @@ const envFromArgs = require('@joplin/lib/envFromArgs');
const packageInfo = require('./packageInfo.js');
const { isCallbackUrl } = require('@joplin/lib/callbackUrlUtils');
const determineBaseAppDirs = require('@joplin/lib/determineBaseAppDirs').default;
const registerCustomProtocols = require('./utils/customProtocols/registerCustomProtocols.js').default;
const registerCustomProtocols = require('./utils/customProtocols/registerCustomProtocols').default;
// Electron takes the application name from package.json `name` and
// displays this in the tray icon toolip and message box titles, however in
@ -74,7 +74,7 @@ const wrapper = new ElectronAppWrapper(electronApp, {
env, profilePath: rootProfileDir, isDebugMode, initialCallbackUrl, isEndToEndTesting,
});
initBridge(wrapper, appId, appName, rootProfileDir, autoUploadCrashDumps, altInstanceId);
globalThis.joplinBridge = initBridge(wrapper, appId, appName, rootProfileDir, autoUploadCrashDumps, altInstanceId);
wrapper.start().catch((error) => {
console.error('Electron App fatal error:');

View File

@ -2,18 +2,19 @@
"name": "@joplin/app-desktop",
"version": "3.4.1",
"description": "Joplin for Desktop",
"main": "main.js",
"main": "main.bundle.js",
"private": true,
"scripts": {
"dist": "yarn electronRebuild && npx electron-builder",
"dist": "gulp before-dist && yarn electronRebuild && npx electron-builder",
"build": "gulp build",
"bundle": "gulp bundleWithStats",
"electronBuilder": "gulp electronBuilder",
"electronRebuild": "gulp electronRebuild",
"tsc": "tsc --project tsconfig.json",
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json",
"start": "gulp before-start && electron . --env dev --log-level debug --open-dev-tools --no-welcome",
"test": "jest",
"test-ui": "playwright test",
"test-ui": "gulp before-start && playwright test",
"test-ci": "yarn test",
"modifyReleaseAssets": "node tools/modifyReleaseAssets.js"
},
@ -131,61 +132,53 @@
"devDependencies": {
"7zip-bin": "5.2.0",
"@axe-core/playwright": "4.10.1",
"@electron/notarize": "2.3.2",
"@electron/rebuild": "3.6.0",
"@fortawesome/fontawesome-free": "5.15.4",
"@joeattardi/emoji-button": "4.6.4",
"@joplin/default-plugins": "~3.4",
"@joplin/editor": "~3.4",
"@joplin/lib": "~3.4",
"@joplin/renderer": "~3.4",
"@joplin/tools": "~3.4",
"@joplin/utils": "~3.4",
"@playwright/test": "1.51.1",
"@sentry/electron": "4.24.0",
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.12",
"@types/mustache": "4.2.5",
"@types/node": "18.19.67",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/react-redux": "7.1.33",
"@types/styled-components": "5.1.32",
"@types/tesseract.js": "2.0.0",
"axios": "^1.7.7",
"electron": "35.2.1",
"electron-builder": "24.13.3",
"glob": "10.4.5",
"gulp": "4.0.2",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"js-sha512": "0.9.0",
"nan": "2.19.0",
"react-test-renderer": "18.3.1",
"ts-jest": "29.1.5",
"ts-node": "10.9.2",
"typescript": "5.4.5"
},
"dependencies": {
"@electron/notarize": "2.3.2",
"@electron/remote": "2.1.2",
"@fortawesome/fontawesome-free": "5.15.4",
"@joeattardi/emoji-button": "4.6.4",
"@joplin/editor": "~3.4",
"@joplin/lib": "~3.4",
"@joplin/renderer": "~3.4",
"@joplin/utils": "~3.4",
"@sentry/electron": "4.24.0",
"@types/mustache": "4.2.5",
"async-mutex": "0.5.0",
"axios": "^1.7.7",
"codemirror": "5.65.9",
"color": "3.2.1",
"compare-versions": "6.1.1",
"countable": "3.0.1",
"debounce": "1.2.1",
"electron": "35.2.1",
"electron-builder": "24.13.3",
"electron-updater": "6.2.1",
"electron-window-state": "5.0.3",
"esbuild": "^0.25.3",
"formatcoords": "1.1.3",
"fs-extra": "11.2.0",
"glob": "10.4.5",
"gulp": "4.0.2",
"highlight.js": "11.10.0",
"immer": "9.0.21",
"is-apple-silicon": "1.1.2",
"keytar": "7.9.0",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"js-sha512": "0.9.0",
"mark.js": "8.11.1",
"md5": "2.3.0",
"moment": "2.30.1",
"mustache": "4.2.0",
"nan": "2.19.0",
"node-fetch": "2.6.7",
"node-notifier": "10.0.1",
"node-rsa": "1.1.1",
@ -196,17 +189,27 @@
"react-dom": "18.3.1",
"react-redux": "8.1.3",
"react-select": "5.8.0",
"react-test-renderer": "18.3.1",
"react-toggle-button": "2.2.0",
"react-tooltip": "4.5.1",
"redux": "4.2.1",
"reselect": "4.1.8",
"roboto-fontface": "0.10.0",
"smalltalk": "2.5.1",
"sqlite3": "5.1.6",
"styled-components": "5.3.11",
"styled-system": "5.1.5",
"taboverride": "4.0.3",
"tesseract.js": "5.1.0",
"tinymce": "6.8.5"
"tinymce": "6.8.5",
"ts-jest": "29.1.5",
"ts-node": "10.9.2",
"typescript": "5.4.5"
},
"dependencies": {
"@electron/remote": "2.1.2",
"@joplin/onenote-converter": "~3.4",
"fs-extra": "11.2.0",
"keytar": "7.9.0",
"sqlite3": "5.1.6"
}
}

View File

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

View File

@ -8,6 +8,7 @@ import { EventHandlers } from '@joplin/lib/services/plugins/utils/mapEventHandle
import shim from '@joplin/lib/shim';
import Logger from '@joplin/utils/Logger';
import getPathToExecutable7Zip from '../../utils/7zip/getPathToExecutable7Zip';
import getAssetPath from '../../utils/getAssetPath';
// import BackOffHandler from './BackOffHandler';
const ipcRenderer = require('electron').ipcRenderer;
@ -134,7 +135,7 @@ export default class PluginRunner extends BasePluginRunner {
};
void pluginWindow.loadURL(`${require('url').format({
pathname: require('path').join(__dirname, 'plugin_index.html'),
pathname: getAssetPath('services/plugins/plugin_index.html'),
protocol: 'file:',
slashes: true,
})}?pluginId=${encodeURIComponent(plugin.id)}&pluginScript=${encodeURIComponent(`file://${scriptPath}`)}&libraryData=${encodeURIComponent(JSON.stringify(libraryData))}`);

View File

@ -12,6 +12,8 @@ import { WindowIdContext } from '../../gui/NewWindowOrIFrame';
import useSubmitHandler from './hooks/useSubmitHandler';
import useFormData from './hooks/useFormData';
import Setting from '@joplin/lib/models/Setting';
import getAssetPath from '../../utils/getAssetPath';
import { toForwardSlashes } from '@joplin/utils/path';
const logger = Logger.create('UserWebview');
@ -124,7 +126,7 @@ function UserWebview(props: Props, ref: any) {
const src = useMemo(() => {
const isolate = Setting.value('featureFlag.plugins.isolatePluginWebViews');
const path = `${__dirname}/UserWebviewIndex.html`;
const path = toForwardSlashes(getAssetPath('services/plugins/UserWebviewIndex.html'));
if (isolate) {
return `joplin-content://plugin-webview/${path}`;
} else {

View File

@ -0,0 +1,111 @@
import { filename, toForwardSlashes } from '@joplin/utils/path';
import * as esbuild from 'esbuild';
import { existsSync } from 'fs';
import { writeFile } from 'fs/promises';
import { dirname, join, relative } from 'path';
// Note: Roughly based on js-draw's use of esbuild:
// https://github.com/personalizedrefrigerator/js-draw/blob/6fe6d6821402a08a8d17f15a8f48d95e5d7b084f/packages/build-tool/src/BundledFile.ts#L64
const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSizeStats: boolean) => {
return esbuild.context({
entryPoints: [entryPoint],
outfile: `${filename(entryPoint)}.bundle.js`,
bundle: true,
minify: true,
sourcemap: true,
metafile: computeFileSizeStats,
platform: 'node',
target: ['node20.0'],
mainFields: renderer ? ['browser', 'main'] : ['main'],
plugins: [
{
// Configures ESBuild to require(...) certain libraries that cause issues if included directly
// in the bundle. Some of these are transitive dependencies and so need to have relative paths
// in the final bundle.
name: 'joplin--relative-imports-for-externals',
setup: build => {
const externalRegex = /^(.*\.node|sqlite3|electron|@electron\/remote\/.*|electron\/.*|@mapbox\/node-pre-gyp|jsdom)$/;
const baseDir = dirname(__dirname);
const baseNodeModules = join(baseDir, 'node_modules');
build.onResolve({ filter: externalRegex }, args => {
// Electron packages don't need relative requires
if (args.path === 'electron' || args.path.startsWith('electron/')) {
return { path: args.path, external: true, namespace: 'node' };
}
// Other packages may need relative requires
let path = toForwardSlashes(relative(
baseDir,
require.resolve(args.path, { paths: [baseNodeModules, args.resolveDir, baseDir] }),
));
if (!path.startsWith('.')) {
path = `./${path}`;
}
// Some files have .node.* extensions but are not native modules. These files are often required using
// require('./something.node') rather than require('./something.node.js'). Skip path remapping for
// these files:
if (args.path.endsWith('.node') && (path.endsWith('.ts') || path.endsWith('.js'))) {
// Normal .ts or .js file -- continue.
return null;
}
// Log that this is external -- it should be included in "dependencies" and not "devDependencies" in package.json:
console.log('External path:', path, args.importer);
return {
path,
external: true,
};
});
},
},
{
// Rewrite imports to prefer .js files to .ts. Otherwise, certain files are duplicated in the final bundle
name: 'joplin--prefer-js-imports',
setup: build => {
const baseDir = dirname(__dirname);
const baseNodeModules = join(baseDir, 'node_modules');
// Rewrite all relative imports
build.onResolve({ filter: /^\./ }, args => {
try {
const importPath = args.path === '.' ? './index' : args.path;
let path = require.resolve(importPath, { paths: [args.resolveDir, baseNodeModules, baseDir] });
// require.resolve **can** return paths with .ts extensions, presumably because
// this build script is a .ts file.
if (path.endsWith('.ts')) {
const alternative = path.replace(/\.ts$/, '.js');
if (existsSync(alternative)) {
path = alternative;
}
}
return { path };
} catch (error) {
return {
errors: [{ text: `Failed to import: ${error}`, detail: error }],
};
}
});
},
},
],
});
};
const bundleJs = async (writeStats: boolean) => {
const entryPoints = [
{ fileName: 'main.js', renderer: false },
{ fileName: 'main-html.js', renderer: true },
];
for (const { fileName, renderer } of entryPoints) {
const compiler = await makeBuildContext(fileName, renderer, writeStats);
const result = await compiler.rebuild();
if (writeStats) {
const outPath = `${dirname(__dirname)}/${fileName}.meta.json`;
console.log('Writing bundle stats to ', outPath);
await writeFile(outPath, JSON.stringify(result.metafile, undefined, '\t'));
}
await compiler.dispose();
}
};
export default bundleJs;

View File

@ -90,6 +90,7 @@ async function main() {
'smalltalk/dist/smalltalk.min.js',
'smalltalk/img/IDR_CLOSE_DIALOG_H.png',
'smalltalk/img/IDR_CLOSE_DIALOG.png',
'tesseract.js/dist/tesseract.min.js',
{
src: resolve(__dirname, '../../lib/services/plugins/sandboxProxy.js'),
dest: `${buildLibDir}/@joplin/lib/services/plugins/sandboxProxy.js`,

View File

@ -11,5 +11,5 @@
// Exclude gulpfile.ts to prevent Gulp from trying to build from
// gulpfile.js.
"gulpfile.ts"
],
]
}

View File

@ -0,0 +1,9 @@
import { dirname, join } from 'path';
const getAssetPath = (path: string) => {
// __dirname sometimes points to app-desktop/
const baseDir = __dirname.match(/utils[/\\]?$/) ? dirname(__dirname) : __dirname;
return join(baseDir, path);
};
export default getAssetPath;

View File

@ -19,7 +19,7 @@ export default class PluginAssetsLoader {
}
private destDir_() {
return `${Setting.value('resourceDir')}/pluginAssets`;
return Setting.value('pluginAssetDir');
}
private async importAssetsMobile_() {

View File

@ -527,6 +527,7 @@ async function initialize(dispatch: Dispatch) {
Setting.setConstant('cacheDir', `${getProfilesRootDir()}/cache`);
const resourceDir = getResourceDir(currentProfile, isSubProfile);
Setting.setConstant('resourceDir', resourceDir);
Setting.setConstant('pluginAssetDir', `${Setting.value('resourceDir')}/pluginAssets`);
Setting.setConstant('pluginDir', `${getProfilesRootDir()}/plugins`);
Setting.setConstant('pluginDataDir', getPluginDataDir(currentProfile, isSubProfile));

View File

@ -29,8 +29,9 @@
"exports": {
".": "./index.js",
"./lib/uslug": {
"types": "./lib/uslug.ts",
"require": "./lib/uslug.js",
"types": "./lib/uslug.ts"
"default": "./lib/uslug.js"
}
},
"engines": {

View File

@ -6,12 +6,12 @@
"types": "index.ts",
"exports": {
".": {
"default": "./dist/index.js",
"types": "./index.ts"
"types": "./index.ts",
"default": "./dist/index.js"
},
"./packToString": {
"default": "./dist/packToString.js",
"types": "./packToString.ts"
"types": "./packToString.ts",
"default": "./dist/packToString.js"
}
},
"publishConfig": {

View File

@ -1,6 +1,6 @@
import shim from './shim';
import { _ } from './locale';
const { rtrimSlashes } = require('./path-utils.js');
import { rtrimSlashes } from './path-utils';
import JoplinError from './JoplinError';
import { Env } from './models/Setting';
import Logger from '@joplin/utils/Logger';

View File

@ -1,7 +1,7 @@
import FileApiDriverJoplinServer from './file-api-driver-joplinServer';
import Setting from './models/Setting';
import Synchronizer from './Synchronizer';
import { _ } from './locale.js';
import { _ } from './locale';
import JoplinServerApi, { Session } from './JoplinServerApi';
import BaseSyncTarget from './BaseSyncTarget';
import { FileApi } from './file-api';

View File

@ -21,6 +21,16 @@ export interface RemoveOptions {
recursive?: boolean;
}
export interface ZipExtractOptions {
source: string;
extractTo: string;
}
export interface ZipEntry {
entryName: string;
name: string;
}
export default class FsDriverBase {
@ -258,4 +268,8 @@ export default class FsDriverBase {
throw new Error('Not implemented: tarCreate');
}
public async zipExtract(_options: ZipExtractOptions): Promise<ZipEntry[]> {
throw new Error('Not implemented: zipExtract');
}
}

View File

@ -1,4 +1,5 @@
import FsDriverBase, { Stat } from './fs-driver-base';
import AdmZip = require('adm-zip');
import FsDriverBase, { Stat, ZipEntry, ZipExtractOptions } from './fs-driver-base';
import time from './time';
const md5File = require('md5-file');
const fs = require('fs-extra');
@ -210,4 +211,9 @@ export default class FsDriverNode extends FsDriverBase {
await require('tar').create(options, filePaths);
}
public async zipExtract(options: ZipExtractOptions): Promise<ZipEntry[]> {
const zip = new AdmZip(options.source);
zip.extractAllTo(options.extractTo, false);
return zip.getEntries();
}
}

View File

@ -15,11 +15,14 @@ const { wrapError } = require('./errorUtils');
const { enexXmlToHtml } = require('./import-enex-html-gen.js');
const md5 = require('md5');
const { Base64Decode } = require('base64-stream');
const md5File = require('md5-file');
import * as mime from './mime-utils';
import type * as FsExtra from 'fs-extra';
// const Promise = require('promise');
const fs = require('fs-extra');
let fs_: typeof FsExtra = null;
const fs = () => {
fs_ ??= shim.requireDynamic('fs-extra');
return fs_;
};
function dateToTimestamp(s: string, defaultValue: number = null): number {
// Most dates seem to be in this format
@ -60,9 +63,9 @@ async function decodeBase64File(sourceFilePath: string, destFilePath: string) {
// to disk, thus resulting in the calling code to find a
// file with size 0.
const destFile = fs.openSync(destFilePath, 'w');
const sourceStream = fs.createReadStream(sourceFilePath);
const destStream = fs.createWriteStream(destFile, {
const destFile = fs().openSync(destFilePath, 'w');
const sourceStream = fs().createReadStream(sourceFilePath);
const destStream = fs().createWriteStream(destFilePath, {
fd: destFile,
autoClose: false,
});
@ -72,8 +75,8 @@ async function decodeBase64File(sourceFilePath: string, destFilePath: string) {
// because even if the source has finished sending data, the destination might not have
// finished receiving it and writing it to disk.
destStream.on('finish', () => {
fs.fdatasyncSync(destFile);
fs.closeSync(destFile);
fs().fdatasyncSync(destFile);
fs().closeSync(destFile);
resolve(null);
});
@ -140,7 +143,7 @@ async function processNoteResource(resource: ExtractedResource) {
if (setId) resource.id = md5(Date.now() + Math.random());
resource.size = 0;
resource.dataFilePath = `${Setting.value('tempDir')}/${resource.id}.empty`;
await fs.writeFile(resource.dataFilePath, '');
await shim.fsDriver().writeFile(resource.dataFilePath, '');
};
if (!resource.hasData) {
@ -155,14 +158,14 @@ async function processNoteResource(resource: ExtractedResource) {
throw new Error(`Cannot decode resource with encoding: ${resource.dataEncoding}`);
}
const stats = fs.statSync(resource.dataFilePath);
const stats = await shim.fsDriver().stat(resource.dataFilePath);
resource.size = stats.size;
if (!resource.id) {
// If no resource ID is present, the resource ID is actually the MD5
// of the data. This ID will match the "hash" attribute of the
// corresponding <en-media> tag. resourceId = md5(decodedData);
resource.id = await md5File(resource.dataFilePath);
resource.id = await shim.fsDriver().md5File(resource.dataFilePath);
}
if (!resource.id || !resource.size) {
@ -203,7 +206,7 @@ async function saveNoteResources(note: ExtractedNote) {
const existingResource = await Resource.load(toSave.id);
if (existingResource) continue;
await fs.move(resource.dataFilePath, Resource.fullPath(toSave), { overwrite: true });
await shim.fsDriver().move(resource.dataFilePath, Resource.fullPath(toSave));
await Resource.save(toSave, { isNew: true });
resourcesCreated++;
}
@ -381,7 +384,7 @@ const parseNotes = async (parentFolderId: string, filePath: string, importOption
notesTagged: 0,
};
const stream = fs.createReadStream(fileToProcess);
const stream = fs().createReadStream(fileToProcess);
const options = {};
const strict = true;
@ -547,7 +550,7 @@ const parseNotes = async (parentFolderId: string, filePath: string, importOption
noteResource.hasData = true;
fs.appendFileSync(noteResource.dataFilePath, text);
fs().appendFileSync(noteResource.dataFilePath, text);
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
if (!(n in noteResource)) (noteResource as any)[n] = '';

View File

@ -49,6 +49,7 @@ export interface Constants {
appType: AppType;
resourceDirName: string;
resourceDir: string;
pluginAssetDir: string;
profileDir: string;
rootProfileDir: string;
tempDir: string;
@ -217,6 +218,7 @@ class Setting extends BaseModel {
appType: 'SET_ME' as any, // 'cli' or 'mobile'
resourceDirName: '',
resourceDir: '',
pluginAssetDir: '',
profileDir: '',
rootProfileDir: '',
tempDir: '',

View File

@ -18,7 +18,11 @@ import InteropService_Exporter_Md from './InteropService_Exporter_Md';
import InteropService_Exporter_Md_frontmatter from './InteropService_Exporter_Md_frontmatter';
import InteropService_Importer_Base from './InteropService_Importer_Base';
import InteropService_Exporter_Base from './InteropService_Exporter_Base';
import Module, { dynamicRequireModuleFactory, makeExportModule, makeImportModule } from './Module';
import Module, { makeExportModule, makeImportModule } from './Module';
import InteropService_Exporter_Html from './InteropService_Exporter_Html';
import InteropService_Importer_EnexToHtml from './InteropService_Importer_EnexToHtml';
import InteropService_Importer_EnexToMd from './InteropService_Importer_EnexToMd';
import InteropService_Importer_OneNote from './InteropService_Importer_OneNote';
const { sprintf } = require('sprintf-js');
const { fileExtension } = require('../../path-utils');
const EventEmitter = require('events');
@ -76,7 +80,7 @@ export default class InteropService {
description: _('Evernote Export File (as HTML)'),
supportsMobile: false,
outputFormat: ImportModuleOutputFormat.Html,
}, dynamicRequireModuleFactory('./InteropService_Importer_EnexToHtml')),
}, () => new InteropService_Importer_EnexToHtml()),
makeImportModule({
format: 'enex',
@ -85,7 +89,7 @@ export default class InteropService {
description: _('Evernote Export File (as Markdown)'),
supportsMobile: false,
isDefault: true,
}, dynamicRequireModuleFactory('./InteropService_Importer_EnexToMd')),
}, () => new InteropService_Importer_EnexToMd()),
makeImportModule({
format: 'enex',
@ -94,7 +98,7 @@ export default class InteropService {
description: _('Evernote Export Files (Directory, as HTML)'),
supportsMobile: false,
outputFormat: ImportModuleOutputFormat.Html,
}, dynamicRequireModuleFactory('./InteropService_Importer_EnexToHtml')),
}, () => new InteropService_Importer_EnexToHtml()),
makeImportModule({
format: 'enex',
@ -102,7 +106,7 @@ export default class InteropService {
sources: [FileSystemItem.Directory],
description: _('Evernote Export Files (Directory, as Markdown)'),
supportsMobile: false,
}, dynamicRequireModuleFactory('./InteropService_Importer_EnexToMd')),
}, () => new InteropService_Importer_EnexToMd()),
makeImportModule({
format: 'html',
@ -142,7 +146,7 @@ export default class InteropService {
sources: [FileSystemItem.File],
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
description: _('OneNote Notebook'),
}, dynamicRequireModuleFactory('./InteropService_Importer_OneNote')),
}, () => new InteropService_Importer_OneNote()),
];
const exportModules = [
@ -178,14 +182,14 @@ export default class InteropService {
isNoteArchive: false,
description: _('HTML File'),
supportsMobile: false,
}, dynamicRequireModuleFactory('./InteropService_Exporter_Html')),
}, () => new InteropService_Exporter_Html()),
makeExportModule({
format: ExportModuleOutputFormat.Html,
target: FileSystemItem.Directory,
description: _('HTML Directory'),
supportsMobile: false,
}, dynamicRequireModuleFactory('./InteropService_Exporter_Html')),
}, () => new InteropService_Exporter_Html()),
];
this.defaultModules_ = (importModules as Module[]).concat(exportModules);

View File

@ -9,7 +9,7 @@ import { MarkupToHtml } from '@joplin/renderer';
import { NoteEntity, ResourceEntity, ResourceLocalStateEntity } from '../database/types';
import { contentScriptsToRendererRules } from '../plugins/utils/loadContentScripts';
import { basename, friendlySafeFilename, rtrimSlashes, dirname } from '../../path-utils';
import htmlpack from '@joplin/htmlpack';
import packToString from '@joplin/htmlpack/packToString';
const { themeStyle } = require('../../theme');
const { escapeHtml } = require('../../string-utils.js');
import { assetsToHeaders } from '@joplin/renderer';
@ -19,6 +19,7 @@ import Logger from '@joplin/utils/Logger';
import { parseRenderedNoteMetadata } from './utils';
import ResourceLocalState from '../../models/ResourceLocalState';
import { ResourceInfos } from '@joplin/renderer/types';
import { fromFilename } from '../../mime-utils';
const logger = Logger.create('InteropService_Exporter_Html');
@ -138,13 +139,11 @@ export default class InteropService_Exporter_Html extends InteropService_Exporte
if (metadata.printTitle && item.title) noteContent.push(`<div class="exported-note-title">${escapeHtml(item.title)}</div>`);
if (result.html) noteContent.push(result.html);
const libRootPath = dirname(dirname(__dirname));
// We need to export all the plugin assets too and refer them from the header
// The source path is a bit hard-coded but shouldn't change.
for (let i = 0; i < result.pluginAssets.length; i++) {
const asset = result.pluginAssets[i];
const filePath = asset.pathIsAbsolute ? asset.path : `${libRootPath}/node_modules/@joplin/renderer/assets/${asset.name}`;
const filePath = asset.pathIsAbsolute ? asset.path : `${Setting.value('pluginAssetDir')}/${asset.name}`;
if (!(await shim.fsDriver().exists(filePath))) {
logger.warn(`File does not exist and cannot be exported: ${filePath}`);
} else {
@ -175,8 +174,12 @@ export default class InteropService_Exporter_Html extends InteropService_Exporte
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public async processResource(resource: ResourceEntity, filePath: string) {
if (!this.resourceDir_) return;
if (!await shim.fsDriver().exists(this.resourceDir_)) {
await shim.fsDriver().mkdir(this.resourceDir_);
}
const destResourcePath = `${this.resourceDir_}/${basename(filePath)}`;
await shim.fsDriver().copy(filePath, destResourcePath);
const localState: ResourceLocalStateEntity = await ResourceLocalState.load(resource.id);
@ -188,10 +191,37 @@ export default class InteropService_Exporter_Html extends InteropService_Exporte
public async close() {
if (this.packIntoSingleFile_) {
const tempFilePath = `${this.filePath_}.tmp`;
await shim.fsDriver().move(this.filePath_, tempFilePath);
await htmlpack(tempFilePath, this.filePath_);
await shim.fsDriver().remove(tempFilePath);
const mainHtml = await shim.fsDriver().readFile(this.filePath_, 'utf8');
const resolveToAllowedDir = (path: string) => {
// TODO: Enable this for all platforms -- at present, this is mobile-only.
const restrictToDestDir = !!shim.mobilePlatform();
if (restrictToDestDir) {
return shim.fsDriver().resolveRelativePathWithinDir(this.destDir_, path);
} else {
return shim.fsDriver().resolve(this.destDir_, path);
}
};
const packedHtml = await packToString(
this.destDir_,
mainHtml,
{
exists: (path) => {
path = resolveToAllowedDir(path);
return shim.fsDriver().exists(path);
},
readFileDataUri: async (path) => {
path = resolveToAllowedDir(path);
const mimeType = fromFilename(path);
const content = await shim.fsDriver().readFile(path, 'base64');
return `data:${mimeType};base64,${content}`;
},
readFileText: (path) => {
path = resolveToAllowedDir(path);
return shim.fsDriver().readFile(path, 'utf8');
},
},
);
await shim.fsDriver().writeFile(this.filePath_, packedHtml, 'utf8');
for (const d of this.createdDirs_) {
await shim.fsDriver().remove(d);

View File

@ -3,7 +3,6 @@ import { ImportExportResult, ImportModuleOutputFormat, ImportOptions } from './t
import InteropService_Importer_Base from './InteropService_Importer_Base';
import { NoteEntity } from '../database/types';
import { rtrimSlashes } from '../../path-utils';
import * as AdmZip from 'adm-zip';
import InteropService_Importer_Md from './InteropService_Importer_Md';
import { join, resolve, normalize, sep, dirname } from 'path';
import Logger from '@joplin/utils/Logger';
@ -45,11 +44,9 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
public async exec(result: ImportExportResult) {
const sourcePath = rtrimSlashes(this.sourcePath_);
const unzipTempDirectory = await this.temporaryDirectory_(true);
const zip = new AdmZip(sourcePath);
logger.info('Unzipping files...');
zip.extractAllTo(unzipTempDirectory, false);
const files = await shim.fsDriver().zipExtract({ source: sourcePath, extractTo: unzipTempDirectory });
const files = zip.getEntries();
if (files.length === 0) {
result.warnings.push('Zip file has no files.');
return result;
@ -60,7 +57,7 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
const notebookBaseDir = join(unzipTempDirectory, baseFolder, sep);
const outputDirectory2 = join(tempOutputDirectory, baseFolder);
const notebookFiles = zip.getEntries().filter(e => e.name !== '.onetoc2' && e.name !== 'OneNote_RecycleBin.onetoc2');
const notebookFiles = files.filter(e => e.name !== '.onetoc2' && e.name !== 'OneNote_RecycleBin.onetoc2');
const { oneNoteConverter } = shim.requireDynamic('@joplin/onenote-converter');
logger.info('Extracting OneNote to HTML');

View File

@ -1,5 +1,4 @@
import { _ } from '../../locale';
import shim from '../../shim';
import InteropService_Exporter_Base from './InteropService_Exporter_Base';
import InteropService_Importer_Base from './InteropService_Importer_Base';
import { ExportModuleOutputFormat, ExportOptions, FileSystemItem, ImportModuleOutputFormat, ImportOptions, ModuleType } from './types';
@ -126,16 +125,5 @@ export const makeExportModule = (
};
};
// A module factory that uses dynamic requires.
// TODO: This is currently only used because some importers/exporters import libraries that
// don't work on mobile (e.g. htmlpack or fs). These importers/exporters should be migrated
// to fs so that this can be removed.
export const dynamicRequireModuleFactory = (fileName: string) => {
return () => {
const ModuleClass = shim.requireDynamic(fileName).default;
return new ModuleClass();
};
};
type Module = ImportModule|ExportModule;
export default Module;

View File

@ -68,6 +68,7 @@ import OcrService from '../services/ocr/OcrService';
import { createWorker } from 'tesseract.js';
import { reg } from '../registry';
import { Store } from 'redux';
import { dirname } from '@joplin/utils/path';
import SyncTargetJoplinServerSAML from '../SyncTargetJoplinServerSAML';
// Each suite has its own separate data and temp directory so that multiple
@ -198,6 +199,7 @@ Setting.setConstant('tempDir', baseTempDir);
Setting.setConstant('cacheDir', baseTempDir);
Setting.setConstant('resourceDir', baseTempDir);
Setting.setConstant('pluginDataDir', `${profileDir}/profile/plugin-data`);
Setting.setConstant('pluginAssetDir', `${dirname(require.resolve('@joplin/renderer'))}/assets`);
Setting.setConstant('profileDir', profileDir);
Setting.setConstant('rootProfileDir', rootProfileDir);
Setting.setConstant('env', Env.Dev);

View File

@ -179,4 +179,7 @@ Haverbeke
unfocusable
unlocker
Tiktok
topagency
topagency
esbuild
mapbox
outfile

View File

@ -7,9 +7,7 @@
"publishConfig": {
"access": "public"
},
"browser": {
"jsdom": false
},
"browser": "lib/turndown.browser.cjs.js",
"dependencies": {
"@adobe/css-tools": "4.4.2",
"html-entities": "1.4.0",
@ -41,12 +39,12 @@
},
"scripts": {
"build-all": "npm run build-cjs && npm run build-es && npm run build-umd && npm run build-iife",
"build": "rollup -c config/rollup.config.cjs.mjs",
"build-cjs": "rollup -c config/rollup.config.cjs.mjs && rollup -c config/rollup.config.browser.cjs.mjs",
"build-es": "rollup -c config/rollup.config.es.mjs && rollup -c config/rollup.config.browser.es.mjs",
"build-umd": "rollup -c config/rollup.config.umd.mjs && rollup -c config/rollup.config.browser.umd.mjs",
"build-iife": "rollup -c config/rollup.config.iife.mjs",
"build-test": "browserify test/turndown-test.js --outfile test/turndown-test.browser.js",
"build": "npm run build-cjs",
"prepare": "npm run build"
},
"gitHead": "05a29b450962bf05a8642bbd39446a1f679a96ba"

View File

@ -0,0 +1,27 @@
# Desktop app bundling
For performance and to reduce the application size, the desktop app is bundled with [esbuild](https://esbuild.github.io/). Bundling packs most of the desktop application's JavaScript into one or two JavaScript files. This occurs as a part of both `yarn dist` and `yarn start`.
## Why bundle the app?
- **Performance**: Bundling the app [is recommended by the Electron performance guide](https://www.electronjs.org/docs/latest/tutorial/performance#7-bundle-your-code). The guide states that "Loading modules is a surprisingly expensive operation, especially on Windows." Bundling the application reduces the number of `require` calls.
- **Application size**: Bundling can reduce the size of the app created by `yarn dist`.
## How does bundling reduce the application size?
Bundling allows both:
1. Reducing the size of the JavaScript included in the app through [minification](https://esbuild.github.io/api/#minify), and
2. Reducing the number of dependencies included in `node_modules` in the version of the app built with `electron-builder`. Dependencies often include files unnecessary for the final build (e.g. README images, test files).
## Excluding dependencies from `node_modules`
After bundling the app, most dependencies are completely included within `main.bundle.js` or `main-html.bundle.js`. As a result, the copies of these dependencies in `node_modules` are completely unused.
Some dependencies need to be included in `node_modules` at runtime. This is the case, for example, if the dependency needs to be required with `shim.requireDynamic`, or if the dependency includes native `.node` assets that can't be bundled. For example, `sqlite3` includes native `.node` assets that need to be in `node_modules` at runtime.
Electron-builder can be instructed to exclude dependencies from the built application [by moving them to `devDependencies`](https://github.com/electron-userland/electron-builder/blob/84657680ba5688f1594bc77be3df5c2c78125723/README.md?plain=1#L73). A dependency should only be in the production `dependencies` if it needs to be included in `node_modules` at runtime.
## Determining what contributes to the bundle size
To see what contributes to the size of the application's bundled JavaScript, consider using [esbuild's bundle size analyzer](https://esbuild.github.io/analyze/). The size analyzer accepts esbuild metafiles. To build these metafiles, manually run `yarn bundle`. Bundle metadata will be written to the `app-desktop` directory as `.meta.json` files.

View File

@ -2,6 +2,7 @@
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"esModuleInterop": false,
//"lib": ["es2015", "es2020.string", "dom", "dom.iterable"],
"alwaysStrict": true,
"forceConsistentCasingInFileNames": true,

267
yarn.lock
View File

@ -7384,6 +7384,181 @@ __metadata:
languageName: node
linkType: hard
"@esbuild/aix-ppc64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/aix-ppc64@npm:0.25.3"
conditions: os=aix & cpu=ppc64
languageName: node
linkType: hard
"@esbuild/android-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/android-arm64@npm:0.25.3"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
"@esbuild/android-arm@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/android-arm@npm:0.25.3"
conditions: os=android & cpu=arm
languageName: node
linkType: hard
"@esbuild/android-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/android-x64@npm:0.25.3"
conditions: os=android & cpu=x64
languageName: node
linkType: hard
"@esbuild/darwin-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/darwin-arm64@npm:0.25.3"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@esbuild/darwin-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/darwin-x64@npm:0.25.3"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@esbuild/freebsd-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/freebsd-arm64@npm:0.25.3"
conditions: os=freebsd & cpu=arm64
languageName: node
linkType: hard
"@esbuild/freebsd-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/freebsd-x64@npm:0.25.3"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
"@esbuild/linux-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-arm64@npm:0.25.3"
conditions: os=linux & cpu=arm64
languageName: node
linkType: hard
"@esbuild/linux-arm@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-arm@npm:0.25.3"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@esbuild/linux-ia32@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-ia32@npm:0.25.3"
conditions: os=linux & cpu=ia32
languageName: node
linkType: hard
"@esbuild/linux-loong64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-loong64@npm:0.25.3"
conditions: os=linux & cpu=loong64
languageName: node
linkType: hard
"@esbuild/linux-mips64el@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-mips64el@npm:0.25.3"
conditions: os=linux & cpu=mips64el
languageName: node
linkType: hard
"@esbuild/linux-ppc64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-ppc64@npm:0.25.3"
conditions: os=linux & cpu=ppc64
languageName: node
linkType: hard
"@esbuild/linux-riscv64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-riscv64@npm:0.25.3"
conditions: os=linux & cpu=riscv64
languageName: node
linkType: hard
"@esbuild/linux-s390x@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-s390x@npm:0.25.3"
conditions: os=linux & cpu=s390x
languageName: node
linkType: hard
"@esbuild/linux-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-x64@npm:0.25.3"
conditions: os=linux & cpu=x64
languageName: node
linkType: hard
"@esbuild/netbsd-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/netbsd-arm64@npm:0.25.3"
conditions: os=netbsd & cpu=arm64
languageName: node
linkType: hard
"@esbuild/netbsd-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/netbsd-x64@npm:0.25.3"
conditions: os=netbsd & cpu=x64
languageName: node
linkType: hard
"@esbuild/openbsd-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/openbsd-arm64@npm:0.25.3"
conditions: os=openbsd & cpu=arm64
languageName: node
linkType: hard
"@esbuild/openbsd-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/openbsd-x64@npm:0.25.3"
conditions: os=openbsd & cpu=x64
languageName: node
linkType: hard
"@esbuild/sunos-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/sunos-x64@npm:0.25.3"
conditions: os=sunos & cpu=x64
languageName: node
linkType: hard
"@esbuild/win32-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/win32-arm64@npm:0.25.3"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@esbuild/win32-ia32@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/win32-ia32@npm:0.25.3"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@esbuild/win32-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/win32-x64@npm:0.25.3"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0":
version: 4.4.0
resolution: "@eslint-community/eslint-utils@npm:4.4.0"
@ -8727,6 +8902,7 @@ __metadata:
"@joplin/default-plugins": ~3.4
"@joplin/editor": ~3.4
"@joplin/lib": ~3.4
"@joplin/onenote-converter": ~3.4
"@joplin/renderer": ~3.4
"@joplin/tools": ~3.4
"@joplin/utils": ~3.4
@ -8752,6 +8928,7 @@ __metadata:
electron-builder: 24.13.3
electron-updater: 6.2.1
electron-window-state: 5.0.3
esbuild: ^0.25.3
formatcoords: 1.1.3
fs-extra: 11.2.0
glob: 10.4.5
@ -24052,6 +24229,92 @@ __metadata:
languageName: node
linkType: hard
"esbuild@npm:^0.25.3":
version: 0.25.3
resolution: "esbuild@npm:0.25.3"
dependencies:
"@esbuild/aix-ppc64": 0.25.3
"@esbuild/android-arm": 0.25.3
"@esbuild/android-arm64": 0.25.3
"@esbuild/android-x64": 0.25.3
"@esbuild/darwin-arm64": 0.25.3
"@esbuild/darwin-x64": 0.25.3
"@esbuild/freebsd-arm64": 0.25.3
"@esbuild/freebsd-x64": 0.25.3
"@esbuild/linux-arm": 0.25.3
"@esbuild/linux-arm64": 0.25.3
"@esbuild/linux-ia32": 0.25.3
"@esbuild/linux-loong64": 0.25.3
"@esbuild/linux-mips64el": 0.25.3
"@esbuild/linux-ppc64": 0.25.3
"@esbuild/linux-riscv64": 0.25.3
"@esbuild/linux-s390x": 0.25.3
"@esbuild/linux-x64": 0.25.3
"@esbuild/netbsd-arm64": 0.25.3
"@esbuild/netbsd-x64": 0.25.3
"@esbuild/openbsd-arm64": 0.25.3
"@esbuild/openbsd-x64": 0.25.3
"@esbuild/sunos-x64": 0.25.3
"@esbuild/win32-arm64": 0.25.3
"@esbuild/win32-ia32": 0.25.3
"@esbuild/win32-x64": 0.25.3
dependenciesMeta:
"@esbuild/aix-ppc64":
optional: true
"@esbuild/android-arm":
optional: true
"@esbuild/android-arm64":
optional: true
"@esbuild/android-x64":
optional: true
"@esbuild/darwin-arm64":
optional: true
"@esbuild/darwin-x64":
optional: true
"@esbuild/freebsd-arm64":
optional: true
"@esbuild/freebsd-x64":
optional: true
"@esbuild/linux-arm":
optional: true
"@esbuild/linux-arm64":
optional: true
"@esbuild/linux-ia32":
optional: true
"@esbuild/linux-loong64":
optional: true
"@esbuild/linux-mips64el":
optional: true
"@esbuild/linux-ppc64":
optional: true
"@esbuild/linux-riscv64":
optional: true
"@esbuild/linux-s390x":
optional: true
"@esbuild/linux-x64":
optional: true
"@esbuild/netbsd-arm64":
optional: true
"@esbuild/netbsd-x64":
optional: true
"@esbuild/openbsd-arm64":
optional: true
"@esbuild/openbsd-x64":
optional: true
"@esbuild/sunos-x64":
optional: true
"@esbuild/win32-arm64":
optional: true
"@esbuild/win32-ia32":
optional: true
"@esbuild/win32-x64":
optional: true
bin:
esbuild: bin/esbuild
checksum: 1f9af51aa1d7d1f57e7294823d19ed69b0f6da413b7b0e8123abcebd1bb4011ef19961e2e6679c07301fcd00a85c4d102160fc40a91c25ceeaf594932509d84d
languageName: node
linkType: hard
"escalade@npm:^3.1.1":
version: 3.1.1
resolution: "escalade@npm:3.1.1"
@ -37986,7 +38249,7 @@ __metadata:
"pdfjs-dist@patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch::locator=root%40workspace%3A.":
version: 3.11.174
resolution: "pdfjs-dist@patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch::version=3.11.174&hash=4ecadb&locator=root%40workspace%3A."
resolution: "pdfjs-dist@patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch::version=3.11.174&hash=d35ec8&locator=root%40workspace%3A."
dependencies:
path2d-polyfill: ^2.0.1
dependenciesMeta:
@ -37994,7 +38257,7 @@ __metadata:
optional: true
path2d-polyfill:
optional: true
checksum: bc7597789b13b3ea59ee4ff29db40a15f0a20094ef8f6fa9ba59a80db0501a9b94d0fb3602515666057ebe9c92e59d938ac157f4d2852a125ccc1511c9d8adf6
checksum: aef2f87e478a66541311228716cd91834d4c83320d0fbf9bb0fcbb427f503c125edfb6c5def77d2d4c57db27b92de652752783b50c3edb4a059c4d2b84e1c913
languageName: node
linkType: hard