UNPKG

@toolpad/utils

Version:

Shared utilities used by Toolpad packages.

1 lines 16.4 kB
{"version":3,"sources":["../src/workerRpc.ts","../src/collections.ts","../src/strings.ts","../src/errors.ts"],"sourcesContent":["import { MessagePort, MessageChannel } from 'worker_threads';\nimport { Awaitable } from './types';\nimport { errorFrom, serializeError } from './errors';\n\n/**\n * Helpers that are intended to set up rpc between a Node.js worker thread and the main thread.\n * Create the worker and pass a port in the workerData.\n *\n * On the main thread:\n *\n * const rpcChannel = new MessageChannel()\n * const worker = new Worker('./myWorker.js', {\n * workerData: { rpcPort: rpcChannel.port1 },\n * transferList: [rpcChannel.port1]\n * })\n *\n * // Depending of the direction of communication, either\n * const client = createRpcClient(rpcChannel.port2)\n * // or\n * serveRpc(rpcChannel.port2, {\n * myMethod\n * })\n *\n * On the worker thread:\n *\n * // Depending of the direction of communication, either\n * const client = createRpcClient(workerData.rpcPort)\n * // or\n * serveRpc(workerData.rpcPort, {\n * myMethod\n * })\n *\n * Use multiple channels for bidirectional communication.\n */\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type Methods = Record<string, (...args: any[]) => Awaitable<any>>;\n\ntype MessageRequest = {\n method: string;\n args: unknown[];\n port: MessagePort;\n};\n\ninterface MsgResponse<T = unknown> {\n error?: unknown;\n result?: T;\n}\n\ninterface CreateRpcClientOptions {\n timeout?: number;\n}\n\nexport function createRpcClient<M extends Methods>(\n port: MessagePort,\n { timeout = 30000 }: CreateRpcClientOptions = {},\n): M {\n return new Proxy({} as M, {\n get: (target, prop) => {\n if (typeof prop !== 'string') {\n return Reflect.get(target, prop);\n }\n return (...args: unknown[]) => {\n return new Promise((resolve, reject) => {\n const { port1, port2 } = new MessageChannel();\n\n const timeoutId = setTimeout(() => {\n port1.close();\n }, timeout);\n\n port1.on('message', (msg: MsgResponse) => {\n clearTimeout(timeoutId);\n if (msg.error) {\n reject(msg.error);\n } else {\n resolve(msg.result);\n }\n });\n\n port1.start();\n\n port.postMessage(\n {\n method: prop,\n args,\n port: port2,\n } satisfies MessageRequest,\n [port2],\n );\n });\n };\n },\n });\n}\n\nexport function serveRpc<M extends Methods>(port: MessagePort, methods: M) {\n const methodMap = new Map(Object.entries(methods));\n port.on('message', async (msg: MessageRequest) => {\n const method = methodMap.get(msg.method);\n if (method) {\n try {\n const result = await method(...msg.args);\n msg.port.postMessage({ result } satisfies MsgResponse);\n } catch (rawError) {\n msg.port.postMessage({ error: serializeError(errorFrom(rawError)) } satisfies MsgResponse);\n }\n } else {\n msg.port.postMessage({\n error: new Error(`Method \"${msg.method}\" not found`),\n } satisfies MsgResponse);\n }\n });\n port.start();\n}\n","export function asArray<T>(maybeArray: T | T[]): T[] {\n return Array.isArray(maybeArray) ? maybeArray : [maybeArray];\n}\n\ntype PropertiesOf<P> = Extract<keyof P, string>;\n\ntype Require<T, K extends keyof T> = T & { [P in K]-?: T[P] };\n\ntype Ensure<U, K extends PropertyKey> = K extends keyof U ? Require<U, K> : U & Record<K, unknown>;\n\n/**\n * Type aware version of Object.protoype.hasOwnProperty.\n * See https://fettblog.eu/typescript-hasownproperty/\n */\nexport function hasOwnProperty<X extends {}, Y extends PropertyKey>(\n obj: X,\n prop: Y,\n): obj is Ensure<X, Y> {\n return obj.hasOwnProperty(prop);\n}\n\n/**\n * Maps `obj` to a new object. The `mapper` function receives an entry array of key and value and\n * is allowed to manipulate both. It can also return `null` to omit a property from the result.\n */\nexport function mapProperties<P, L extends PropertyKey, U>(\n obj: P,\n mapper: <K extends PropertiesOf<P>>(old: [K, P[K]]) => [L, U] | null,\n): Record<L, U>;\nexport function mapProperties<U, V>(\n obj: Record<string, U>,\n mapper: (old: [string, U]) => [string, V] | null,\n): Record<string, V> {\n return Object.fromEntries(\n Object.entries(obj).flatMap((entry) => {\n const mapped = mapper(entry);\n return mapped ? [mapped] : [];\n }),\n );\n}\n\n/**\n * Maps an objects' property keys. The result is a new object with the mapped keys, but the same values.\n */\nexport function mapKeys<U>(\n obj: Record<string, U>,\n mapper: (old: string) => string,\n): Record<string, U> {\n return mapProperties(obj, ([key, value]) => [mapper(key), value]);\n}\n\n/**\n * Maps an objects' property values. The result is a new object with the same keys, but mapped values.\n */\nexport function mapValues<P, V>(\n obj: P,\n mapper: (old: P[PropertiesOf<P>], key: PropertiesOf<P>) => V,\n): Record<PropertiesOf<P>, V> {\n return mapProperties(obj, ([key, value]) => [key, mapper(value, key)]);\n}\n/**\n * Filters an objects' property values. Similar to `array.filter` but for objects. The result is a new\n * object with all the properties removed for which `filter` returned `false`.\n */\nexport function filterValues<K extends PropertyKey, P, Q extends P>(\n obj: Record<K, P>,\n filter: (old: P) => old is Q,\n): Record<K, Q>;\nexport function filterValues<P>(obj: P, filter: (old: P[keyof P]) => boolean): Partial<P>;\nexport function filterValues<U>(\n obj: Record<string, U>,\n filter: (old: U) => boolean,\n): Record<string, U>;\nexport function filterValues<U>(\n obj: Record<string, U>,\n filter: (old: U) => boolean,\n): Record<string, U> {\n return mapProperties(obj, ([key, value]) => (filter(value) ? [key, value] : null));\n}\n\n/**\n * Filters an objects' property keys. Similar to `array.filter` but for objects. The result is a new\n * object with all the properties removed for which `filter` returned `false`.\n */\nexport function filterKeys<P>(obj: P, filter: (old: keyof P) => boolean): Partial<P>;\nexport function filterKeys<U>(\n obj: Record<string, U>,\n filter: (old: string) => boolean,\n): Record<string, U>;\nexport function filterKeys<U>(\n obj: Record<string, U>,\n filter: (old: string) => boolean,\n): Record<string, U> {\n return mapProperties(obj, ([key, value]) => (filter(key) ? [key, value] : null));\n}\n\n/**\n * Compares the properties of two objects. Returns `true` if all properties are strictly equal, `false`\n * otherwise.\n * Pass a subset of properties to only compare those.\n */\nexport function equalProperties<P extends object>(obj1: P, obj2: P, subset?: (keyof P)[]): boolean {\n const keysToCheck = new Set(\n subset ?? ([...Object.keys(obj1), ...Object.keys(obj2)] as (keyof P)[]),\n );\n return Array.from(keysToCheck).every((key) => Object.is(obj1[key], obj2[key]));\n}\n","import title from 'title';\n\n/**\n * Makes the first letter of [str] uppercase.\n * Not locale aware.\n */\nexport function uncapitalize(str: string): string {\n return str.length > 0 ? str[0].toLowerCase() + str.slice(1) : '';\n}\n\n/**\n * Makes the first letter of [str] lowercase.\n * Not locale aware.\n */\nexport function capitalize(str: string): string {\n return str.length > 0 ? str[0].toUpperCase() + str.slice(1) : '';\n}\n\n/**\n * Capitalizes and joins all [parts].\n */\nexport function pascalCase(...parts: string[]): string {\n return parts.map((part) => capitalize(part.toLowerCase())).join('');\n}\n\n/**\n * Joins all [parts] and camelcases the result\n */\nexport function camelCase(...parts: string[]): string {\n if (parts.length > 0) {\n const [first, ...rest] = parts;\n return uncapitalize(first) + pascalCase(...rest);\n }\n return '';\n}\n\n/**\n * Turns a kebab-case string into a constant case string.\n */\nexport function kebabToConstant(input: string): string {\n return input\n .split('-')\n .map((part) => part.toUpperCase())\n .join('_');\n}\n\n/**\n * Turns a kebab-case string into a PascalCase string.\n */\nexport function kebabToPascal(input: string): string {\n return input\n .split('-')\n .map((part) => capitalize(part))\n .join('');\n}\n\n/**\n * Generates a string for `base` by add a number until it's unique amongst a set of predefined names.\n */\nexport function generateUniqueString(base: string, existingNames: Set<string>) {\n let i = 1;\n if (!existingNames.has(base)) {\n return base;\n }\n const newBase = base.replace(/\\d+$/, '');\n let suggestion = newBase;\n while (existingNames.has(suggestion)) {\n suggestion = newBase + String(i);\n i += 1;\n }\n return suggestion;\n}\n\n/**\n * Escape string for use in HTML.\n */\nexport function escapeHtml(unsafe: string): string {\n return unsafe\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#039;');\n}\n\n/**\n * Normalizes and removes all diacritics from a javascript string.\n *\n * See https://stackoverflow.com/a/37511463\n */\nexport function removeDiacritics(input: string): string {\n return input.normalize('NFD').replace(/[\\u0300-\\u036f]/g, '');\n}\n\nexport function isAbsoluteUrl(maybeUrl: string) {\n try {\n return !!new URL(maybeUrl);\n } catch {\n return false;\n }\n}\n\n/**\n * Removes a prefix from a string if it starts with it.\n */\nexport function removePrefix(input: string, prefix: string): string {\n return input.startsWith(prefix) ? input.slice(prefix.length) : input;\n}\n\n/**\n * Removes a suffix from a string if it ends with it.\n */\nexport function removeSuffix(input: string, suffix: string): string {\n return input.endsWith(suffix) ? input.slice(0, -suffix.length) : input;\n}\n\n/**\n * Adds a prefix to a string if it doesn't start with it.\n */\nexport function ensurePrefix(input: string, prefix: string): string {\n return input.startsWith(prefix) ? input : prefix + input;\n}\n\n/**\n * Adds a suffix to a string if it doesn't end with it.\n */\nexport function ensureSuffix(input: string, suffix: string): string {\n return input.endsWith(suffix) ? input : input + suffix;\n}\n\n/**\n * Regex to statically find all static import statements\n *\n * Tested against:\n * import {\n * Component\n * } from '@angular2/core';\n * import defaultMember from \"module-name\";\n * import * as name from \"module-name \";\n * import { member } from \" module-name\";\n * import { member as alias } from \"module-name\";\n * import { member1 ,\n * member2 } from \"module-name\";\n * import { member1 , member2 as alias2 , member3 as alias3 } from \"module-name\";\n * import defaultMember, { member, member } from \"module-name\";\n * import defaultMember, * as name from \"module-name\";\n * import \"module-name\";\n * import * from './smdn';\n */\nconst IMPORT_STATEMENT_REGEX =\n /^\\s*import(?:[\"'\\s]*([\\w*{}\\n, ]+)from\\s*)?[\"'\\s]*([^\"']+)[\"'\\s].*/gm;\n\n/**\n * Statically analyses a javascript source code for import statements and return the specifiers.\n *\n * NOTE: This function does a best effort without parsing the code. The result may contain false\n * positives\n */\nexport function findImports(src: string): string[] {\n return Array.from(src.matchAll(IMPORT_STATEMENT_REGEX), (match) => match[2]);\n}\n\n/**\n * Limits the length of a string and adds ellipsis if necessary.\n */\nexport function truncate(str: string, maxLength: number, dots: string = '...') {\n if (str.length <= maxLength) {\n return str;\n }\n return str.slice(0, maxLength) + dots;\n}\n\n/**\n * Prepend a prefix to each line in the text\n */\nexport function prependLines(text: string, prefix: string): string {\n return text\n .split('\\n')\n .map((line) => prefix + line)\n .join('\\n');\n}\n\n/**\n * Indent the text with [length] number of spaces\n */\nexport function indent(text: string, length = 2): string {\n return prependLines(text, ' '.repeat(length));\n}\n\n/**\n * Returns true if the string is a valid javascript identifier\n */\nexport function isValidJsIdentifier(base: string): boolean {\n return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(base);\n}\n\nexport function guessTitle(str: string): string {\n // Replace snake_case with space\n str = str.replace(/[_-]/g, ' ');\n // Split camelCase\n str = str.replace(/([a-z0-9])([A-Z])/g, '$1 $2');\n // Split acronyms\n str = str.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');\n // Split numbers\n str = str.replace(/([a-zA-Z])(\\d+)/g, '$1 $2');\n str = str.replace(/(\\d+)([a-zA-Z])/g, '$1 $2');\n\n return title(str);\n}\n","import { hasOwnProperty } from './collections';\nimport { truncate } from './strings';\n\ndeclare global {\n interface Error {\n code?: unknown;\n }\n}\n\nexport type PlainObject = Record<string, unknown>;\n\nexport interface SerializedError extends PlainObject {\n message: string;\n name: string;\n stack?: string;\n code?: unknown;\n}\n\nexport function serializeError(error: Error): SerializedError {\n const { message, name, stack, code } = error;\n return { message, name, stack, code };\n}\n\n/**\n * Creates a javascript `Error` from an unknown value if it's not already an error.\n * Does a best effort at inferring a message. Intended to be used typically in `catch`\n * blocks, as there is no way to enforce only `Error` objects being thrown.\n *\n * ```\n * try {\n * // ...\n * } catch (rawError) {\n * const error = errorFrom(rawError);\n * console.assert(error instanceof Error);\n * }\n * ```\n */\nexport function errorFrom(maybeError: unknown): Error {\n if (maybeError instanceof Error) {\n return maybeError;\n }\n\n if (\n typeof maybeError === 'object' &&\n maybeError &&\n hasOwnProperty(maybeError, 'message') &&\n typeof maybeError.message === 'string'\n ) {\n return new Error(maybeError.message, { cause: maybeError });\n }\n\n if (typeof maybeError === 'string') {\n return new Error(maybeError, { cause: maybeError });\n }\n\n const message = truncate(String(JSON.stringify(maybeError)), 500);\n return new Error(message, { cause: maybeError });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAA4C;;;ACcrC,SAAS,eACd,KACA,MACqB;AACrB,SAAO,IAAI,eAAe,IAAI;AAChC;;;ACnBA,mBAAkB;AAqKX,SAAS,SAAS,KAAa,WAAmB,OAAe,OAAO;AAC7E,MAAI,IAAI,UAAU,WAAW;AAC3B,WAAO;AAAA,EACT;AACA,SAAO,IAAI,MAAM,GAAG,SAAS,IAAI;AACnC;;;ACxJO,SAAS,eAAe,OAA+B;AAC5D,QAAM,EAAE,SAAS,MAAM,OAAO,KAAK,IAAI;AACvC,SAAO,EAAE,SAAS,MAAM,OAAO,KAAK;AACtC;AAgBO,SAAS,UAAU,YAA4B;AACpD,MAAI,sBAAsB,OAAO;AAC/B,WAAO;AAAA,EACT;AAEA,MACE,OAAO,eAAe,YACtB,cACA,eAAe,YAAY,SAAS,KACpC,OAAO,WAAW,YAAY,UAC9B;AACA,WAAO,IAAI,MAAM,WAAW,SAAS,EAAE,OAAO,WAAW,CAAC;AAAA,EAC5D;AAEA,MAAI,OAAO,eAAe,UAAU;AAClC,WAAO,IAAI,MAAM,YAAY,EAAE,OAAO,WAAW,CAAC;AAAA,EACpD;AAEA,QAAM,UAAU,SAAS,OAAO,KAAK,UAAU,UAAU,CAAC,GAAG,GAAG;AAChE,SAAO,IAAI,MAAM,SAAS,EAAE,OAAO,WAAW,CAAC;AACjD;;;AHJO,SAAS,gBACd,MACA,EAAE,UAAU,IAAM,IAA4B,CAAC,GAC5C;AACH,SAAO,IAAI,MAAM,CAAC,GAAQ;AAAA,IACxB,KAAK,CAAC,QAAQ,SAAS;AACrB,UAAI,OAAO,SAAS,UAAU;AAC5B,eAAO,QAAQ,IAAI,QAAQ,IAAI;AAAA,MACjC;AACA,aAAO,IAAI,SAAoB;AAC7B,eAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,gBAAM,EAAE,OAAO,MAAM,IAAI,IAAI,qCAAe;AAE5C,gBAAM,YAAY,WAAW,MAAM;AACjC,kBAAM,MAAM;AAAA,UACd,GAAG,OAAO;AAEV,gBAAM,GAAG,WAAW,CAAC,QAAqB;AACxC,yBAAa,SAAS;AACtB,gBAAI,IAAI,OAAO;AACb,qBAAO,IAAI,KAAK;AAAA,YAClB,OAAO;AACL,sBAAQ,IAAI,MAAM;AAAA,YACpB;AAAA,UACF,CAAC;AAED,gBAAM,MAAM;AAEZ,eAAK;AAAA,YACH;AAAA,cACE,QAAQ;AAAA,cACR;AAAA,cACA,MAAM;AAAA,YACR;AAAA,YACA,CAAC,KAAK;AAAA,UACR;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEO,SAAS,SAA4B,MAAmB,SAAY;AACzE,QAAM,YAAY,IAAI,IAAI,OAAO,QAAQ,OAAO,CAAC;AACjD,OAAK,GAAG,WAAW,OAAO,QAAwB;AAChD,UAAM,SAAS,UAAU,IAAI,IAAI,MAAM;AACvC,QAAI,QAAQ;AACV,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,GAAG,IAAI,IAAI;AACvC,YAAI,KAAK,YAAY,EAAE,OAAO,CAAuB;AAAA,MACvD,SAAS,UAAU;AACjB,YAAI,KAAK,YAAY,EAAE,OAAO,eAAe,UAAU,QAAQ,CAAC,EAAE,CAAuB;AAAA,MAC3F;AAAA,IACF,OAAO;AACL,UAAI,KAAK,YAAY;AAAA,QACnB,OAAO,IAAI,MAAM,WAAW,IAAI,MAAM,aAAa;AAAA,MACrD,CAAuB;AAAA,IACzB;AAAA,EACF,CAAC;AACD,OAAK,MAAM;AACb;","names":[]}