import fs from 'node:fs'; import path from 'node:path'; import timers from 'node:timers/promises'; import crypto from 'crypto'; /** * Returns Symbol by name. * See: https://stackoverflow.com/questions/50453640/how-can-i-get-the-value-of-a-symbol-property */ export function getSymbolByName(target: T, name?: string) { const ownKeys = Reflect.ownKeys(target); const symbol = ownKeys.find((key) => key.toString() === `Symbol(${name})`); if (!symbol) { throw new Error(`Symbol "${name}" not found in target. ownKeys: ${ownKeys}`); } return symbol as keyof T; } export function removeDuplicates(arr: T[]) { return [...new Set(arr)]; } export function resolvePackageRoot(packageName: string) { try { const packageJsonPath = require.resolve(`${packageName}/package.json`); return path.dirname(packageJsonPath); } catch { throw new Error(`Package "${packageName}" is not installed.`); } } export function getPackageVersion(packageName: string) { try { const packageRoot = resolvePackageRoot(packageName); const packageJsonPath = path.join(packageRoot, 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); return (packageJson.version || '') as string; } catch { return ''; } } export async function callWithTimeout(fn: () => T, timeout?: number, timeoutMsg?: string) { if (!timeout) return fn(); const ac = new AbortController(); return Promise.race([ fn(), timers.setTimeout(timeout, null, { ref: false, signal: ac.signal }).then(() => { throw new Error(timeoutMsg || `Function timeout (${timeout} ms)`); }), ]).finally(() => ac.abort()); } export function stringifyLocation({ line, column }: { line: number; column?: number }) { return `${line}:${column || 0}`; } export function omit(obj: T, key: K) { const res = { ...obj }; delete res[key]; return res as Omit; } export function removeUndefined(obj: T) { if (!obj) return obj; const res = {} as T; const keys = Object.keys(obj) as (keyof T)[]; keys.forEach((key) => { if (obj[key] !== undefined) res[key] = obj[key]; }); return res; } export function toArray(value: T | T[]) { return Array.isArray(value) ? value : [value]; } export function trimTrailingSlash(s: string) { return s.replace(/[/\\]+$/, ''); } export function booleanDefault(value: unknown, defaultValue: boolean) { return typeof value === 'boolean' ? value : defaultValue; } // See: https://github.com/microsoft/playwright/blob/6f16b6cc08f7d59a079d9afa67afacc321a37675/packages/playwright-core/src/utils/crypto.ts#L24 export function calculateSha1(buffer: Buffer | string): string { const hash = crypto.createHash('sha1'); const data = typeof buffer === 'string' ? buffer : new Uint8Array(buffer); hash.update(data); return hash.digest('hex'); } export function throwIf(condition: unknown, message: string) { if (condition) throw new Error(message); } /** * Save file synchronously, create directory if needed. */ export function saveFileSync(filePath: string, content: string) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, content); } export function toBoolean(value: T): value is NonNullable { return Boolean(value); } export function areSetsEqual(set1: Set, set2: Set) { return set1.size === set2.size && [...set1].every((val) => set2.has(val)); } export function escapeHtml(str: string) { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Replace placeholders in a template string. * Example: * 'Hello, {name}' + {name: 'John' } -> 'Hello, John' */ export function substitute(template: string, params: Record) { return template.replace(/{(\w+)}/g, (_, key) => params[key] ?? `{${key}}`); }