@toolpad/utils
Version:
Shared utilities used by Toolpad packages.
1 lines • 17.7 kB
Source Map (JSON)
{"version":3,"sources":["../src/fs.ts","../src/collections.ts","../src/strings.ts","../src/errors.ts"],"sourcesContent":["import * as fs from 'fs/promises';\nimport * as path from 'path';\nimport { Dirent } from 'fs';\nimport * as yaml from 'yaml';\nimport { yamlOverwrite } from 'yaml-diff-patch';\nimport prettier from 'prettier';\nimport { errorFrom } from './errors';\n\n/**\n * Formats a yaml source with `prettier`.\n */\nasync function formatYaml(code: string, filePath: string): Promise<string> {\n const readConfig = await prettier.resolveConfig(filePath);\n return prettier.format(code, {\n ...readConfig,\n parser: 'yaml',\n });\n}\n\nexport type Reviver = NonNullable<Parameters<typeof JSON.parse>[1]>;\n\n/**\n * Like `fs.readFile`, but for JSON files specifically. Will throw on malformed JSON.\n */\nexport async function readJsonFile(filePath: string, reviver?: Reviver): Promise<unknown> {\n const content = await fs.readFile(filePath, { encoding: 'utf-8' });\n return JSON.parse(content, reviver);\n}\n\nexport async function readMaybeFile(filePath: string): Promise<string | null> {\n try {\n return await fs.readFile(filePath, { encoding: 'utf-8' });\n } catch (rawError) {\n const error = errorFrom(rawError);\n if (error.code === 'ENOENT' || error.code === 'EISDIR') {\n return null;\n }\n throw error;\n }\n}\n\nexport async function readMaybeDir(dirPath: string): Promise<Dirent[]> {\n try {\n return await fs.readdir(dirPath, { withFileTypes: true });\n } catch (rawError: unknown) {\n const error = errorFrom(rawError);\n if (error.code === 'ENOENT' || error.code === 'ENOTDIR') {\n return [];\n }\n throw error;\n }\n}\n\nexport type WriteFileOptions = Parameters<typeof fs.writeFile>[2];\n\nexport async function writeFileRecursive(\n filePath: string,\n content: string | Buffer,\n options?: WriteFileOptions,\n): Promise<void> {\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n await fs.writeFile(filePath, content, options);\n}\n\nexport interface UpdateYamlOptions {\n schemaUrl?: string;\n}\n\nexport async function updateYamlFile(\n filePath: string,\n content: object,\n options?: UpdateYamlOptions,\n) {\n const oldContent = await readMaybeFile(filePath);\n\n let newContent = oldContent ? yamlOverwrite(oldContent, content) : yaml.stringify(content);\n\n if (options?.schemaUrl) {\n const yamlDoc = yaml.parseDocument(newContent);\n yamlDoc.commentBefore = ` yaml-language-server: $schema=${options.schemaUrl}`;\n newContent = yamlDoc.toString();\n }\n\n newContent = await formatYaml(newContent, filePath);\n if (newContent !== oldContent) {\n await writeFileRecursive(filePath, newContent);\n }\n}\n\nexport async function fileExists(filepath: string): Promise<boolean> {\n try {\n const stat = await fs.stat(filepath);\n return stat.isFile();\n } catch (err) {\n if (errorFrom(err).code === 'ENOENT') {\n return false;\n }\n throw err;\n }\n}\n\nexport async function folderExists(folderpath: string): Promise<boolean> {\n try {\n const stat = await fs.stat(folderpath);\n return stat.isDirectory();\n } catch (err) {\n if (errorFrom(err).code === 'ENOENT') {\n return false;\n }\n throw err;\n }\n}\n\nexport async function fileReplace(\n filePath: string,\n searchValue: string | RegExp,\n replaceValue: string,\n): Promise<void> {\n const queriesFileContent = await fs.readFile(filePath, { encoding: 'utf-8' });\n const updatedFileContent = queriesFileContent.replace(searchValue, () => replaceValue);\n await fs.writeFile(filePath, updatedFileContent);\n}\n\nexport async function fileReplaceAll(\n filePath: string,\n searchValue: string | RegExp,\n replaceValue: string,\n) {\n const queriesFileContent = await fs.readFile(filePath, { encoding: 'utf-8' });\n const updatedFileContent = queriesFileContent.replaceAll(searchValue, () => replaceValue);\n await fs.writeFile(filePath, updatedFileContent);\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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\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":"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","names":["prettier","stat"]}