Files
iptvnator/apps/electron-backend/src/app/workers/worker-runtime-paths.ts
T
4grayandClaude Opus 5 0334296f15 fix(electron-backend): make test suite and lint host-agnostic on Windows checkouts (#1176)
* fix(electron-backend): make test suite and lint host-agnostic across Windows/Linux checkouts

Windows checkouts (core.autocrlf=true) had 13 pre-existing jest failures
and 5 Windows-only lint errors in electron-backend while Linux CI was
green:

- embedded-mpv-native-source.spec: normalize CRLF after readFileSync so
  multi-line source assertions match on autocrlf checkouts
- worker-runtime-paths.spec: build expected candidate paths with
  path.join instead of hardcoded POSIX strings
- external-player-launch-context: join darwin-only paths (.app bundle
  executables, Homebrew Caskroom) with path.posix.join so simulated
  darwin platforms resolve correctly on win32 hosts (no-op on macOS)
- app.spec: build packaged-navigation fixtures with path.resolve +
  pathToFileURL; file:///tmp/... is not a valid win32 file URL
- lint target: quote the eslint glob. The unquoted ** was expanded by
  the POSIX shell on Linux (shallow match), so CI linted only a subset
  of files while Windows passed the literal pattern to ESLint and
  linted the full tree - the hosts checked different file sets
- fix the 5 errors full-tree linting surfaces: prefer-const in
  epg-worker.service and database.worker-connection, no-unsafe-finally
  in external-player-session-registry and embedded-mpv-native.service
  (rewritten as catch-swallow with identical semantics, pinned by new
  regression tests), intentional no-control-regex in the recording
  filename sanitizer; drop stale unused disable directives
- document the quoted-glob convention in CLAUDE.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: mirror the quoted-lint-glob convention into AGENTS.md

AGENTS.md already mirrors the neighbouring max-lines/baseline paragraph from
CLAUDE.md, and it requires coding conventions to stay in sync between the two
files. The quoted-glob rule landed only in CLAUDE.md, so agents bootstrapping
from AGENTS.md could reintroduce a host-dependent lint target.

Also corrects the wording in both copies: the shallow expansion happens on
macOS as well as Linux — /bin/sh has no globstar on either — and the target
still exits 0 with a broken glob, which is why this went unnoticed. Adds the
file-count check that catches it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 23:05:22 +02:00

261 lines
7.0 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;
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;
}