UNPKG

@typespec/compiler

Version:

TypeSpec compiler and standard library

406 lines 11.5 kB
import { isPathAbsolute, isUrl, normalizePath, resolvePath } from "../core/path-utils.js"; /** * Recursively calls Object.freeze such that all objects and arrays * referenced are frozen. * * Does not support cycles. Intended to be used only on plain data that can * be directly represented in JSON. */ export function deepFreeze(value) { if (Array.isArray(value)) { value.forEach(deepFreeze); } else if (typeof value === "object") { for (const prop in value) { deepFreeze(value[prop]); } } return Object.freeze(value); } /** * Deeply clones an object. * * Does not support cycles. Intended to be used only on plain data that can * be directly represented in JSON. * * @deprecated Use `structuredClone` instead. */ export function deepClone(value) { if (Array.isArray(value)) { // eslint-disable-next-line @typescript-eslint/no-deprecated return value.map(deepClone); } if (value === null) { return value; } if (typeof value === "object") { const obj = {}; for (const prop in value) { // eslint-disable-next-line @typescript-eslint/no-deprecated obj[prop] = deepClone(value[prop]); } return obj; } return value; } /** * Checks if two objects are deeply equal. * * Does not support cycles. Intended to be used only on plain data that can * be directly represented in JSON. */ export function deepEquals(left, right) { if (left === right) { return true; } if (left === null || right === null || typeof left !== "object" || typeof right !== "object") { return false; } if (Array.isArray(left)) { return Array.isArray(right) ? arrayEquals(left, right, deepEquals) : false; } return mapEquals(new Map(Object.entries(left)), new Map(Object.entries(right)), deepEquals); } /** * Check if two arrays have the same elements. * * @param equals Optional callback for element equality comparison. * Default is to compare by identity using `===`. */ export function arrayEquals(left, right, equals = (x, y) => x === y) { if (left === right) { return true; } if (left.length !== right.length) { return false; } for (let i = 0; i < left.length; i++) { if (!equals(left[i], right[i])) { return false; } } return true; } /** * Check if two maps have the same entries. * * @param equals Optional callback for value equality comparison. * Default is to compare by identity using `===`. */ export function mapEquals(left, right, equals = (x, y) => x === y) { if (left === right) { return true; } if (left.size !== right.size) { return false; } for (const [key, value] of left) { if (!right.has(key) || !equals(value, right.get(key))) { return false; } } return true; } export async function getNormalizedRealPath(host, path) { try { return normalizePath(await host.realpath(path)); } catch (error) { // This could mean the file got deleted but VSCode still has it in memory. So keep the original path. if (error.code === "ENOENT") { return normalizePath(path); } throw error; } } export async function readUrlOrPath(host, pathOrUrl) { if (isUrl(pathOrUrl)) { return host.readUrl(pathOrUrl); } return host.readFile(pathOrUrl); } export function resolveRelativeUrlOrPath(base, relativeOrAbsolute) { if (isUrl(relativeOrAbsolute)) { return relativeOrAbsolute; } else if (isPathAbsolute(relativeOrAbsolute)) { return relativeOrAbsolute; } else if (isUrl(base)) { return new URL(relativeOrAbsolute, base).href; } else { return resolvePath(base, relativeOrAbsolute); } } /** * A specially typed version of `Array.isArray` to work around [this issue](https://github.com/microsoft/TypeScript/issues/17002). */ export function isArray(arg) { return Array.isArray(arg); } /** * Check if argument is not undefined. */ export function isDefined(arg) { return arg !== undefined; } export function isWhitespaceStringOrUndefined(str) { return !str || /^\s*$/.test(str); } export function firstNonWhitespaceCharacterIndex(line) { return line.search(/\S/); } export function distinctArray(arr, keySelector) { const map = new Map(); for (const item of arr) { map.set(keySelector(item), item); } return Array.from(map.values()); } export function tryParseJson(content) { try { return JSON.parse(content); } catch { return undefined; } } export function debounce(fn, delayInMs) { let timer; return function (...args) { if (timer) { clearTimeout(timer); } timer = setTimeout(() => { fn.apply(this, args); }, delayInMs); }; } /** * Remove undefined properties from object. */ export function omitUndefined(data) { return Object.fromEntries(Object.entries(data).filter(([k, v]) => v !== undefined)); } /** * Extract package.json's tspMain entry point in a given path. * @param path Path that contains package.json * @param reportDiagnostic optional diagnostic handler. */ export function resolveTspMain(packageJson) { if (packageJson?.tspMain !== undefined) { return packageJson.tspMain; } return undefined; } /** * A map keyed by a set of objects. * * This is likely non-optimal. */ export class MultiKeyMap { #currentId = 0; #idMap = new WeakMap(); #items = new Map(); get(items) { return this.#items.get(this.compositeKeyFor(items)); } set(items, value) { const key = this.compositeKeyFor(items); this.#items.set(key, value); } compositeKeyFor(items) { return items.map((i) => this.keyFor(i)).join(","); } keyFor(item) { if (this.#idMap.has(item)) { return this.#idMap.get(item); } const id = this.#currentId++; this.#idMap.set(item, id); return id; } } /** * A map with exactly two keys per value. * * Functionally the same as `MultiKeyMap<[K1, K2], V>`, but more efficient. * @hidden bug in typedoc */ export class TwoLevelMap extends Map { /** * Get an existing entry in the map or add a new one if not found. * * @param key1 The first key * @param key2 The second key * @param create A callback to create the new entry when not found. * @param sentinel An optional sentinel value to use to indicate that the * entry is being created. */ getOrAdd(key1, key2, create, sentinel) { let map = this.get(key1); if (map === undefined) { map = new Map(); this.set(key1, map); } let entry = map.get(key2); if (entry === undefined) { if (sentinel !== undefined) { map.set(key2, sentinel); } entry = create(); map.set(key2, entry); } return entry; } } // Adapted from https://github.com/microsoft/TypeScript/blob/bc52ff6f4be9347981de415a35da90497eae84ac/src/compiler/core.ts#L1507 export class Queue { #elements; #headIndex = 0; constructor(elements) { this.#elements = elements?.slice() ?? []; } isEmpty() { return this.#headIndex === this.#elements.length; } enqueue(...items) { this.#elements.push(...items); } dequeue() { if (this.isEmpty()) { throw new Error("Queue is empty."); } const result = this.#elements[this.#headIndex]; this.#elements[this.#headIndex] = undefined; // Don't keep referencing dequeued item this.#headIndex++; // If more than half of the queue is empty, copy the remaining elements to the // front and shrink the array (unless we'd be saving fewer than 100 slots) if (this.#headIndex > 100 && this.#headIndex > this.#elements.length >> 1) { const newLength = this.#elements.length - this.#headIndex; this.#elements.copyWithin(0, this.#headIndex); this.#elements.length = newLength; this.#headIndex = 0; } return result; } } /** * Casts away readonly typing. * * Use it like this when it is safe to override readonly typing: * mutate(item).prop = value; */ export function mutate(value) { return value; } export function createStringMap(caseInsensitive) { return caseInsensitive ? new CaseInsensitiveMap() : new Map(); } class CaseInsensitiveMap extends Map { get(key) { return super.get(key.toUpperCase()); } set(key, value) { return super.set(key.toUpperCase(), value); } has(key) { return super.has(key.toUpperCase()); } delete(key) { return super.delete(key.toUpperCase()); } } export function createRekeyableMap(entries) { return new RekeyableMapImpl(entries); } class RekeyableMapImpl { #keys = new Map(); #values = new Map(); constructor(entries) { if (entries) { for (const [key, value] of entries) { this.set(key, value); } } } clear() { this.#keys.clear(); this.#values.clear(); } delete(key) { const keyItem = this.#keys.get(key); if (keyItem) { this.#keys.delete(key); return this.#values.delete(keyItem); } return false; } forEach(callbackfn, thisArg) { this.#values.forEach((value, keyItem) => { callbackfn(value, keyItem.key, this); }, thisArg); } get(key) { const keyItem = this.#keys.get(key); return keyItem ? this.#values.get(keyItem) : undefined; } has(key) { return this.#keys.has(key); } set(key, value) { let keyItem = this.#keys.get(key); if (!keyItem) { keyItem = { key }; this.#keys.set(key, keyItem); } this.#values.set(keyItem, value); return this; } get size() { return this.#values.size; } *entries() { for (const [k, v] of this.#values) { yield [k.key, v]; } } *keys() { for (const k of this.#values.keys()) { yield k.key; } } values() { return this.#values.values(); } [Symbol.iterator]() { return this.entries(); } [Symbol.toStringTag] = "RekeyableMap"; rekey(existingKey, newKey) { const keyItem = this.#keys.get(existingKey); if (!keyItem) { return false; } this.#keys.delete(existingKey); const newKeyItem = this.#keys.get(newKey); if (newKeyItem) { this.#values.delete(newKeyItem); } keyItem.key = newKey; this.#keys.set(newKey, keyItem); return true; } } export function isPromise(value) { return !!value && typeof value.then === "function"; } export function getEnvironmentVariable(envVarName, defaultWhenNotAvailable) { // make sure we are fine in both node and browser environments if (typeof process !== "undefined") { return process?.env?.[envVarName] ?? defaultWhenNotAvailable; } return defaultWhenNotAvailable; } //# sourceMappingURL=misc.js.map