import type { Arrayable, Nullable } from './types' import { NULL_BYTE_PLACEHOLDER, VALID_ID_PREFIX } from './constants' interface CloneOptions { forceWritable?: boolean } interface ErrorOptions { message?: string stackTraceLimit?: number } export { nanoid } from './nanoid' export { shuffle } from './random' /** * Get original stacktrace without source map support the most performant way. * - Create only 1 stack frame. * - Rewrite prepareStackTrace to bypass "support-stack-trace" (usually takes ~250ms). */ export function createSimpleStackTrace(options?: ErrorOptions): string { const { message = '$$stack trace error', stackTraceLimit = 1 } = options || {} const limit = Error.stackTraceLimit const prepareStackTrace = Error.prepareStackTrace Error.stackTraceLimit = stackTraceLimit Error.prepareStackTrace = (e) => e.stack const err = new Error(message) const stackTrace = err.stack || '' Error.prepareStackTrace = prepareStackTrace Error.stackTraceLimit = limit return stackTrace } export function notNullish(v: T | null | undefined): v is NonNullable { return v != null } export function assertTypes(value: unknown, name: string, types: string[]): void { const receivedType = typeof value const pass = types.includes(receivedType) if (!pass) { throw new TypeError(`${name} value must be ${types.join(' or ')}, received "${receivedType}"`) } } export function isPrimitive(value: unknown): boolean { return value === null || (typeof value !== 'function' && typeof value !== 'object') } export function slash(path: string): string { return path.replace(/\\/g, '/') } const postfixRE = /[?#].*$/ export function cleanUrl(url: string): string { return url.replace(postfixRE, '') } export function splitFileAndPostfix(path: string): { file: string postfix: string } { const file = cleanUrl(path) return { file, postfix: path.slice(file.length) } } const externalRE = /^(?:[a-z]+:)?\/\// export const isExternalUrl = (url: string): boolean => externalRE.test(url) /** * Prepend `/@id/` and replace null byte so the id is URL-safe. * This is prepended to resolved ids that are not valid browser * import specifiers by the importAnalysis plugin. */ export function wrapId(id: string): string { return id.startsWith(VALID_ID_PREFIX) ? id : VALID_ID_PREFIX + id.replace('\0', NULL_BYTE_PLACEHOLDER) } /** * Undo {@link wrapId}'s `/@id/` and null byte replacements. */ export function unwrapId(id: string): string { return id.startsWith(VALID_ID_PREFIX) ? id.slice(VALID_ID_PREFIX.length).replace(NULL_BYTE_PLACEHOLDER, '\0') : id } export function withTrailingSlash(path: string): string { if (path.at(-1) !== '/') { return `${path}/` } return path } export function filterOutComments(s: string): string { const result: string[] = [] let commentState: 'none' | 'singleline' | 'multiline' = 'none' for (let i = 0; i < s.length; ++i) { if (commentState === 'singleline') { if (s[i] === '\n') { commentState = 'none' } } else if (commentState === 'multiline') { if (s[i - 1] === '*' && s[i] === '/') { commentState = 'none' } } else if (commentState === 'none') { if (s[i] === '/' && s[i + 1] === '/') { commentState = 'singleline' } else if (s[i] === '/' && s[i + 1] === '*') { commentState = 'multiline' i += 2 } else { result.push(s[i]) } } } return result.join('') } const bareImportRE = /^(?![a-z]:)[\w@](?!.*:\/\/)/i export function isBareImport(id: string): boolean { return bareImportRE.test(id) } export function toArray(array?: Nullable>): Array { array ??= [] if (Array.isArray(array)) { return array } return [array] } export function isObject(item: unknown): boolean { return item != null && typeof item === 'object' && !Array.isArray(item) } function isFinalObj(obj: any) { return obj === Object.prototype || obj === Function.prototype || obj === RegExp.prototype } export function getType(value: unknown): string { return Object.prototype.toString.apply(value).slice(8, -1) } function collectOwnProperties( obj: any, collector: Set | ((key: string | symbol) => void), ) { const collect = typeof collector === 'function' ? collector : (key: string | symbol) => collector.add(key) Object.getOwnPropertyNames(obj).forEach(collect) Object.getOwnPropertySymbols(obj).forEach(collect) } export function getOwnProperties(obj: any): (string | symbol)[] { const ownProps = new Set() if (isFinalObj(obj)) { return [] } collectOwnProperties(obj, ownProps) return Array.from(ownProps) } const defaultCloneOptions: CloneOptions = { forceWritable: false } export function deepClone(val: T, options: CloneOptions = defaultCloneOptions): T { const seen = new WeakMap() return clone(val, seen, options) } export function clone( val: T, seen: WeakMap, options: CloneOptions = defaultCloneOptions, ): T { let k: any, out: any if (seen.has(val)) { return seen.get(val) } if (Array.isArray(val)) { out = Array.from({ length: (k = val.length) }) seen.set(val, out) while (k--) { out[k] = clone(val[k], seen, options) } return out as any } if (Object.prototype.toString.call(val) === '[object Object]') { out = Object.create(Object.getPrototypeOf(val)) seen.set(val, out) // we don't need properties from prototype const props = getOwnProperties(val) for (const k of props) { const descriptor = Object.getOwnPropertyDescriptor(val, k) if (!descriptor) { continue } const cloned = clone((val as any)[k], seen, options) if (options.forceWritable) { Object.defineProperty(out, k, { enumerable: descriptor.enumerable, configurable: true, writable: true, value: cloned, }) } else if ('get' in descriptor) { Object.defineProperty(out, k, { ...descriptor, get() { return cloned }, }) } else { Object.defineProperty(out, k, { ...descriptor, value: cloned, }) } } return out } return val } export function noop(): void {} export function objectAttr(source: any, path: string, defaultValue = undefined): any { // a[3].b -> a.3.b const paths = path.replace(/\[(\d+)\]/g, '.$1').split('.') let result = source for (const p of paths) { result = (new Object(result) as any)[p] if (result === undefined) { return defaultValue } } return result } export type DeferPromise = Promise & { resolve: (value: T | PromiseLike) => void reject: (reason?: any) => void } export function createDefer(): DeferPromise { let resolve: ((value: T | PromiseLike) => void) | null = null let reject: ((reason?: any) => void) | null = null const p = new Promise((_resolve, _reject) => { resolve = _resolve reject = _reject }) as DeferPromise p.resolve = resolve! p.reject = reject! return p } /** * If code starts with a function call, will return its last index, respecting arguments. * This will return 25 - last ending character of toMatch ")" * Also works with callbacks * ``` * toMatch({ test: '123' }); * toBeAliased('123') * ``` */ export function getCallLastIndex(code: string): number | null { let charIndex = -1 let inString: string | null = null let startedBracers = 0 let endedBracers = 0 let beforeChar: string | null = null while (charIndex <= code.length) { beforeChar = code[charIndex] charIndex++ const char = code[charIndex] const isCharString = char === '"' || char === "'" || char === '`' if (isCharString && beforeChar !== '\\') { if (inString === char) { inString = null } else if (!inString) { inString = char } } if (!inString) { if (char === '(') { startedBracers++ } if (char === ')') { endedBracers++ } } if (startedBracers && endedBracers && startedBracers === endedBracers) { return charIndex } } return null } export function isNegativeNaN(val: number): boolean { if (!Number.isNaN(val)) { return false } const f64 = new Float64Array(1) f64[0] = val const u32 = new Uint32Array(f64.buffer) const isNegative = u32[1] >>> 31 === 1 return isNegative } function toString(v: any) { return Object.prototype.toString.call(v) } function isPlainObject(val: any): val is object { return ( toString(val) === '[object Object]' && (!val.constructor || val.constructor.name === 'Object') ) } function isMergeableObject(item: any): item is object { return isPlainObject(item) && !Array.isArray(item) } export function ordinal(i: number): string { const j = i % 10 const k = i % 100 if (j === 1 && k !== 11) { return `${i}st` } if (j === 2 && k !== 12) { return `${i}nd` } if (j === 3 && k !== 13) { return `${i}rd` } return `${i}th` } /** * Deep merge :P * * Will merge objects only if they are plain * * Do not merge types - it is very expensive and usually it's better to case a type here */ export function deepMerge(target: T, ...sources: any[]): T { if (!sources.length) { return target as any } const source = sources.shift() if (source === undefined) { return target as any } if (isMergeableObject(target) && isMergeableObject(source)) { ;(Object.keys(source) as (keyof T)[]).forEach((key) => { if (key === '__proto__' || key === 'constructor' || key === 'prototype') { return } const _source = source as T if (isMergeableObject(_source[key])) { if (!target[key]) { target[key] = {} as any } deepMerge(target[key] as any, _source[key]) } else { target[key] = _source[key] as any } }) } return deepMerge(target, ...sources) } export function unique(array: T[]): T[] { return Array.from(new Set(array)) } export function sanitizeFilePath(s: string): string { // oxlint-disable-next-line no-control-regex return s.replace(/[\x00-\x2C\x2E\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-') }