import type * as React from 'react'; import type * as ReactDom from 'react-dom'; import type { NoteEntity, ResourceEntity } from './services/database/types'; import type FsDriverBase from './fs-driver-base'; import type FileApiDriverLocal from './file-api-driver-local'; import type { Crypto } from './services/e2ee/types'; import type { MarkupLanguage } from '@joplin/renderer'; import { FetchBlobOptions } from './types'; export interface CreateResourceFromPathOptions { resizeLargeImages?: 'always' | 'never' | 'ask'; userSideValidation?: boolean; destinationResourceId?: string; } export interface CreatePdfFromImagesOptions { minPage?: number; maxPage?: number; scaleFactor?: number; } export interface PdfInfo { pageCount: number; } export interface PdfPageImage { path: string; width: number; height: number; } export interface Keytar { setPassword(key: string, client: string, password: string): Promise; getPassword(key: string, client: string): Promise; deletePassword(key: string, client: string): Promise; } export interface FetchOptions { method?: string; headers?: Record; body?: string; agent?: unknown; signal?: AbortSignal; } interface AttachFileToNoteOptions { resizeLargeImages?: 'always'|'never'; position?: number; markupLanguage?: MarkupLanguage; } export enum MessageBoxType { Confirm = 'question', Error = 'error', Info = 'info', } export interface ShowMessageBoxOptions { title?: string; buttons?: string[]; type?: MessageBoxType; defaultId?: number; cancelId?: number; } export enum ToastType { Info = 'info', Error = 'error', Success = 'success', } export interface ShowToastOptions { type: ToastType; } export enum MobilePlatform { None = '', Android = 'android', Ios = 'ios', Web = 'web', } export type SetClientCertificateOptions = { certPath: string; keyPath: string; keyPassword: string; domains: string[]; }; export interface HttpAgentOptions { timeout?: number; } let isTestingEnv_ = false; // We need to ensure that there's only one instance of React being used by all // the packages. In particular, the lib might need React to define generic // hooks, but it shouldn't have React in its dependencies as that would cause // the following error: // // https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react // // So instead, the **applications** include React as a dependency, then pass it // to any other packages using the shim. Essentially, only one package should // require React, and in our case that should be one of the applications // (app-desktop, app-mobile, etc.) since we are sure they won't be dependency to // other packages (unlike the lib which can be included anywhere). // // Regarding the type - although we import React, we only use it as a type // using `typeof React`. This is just to get types in hooks. // // https://stackoverflow.com/a/42816077/561309 let react_: typeof React = null; let reactDom_: typeof ReactDom = null; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- node sqlite driver is set per-platform (better-sqlite3, react-native sqlite, etc.); accessed structurally let nodeSqlite_: any = null; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- sqlite-vec is only bundled with desktop; null on platforms that don't ship it let sqliteVec_: any = null; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- onnxruntime-node is only bundled with desktop; null on platforms (mobile/CLI/web) that don't ship it let onnxRuntime_: any = null; const shim = { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Geolocation API differs across platforms (browser Geolocation, RN Geolocation module); accessed structurally Geolocation: null as any, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- electron bridge is set in app-desktop; its shape is concrete there but lib references it structurally electronBridge_: null as any, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- fsDriver set per-platform (node/RN/web); FsDriverBase subclasses expose additional methods accessed structurally fsDriver_: null as any, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- http agent set per-platform (node https.Agent, RN noop); accessed structurally httpAgent_: null as any, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- See electronBridge_ electronBridge: (): any => { throw new Error('Not implemented: electronBridge'); }, msleep_: (ms: number) => { return new Promise((resolve) => { shim.setTimeout(() => { resolve(); }, ms); }); }, isNode: () => { if (typeof process === 'undefined') return false; if (shim.isElectron()) return true; return !shim.mobilePlatform(); }, isReactNative: () => { if (typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('ReactNativeDebugger') >= 0) { return true; } return !shim.isNode(); }, isLinux: () => { return typeof process !== 'undefined' && process.platform === 'linux'; }, isGNOME: () => { if ((!shim.isLinux() && !shim.isFreeBSD()) || !process) { return false; } const currentDesktop = process.env['XDG_CURRENT_DESKTOP'] ?? ''; // XDG_CURRENT_DESKTOP may be something like "ubuntu:GNOME" and not just "GNOME". // Thus, we use .includes and not ===. if (currentDesktop.includes('GNOME')) { return true; } // On Ubuntu, "XDG_CURRENT_DESKTOP=ubuntu:GNOME" is replaced with "Unity" and // ORIGINAL_XDG_CURRENT_DESKTOP stores the original desktop. const originalCurrentDesktop = process.env['ORIGINAL_XDG_CURRENT_DESKTOP'] ?? ''; if (originalCurrentDesktop.includes('GNOME')) { return true; } return false; }, isFreeBSD: () => { return typeof process !== 'undefined' && process.platform === 'freebsd'; }, isWindows: () => { return typeof process !== 'undefined' && process.platform === 'win32'; }, isMac: () => { return typeof process !== 'undefined' && process.platform === 'darwin'; }, // Tells whether the computer **CPU** is an Apple Silicon (not whether the running version was // built for ARM64) isAppleSilicon: (): boolean => { throw new Error('Not implemented: isAppleSilicon'); }, platformName: () => { if (shim.isReactNative()) return shim.mobilePlatform(); if (shim.isMac()) return 'darwin'; if (shim.isWindows()) return 'win32'; if (shim.isLinux()) return 'linux'; if (shim.isFreeBSD()) return 'freebsd'; if (typeof process !== 'undefined' && process.platform) return process.platform; throw new Error('Cannot determine platform'); }, // Tells the computer CPU architecture. Which if different from the architecture the running // version was built for. For example, the laptop CPU may be an ARM64, while the version was // built for x64 architecture. Here we want to know the laptop CPU. platformArch: (): string => { throw new Error('Not implemented: platformArch'); }, deviceString: () => { const output: string[] = []; output.push(shim.platformName()); if (shim.platformArch()) output.push(shim.platformArch()); return output.join(', '); }, // "ios" or "android", or "" if not on mobile mobilePlatform: (): MobilePlatform => { return MobilePlatform.None; // Default if we're not on mobile (React Native) }, // https://github.com/cheton/is-electron isElectron: () => { // Renderer process if (typeof window !== 'undefined' && typeof window.process === 'object' && (window.process as { type?: string }).type === 'renderer') { return true; } // Main process if (typeof process !== 'undefined' && typeof process.versions === 'object' && !!(process.versions as NodeJS.ProcessVersions & { electron?: string }).electron) { return true; } // Detect the user agent when the `nodeIntegration` option is set to true if (typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0) { return true; } return false; }, isPortable: (): boolean => { return typeof process !== 'undefined' && typeof process.env === 'object' && !!process.env.PORTABLE_EXECUTABLE_DIR; }, // Node requests can go wrong is so many different ways and with so // many different error messages... This handler inspects the error // and decides whether the request can safely be repeated or not. fetchRequestCanBeRetried: (error: { code?: string; message?: string } | null) => { if (!error) return false; // Unfortunately the error 'Network request failed' doesn't have a type // or error code, so hopefully that message won't change and is not localized if (error.message === 'Network request failed') return true; // request to https://public-ch3302....1fab24cb1bd5f.md failed, reason: socket hang up" if (error.code === 'ECONNRESET') return true; // OneDrive (or Node?) sometimes sends back a "not found" error for resources // that definitely exist and in this case repeating the request works. // Error is: // request to https://graph.microsoft.com/v1.0/drive/special/approot failed, reason: getaddrinfo ENOTFOUND graph.microsoft.com graph.microsoft.com:443 // // 2024-04-07: Strictly speaking we shouldn't repeat the request if the resource doesn't // exist. Hopefully OneDrive has now fixed this issue and the hack is no longer necessary. // // (error.code === 'ENOTFOUND') return true; // network timeout at: https://public-ch3302...859f9b0e3ab.md if (error.message && error.message.indexOf('network timeout') === 0) return true; // name: 'FetchError', // message: 'request to https://api.ipify.org/?format=json failed, reason: getaddrinfo EAI_AGAIN api.ipify.org:443', // type: 'system', // errno: 'EAI_AGAIN', // code: 'EAI_AGAIN' } } reason: { FetchError: request to https://api.ipify.org/?format=json failed, reason: getaddrinfo EAI_AGAIN api.ipify.org:443 // // It's a Microsoft error: "A temporary failure in name resolution occurred." if (error.code === 'EAI_AGAIN') return true; // request to https://public-...8fd8bc6bb68e9c4d17a.md failed, reason: connect ETIMEDOUT 204.79.197.213:443 // Code: ETIMEDOUT if (error.code === 'ETIMEDOUT') return true; // Failure to establish a connection // See https://undici.nodejs.org/api/Errors#class-connecttimeouterror if (error.code === 'UND_ERR_CONNECT_TIMEOUT') return true; // ECONNREFUSED is generally temporary if (error.code === 'ECONNREFUSED') return true; return false; }, fetchMaxRetry_: 5, fetchMaxRetrySet: (v: number) => { const previous = shim.fetchMaxRetry_; shim.fetchMaxRetry_ = v; return previous; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Used with various fetch-like functions (shim.fetch, shim.fetchBlob, custom wrappers); return shapes vary; options widened to FetchOptions because callers pass through their request options bag fetchWithRetry: async function(fetchFn: ()=> Promise, options: any = null) { if (!options) options = {}; if (!options.timeout) options.timeout = 1000 * 120; // ms if (!('maxRetry' in options)) options.maxRetry = shim.fetchMaxRetry_; let retryCount = 0; while (true) { try { const response = await fetchFn(); return response; } catch (error) { if (shim.fetchRequestCanBeRetried(error)) { retryCount++; if (retryCount > options.maxRetry) throw error; await shim.msleep_(retryCount * 3000); } else { throw error; } } } }, fetch: (_url: string, _options: FetchOptions|null = null): Promise => { throw new Error('Not implemented: fetch'); }, debugFetch: (_url: string, _options: FetchOptions|null): Promise => { throw new Error('Not implemented: debugFetch'); }, fetchText: async (url: string, options: FetchOptions | null = null): Promise => { const r = await shim.fetch(url, options || {}); if (!r.ok) throw new Error(`Could not fetch ${url}`); return r.text(); }, createResourceFromPath: async (_filePath: string, _defaultProps: ResourceEntity = null, _options: CreateResourceFromPathOptions = null): Promise => { throw new Error('Not implemented: createResourceFromPath'); }, FormData: typeof FormData !== 'undefined' ? FormData : null, fsDriver: (): FsDriverBase => { throw new Error('Not implemented: fsDriver'); }, sharpEnabled: (): boolean => { return true; }, FileApiDriverLocal: null as typeof FileApiDriverLocal, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- node returns string (sync), RN returns Promise; widening across both readLocalFileBase64: (_path: string): any => { throw new Error('Not implemented: readLocalFileBase64'); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- options/return flow through node-fetch and platform-specific implementations uploadBlob: (_url: string, _options: any): any => { throw new Error('Not implemented: uploadBlob'); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- sjcl module set per-platform; accessed structurally sjclModule: null as any, crypto: null as Crypto, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- node returns Buffer, browser/RN returns unknown[]; widening would cascade to EncryptionService and other callers randomBytes: async (_count: number): Promise => { throw new Error('Not implemented: randomBytes'); }, stringByteLength: (_s: string): number => { throw new Error('Not implemented: stringByteLength'); }, // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- node implementation accepts a Setting class argument; lib references it as a generic Function detectAndSetLocale: null as Function, attachFileToNote: async (_note: NoteEntity, _filePath: string, _options?: AttachFileToNoteOptions): Promise => { throw new Error('Not implemented: attachFileToNote'); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- options bag with markupLanguage/createFileURL/markdownLinkUrl and other route-specific keys attachFileToNoteBody: async (_body: string, _filePath: string, _position: number, _options: any): Promise => { throw new Error('Not implemented: attachFileToNoteBody'); }, imageToDataUrl: async (_filePath: string, _maxSize = 0): Promise => { throw new Error('Not implemented: imageToDataUrl'); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- options is { cropRect?, resize? } passed through to the native image module; varies per platform imageFromDataUrl: async (_imageDataUrl: string, _filePath: string, _options: any = null): Promise => { throw new Error('Not implemented: imageFromDataUrl'); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- options flow into FetchBlobOptions; return shape varies (node fetch Response vs RN blob) fetchBlob: function(_url: string, _options: FetchBlobOptions = null): any { throw new Error('Not implemented: fetchBlob'); }, // Does not do OCR -- just extracts existing text from a PDF. pdfExtractEmbeddedText: async (_pdfPath: string): Promise => { throw new Error('Not implemented: textFromPdf'); }, pdfToImages: async (_pdfPath: string, _outputDirectoryPath: string, _options?: CreatePdfFromImagesOptions): Promise => { throw new Error('Not implemented: pdfToImages'); }, // Like pdfToImages but also returns the dimensions of each page image pdfToImagesWithDimensions: async (_pdfPath: string, _outputDirectoryPath: string, _options?: CreatePdfFromImagesOptions): Promise => { throw new Error('Not implemented: pdfToImagesWithDimensions'); }, pdfInfo: async (_pdfPath: string): Promise => { throw new Error('Not implemented: pdfInfo'); }, createAccessiblePdf: async (_originalPdfPath: string, _ocrDetails: string, _outputPath: string, _tempDir: string): Promise => { throw new Error('Not implemented: createAccessiblePdf'); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- node returns boolean (shell.openExternal), RN returns Promise (Linking.openURL) openUrl: (_url: string): any => { throw new Error('Not implemented: openUrl'); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- See httpAgent_ above httpAgent: (_url: string, _options?: HttpAgentOptions): any => { throw new Error('Not implemented: httpAgent'); }, openOrCreateFile: (_path: string, _defaultContents: string): string => { throw new Error('Not implemented: openOrCreateFile'); }, waitForFrame: (): void => { throw new Error('Not implemented: waitForFrame'); }, appVersion: (): string => { throw new Error('Not implemented: appVersion'); }, injectedJs: (_name: string) => '', injectedCss: (_name: string) => '', isTestingEnv: () => { return isTestingEnv_; }, setIsTestingEnv: (v: boolean) => { isTestingEnv_ = v; }, pathRelativeToCwd: (_path: string): string => { throw new Error('Not implemented'); }, // Returns the index of the button that was clicked. By default, // 0 -> OK // 1 -> Cancel showMessageBox: (_message: string, _options: ShowMessageBoxOptions = null): Promise => { throw new Error('Not implemented'); }, showErrorDialog: async (message: string): Promise => { await shim.showMessageBox(message, { type: MessageBoxType.Error }); }, showConfirmationDialog: async (message: string): Promise => { return await shim.showMessageBox(message, { type: MessageBoxType.Confirm }) === 0; }, showToast: async (message: string, { type = ToastType.Info }: ShowToastOptions = null): Promise => { // Should usually be overridden by implementers await shim.showMessageBox(message, { type: type === ToastType.Error ? MessageBoxType.Error : MessageBoxType.Info }); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- image is a NativeImage (Electron) or an equivalent platform object; format is e.g. 'image/png' or 'image/jpeg' writeImageToFile: (_image: any, _format: string, _filePath: string): Promise => { throw new Error('Not implemented'); }, restartApp: (): void|Promise => { throw new Error('Not implemented'); }, // setTimeout/setInterval are in the shim so that in Electron renderer process // we can force the use of the Node timers from the "timers" package. This is to go around // a bug that happened while testing plugins (on WSL2 / X-Server, so same as Linux): // // - The plugin would change some text in the editor // - The editor would schedule a save using setTimeout (via AsyncActionQueue) // - The timer would never get fired (although setTimeout was definitely called) // // It's not clear whether it is due to the code path originating in the plugin script or // some other bugs. But in any case, the issue was fixed using require('timers').setTimeout // instead of the default window.setTimeout. Might be related to this Electron bug: // https://github.com/electron/electron/issues/25311 // (although changing allowRendererProcessReuse solved nothing) // // These unreliable timers might also be the reason for hard to replicate bugs in file watching // or the synchronisation freeze bug on Linux. // // Having the timers wrapped in that way would also make it easier to debug timing issue and // find out what timers have been fired or not. // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Return type varies by platform: node returns NodeJS.Timeout, browsers return number setTimeout: (_fn: ()=> void, _interval: number): any=> { throw new Error('Not implemented'); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- See setTimeout above setInterval: (_fn: ()=> void, _interval: number): any=> { throw new Error('Not implemented'); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Id type matches setTimeout/setInterval return; varies by platform clearTimeout: (_id: any): void => { throw new Error('Not implemented'); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- See clearTimeout clearInterval: (_id: any): void => { throw new Error('Not implemented'); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- See nodeSqlite_ setNodeSqlite: (nodeSqlite: any) => { nodeSqlite_ = nodeSqlite; }, nodeSqlite: () => { if (!nodeSqlite_) throw new Error('Trying to access nodeSqlite before it has been set!!!'); return nodeSqlite_; }, // sqlite-vec is only bundled with the desktop app. Other platforms (CLI, // mobile, web) leave it unset and the embeddings index gracefully reports // itself as unavailable rather than crashing. // eslint-disable-next-line @typescript-eslint/no-explicit-any -- See sqliteVec_ setSqliteVec: (sqliteVec: any) => { sqliteVec_ = sqliteVec; }, sqliteVec: () => { return sqliteVec_; }, // onnxruntime-node powers the bundled local embedding model. Only the // desktop app installs it; other platforms (CLI, mobile, web) leave it // unset and the embedding indexer reports the local provider as // unavailable rather than crashing. // eslint-disable-next-line @typescript-eslint/no-explicit-any -- See onnxRuntime_ setOnnxRuntime: (onnxRuntime: any) => { onnxRuntime_ = onnxRuntime; }, onnxRuntime: () => { return onnxRuntime_; }, setReact: (react: typeof React) => { react_ = react; }, react: () => { if (!react_) throw new Error('Trying to access React before it has been set!!!'); return react_; }, setReactDom: (reactDom: typeof ReactDom) => { reactDom_ = reactDom; }, reactDom: () => { if (!reactDom_) throw new Error('Trying to access react-dom before it has been set!!! Is this a browser environment?'); return reactDom_; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Returns node's `dgram` module on node platforms; the shape is dynamic and unused outside CLI bonjour discovery dgram: (): any => { throw new Error('Not implemented'); }, platformSupportsKeyChain: () => { // keytar throws an error when system keychain is not present; even // when keytar itself is installed. try/catch to ensure system // keychain is present and no error is thrown. // For now, keychain support is disabled on Linux because when // keytar is loaded it seems to cause the following error when // loading Sharp: // // Something went wrong installing the "sharp" module // /lib/x86_64-linux-gnu/libz.so.1: version `ZLIB_1.2.9' not found // (required by // /home/travis/build/laurent22/joplin/packages/app-cli/node_modules/sharp/build/Release/../../vendor/lib/libpng16.so.16) // // See: https://travis-ci.org/github/laurent22/joplin/jobs/686222036 // // Also disabled in portable mode obviously. return (shim.isWindows() || shim.isMac()) && !shim.isPortable(); }, keytar: (): Keytar => { throw new Error('Not implemented'); }, // In general all imports should be static, but for cases where dynamic // require is needed, we should use the shim so that the code can build in // React Native. In React Native that code path will throw an error, but at // least it will build. // https://stackoverflow.com/questions/55581073 // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Returns whatever module is required at runtime; each call site narrows requireDynamic: (_path: string): any => { throw new Error('Not implemented'); }, }; export default shim;