@typespec/compiler
Version:
TypeSpec Compiler Preview
389 lines • 10.9 kB
JavaScript
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.
*/
export function deepClone(value) {
if (Array.isArray(value)) {
return value.map(deepClone);
}
if (typeof value === "object") {
const obj = {};
for (const prop in value) {
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;
}
}
//# sourceMappingURL=misc.js.map