Files
KiraTV---App-Linux/apps/electron-backend/src/app/workers/worker-runtime-paths.ts
T
2026-05-03 23:43:09 +02:00

262 lines
7.1 KiB
TypeScript

import { existsSync } from 'fs';
import { createRequire } from 'module';
import * as path from 'path';
const WORKER_RELATIVE_PATH = [
'dist',
'apps',
'electron-backend',
'workers',
] as const;
const NATIVE_MODULE_RELATIVE_PATHS = [
['app.asar.unpacked', 'node_modules'],
['app.asar.unpacked', 'electron-backend', 'node_modules'],
['app.asar.unpacked', 'dist', 'apps', 'electron-backend', 'node_modules'],
] as const;
type FileExists = (filePath: string) => boolean;
type ModuleRequireFactory = typeof createRequire;
type NodeModuleApi = {
globalPaths: string[];
_initPaths?: () => void;
};
export interface WorkerBootstrapData {
nativeModuleSearchPaths?: string[];
}
export interface ResolveWorkerRuntimeBootstrapOptions {
isPackaged: boolean;
workerFilename: string;
developmentWorkerDir: string;
resourcesPath?: string;
appPath?: string;
fileExists?: FileExists;
}
export interface WorkerRuntimeBootstrap {
workerPath: string;
workerPathCandidates: string[];
nativeModuleSearchPaths?: string[];
}
export interface LoadNativeModuleOptions<TModule> {
moduleName: string;
searchPaths: string[];
loggerLabel: string;
fallbackRequire?: () => TModule;
fileExists?: FileExists;
requireFactory?: ModuleRequireFactory;
}
function dedupePaths(paths: Array<string | undefined>): string[] {
return [...new Set(paths.filter((value): value is string => Boolean(value)))];
}
export function registerNativeModuleSearchPaths(
searchPaths: string[],
options?: {
env?: NodeJS.ProcessEnv;
moduleApi?: NodeModuleApi;
}
): string[] {
const env = options?.env ?? process.env;
// eslint-disable-next-line @typescript-eslint/no-require-imports
const moduleApi =
options?.moduleApi ??
(require('module') as NodeModuleApi);
const mergedPaths = dedupePaths([
...searchPaths,
...(env.NODE_PATH?.split(path.delimiter) ?? []),
...moduleApi.globalPaths,
]);
if (mergedPaths.length === 0) {
return [];
}
env.NODE_PATH = mergedPaths.join(path.delimiter);
if (typeof moduleApi._initPaths === 'function') {
moduleApi._initPaths();
} else {
moduleApi.globalPaths.splice(0, moduleApi.globalPaths.length, ...mergedPaths);
}
for (let index = mergedPaths.length - 1; index >= 0; index -= 1) {
const currentPath = mergedPaths[index];
if (!moduleApi.globalPaths.includes(currentPath)) {
moduleApi.globalPaths.unshift(currentPath);
}
}
return mergedPaths;
}
export function getPackagedResourceRoots(options: {
resourcesPath?: string;
appPath?: string;
}): string[] {
return dedupePaths([
options.resourcesPath,
options.appPath ? path.dirname(options.appPath) : undefined,
]);
}
export function getNativeModuleSearchPaths(options: {
resourcesPath?: string;
appPath?: string;
}): string[] {
return dedupePaths(
getPackagedResourceRoots(options).flatMap((root) =>
NATIVE_MODULE_RELATIVE_PATHS.map((segments) =>
path.join(root, ...segments)
)
)
);
}
export function getPackagedWorkerPathCandidates(options: {
workerFilename: string;
resourcesPath?: string;
appPath?: string;
}): string[] {
return dedupePaths(
getPackagedResourceRoots(options).map((root) =>
path.join(root, ...WORKER_RELATIVE_PATH, options.workerFilename)
)
);
}
function createWorkerResolutionError(
workerFilename: string,
workerPathCandidates: string[]
): Error {
const error = new Error(
[
`Unable to resolve worker "${workerFilename}".`,
'Tried:',
...workerPathCandidates.map((candidate) => `- ${candidate}`),
].join('\n')
);
error.name = 'WorkerPathResolutionError';
return error;
}
export function resolveWorkerRuntimeBootstrap(
options: ResolveWorkerRuntimeBootstrapOptions
): WorkerRuntimeBootstrap {
const fileExists = options.fileExists ?? existsSync;
if (!options.isPackaged) {
const workerPath = path.join(
options.developmentWorkerDir,
options.workerFilename
);
const workerPathCandidates = [workerPath];
if (!fileExists(workerPath)) {
throw createWorkerResolutionError(
options.workerFilename,
workerPathCandidates
);
}
return { workerPath, workerPathCandidates };
}
const workerPathCandidates = getPackagedWorkerPathCandidates(options);
const workerPath = workerPathCandidates.find((candidate) =>
fileExists(candidate)
);
if (!workerPath) {
throw createWorkerResolutionError(
options.workerFilename,
workerPathCandidates
);
}
return {
workerPath,
workerPathCandidates,
nativeModuleSearchPaths: getNativeModuleSearchPaths(options),
};
}
export function getWorkerDataNativeModuleSearchPaths(
value: unknown
): string[] {
if (!value || typeof value !== 'object') {
return [];
}
if (!('nativeModuleSearchPaths' in value)) {
return [];
}
const searchPaths = value.nativeModuleSearchPaths;
if (!Array.isArray(searchPaths)) {
return [];
}
return dedupePaths(
searchPaths.filter(
(searchPath): searchPath is string => typeof searchPath === 'string'
)
);
}
export function loadNativeModuleFromSearchPaths<TModule>(
options: LoadNativeModuleOptions<TModule>
): TModule {
const fileExists = options.fileExists ?? existsSync;
const requireFactory = options.requireFactory ?? createRequire;
const attemptedPaths: string[] = [];
const failedPaths: string[] = [];
for (const searchPath of dedupePaths(options.searchPaths)) {
attemptedPaths.push(searchPath);
if (!fileExists(searchPath)) {
failedPaths.push(`${searchPath} (missing)`);
continue;
}
try {
const nativeRequire = requireFactory(
path.join(searchPath, 'index.js')
);
return nativeRequire(options.moduleName) as TModule;
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
failedPaths.push(`${searchPath} (${message})`);
}
}
if (options.fallbackRequire) {
try {
return options.fallbackRequire();
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
failedPaths.push(`require(${options.moduleName}) (${message})`);
}
}
const error = new Error(
[
`${options.loggerLabel} Unable to load native module "${options.moduleName}".`,
'Tried:',
...attemptedPaths.map((searchPath) => `- ${searchPath}`),
...(failedPaths.length > 0 ? ['Failures:', ...failedPaths.map((failure) => `- ${failure}`)] : []),
].join('\n')
);
error.name = 'NativeModuleResolutionError';
throw error;
}