文件
mynode/packages/lib/shim.ts
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

643 行
23 KiB
TypeScript

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<void>;
getPassword(key: string, client: string): Promise<string|null>;
deletePassword(key: string, client: string): Promise<void>;
}
export interface FetchOptions {
method?: string;
headers?: Record<string, string>;
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<void>((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<any>, 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<Response> => {
throw new Error('Not implemented: fetch');
},
debugFetch: (_url: string, _options: FetchOptions|null): Promise<unknown> => {
throw new Error('Not implemented: debugFetch');
},
fetchText: async (url: string, options: FetchOptions | null = null): Promise<string> => {
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<ResourceEntity> => {
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<string>; 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<any> => {
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<NoteEntity> => {
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<string> => {
throw new Error('Not implemented: attachFileToNoteBody');
},
imageToDataUrl: async (_filePath: string, _maxSize = 0): Promise<string> => {
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<any> => {
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<string[]> => {
throw new Error('Not implemented: textFromPdf');
},
pdfToImages: async (_pdfPath: string, _outputDirectoryPath: string, _options?: CreatePdfFromImagesOptions): Promise<string[]> => {
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<PdfPageImage[]> => {
throw new Error('Not implemented: pdfToImagesWithDimensions');
},
pdfInfo: async (_pdfPath: string): Promise<PdfInfo> => {
throw new Error('Not implemented: pdfInfo');
},
createAccessiblePdf: async (_originalPdfPath: string, _ocrDetails: string, _outputPath: string, _tempDir: string): Promise<void> => {
throw new Error('Not implemented: createAccessiblePdf');
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- node returns boolean (shell.openExternal), RN returns Promise<any> (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<number> => {
throw new Error('Not implemented');
},
showErrorDialog: async (message: string): Promise<void> => {
await shim.showMessageBox(message, { type: MessageBoxType.Error });
},
showConfirmationDialog: async (message: string): Promise<boolean> => {
return await shim.showMessageBox(message, { type: MessageBoxType.Confirm }) === 0;
},
showToast: async (message: string, { type = ToastType.Info }: ShowToastOptions = null): Promise<void> => {
// 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<void> => {
throw new Error('Not implemented');
},
restartApp: (): void|Promise<void> => {
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;