UNPKG

@travetto/transformer

Version:

Functionality for AST transformations, with transformer registration, and general utils

195 lines (183 loc) 7.39 kB
import ts from 'typescript'; import { transformCast, type TemplateLiteral } from '../types/shared.ts'; const TypedObject: { keys<T = unknown, K extends keyof T = keyof T>(value: T): K[]; } & ObjectConstructor = Object; function isNode(value: unknown): value is ts.Node { return !!value && typeof value === 'object' && 'kind' in value; } const KNOWN_FNS = new Set<unknown>([String, Number, Boolean, Date, RegExp]); function isKnownFn(value: unknown): value is Function { return KNOWN_FNS.has(value); } /** * Utilities for dealing with literals */ export class LiteralUtil { /** * Determine if a type is a literal type * @param type */ static isLiteralType(type: ts.Type): type is ts.LiteralType { const flags = type.getFlags(); // eslint-disable-next-line no-bitwise return (flags & (ts.TypeFlags.BooleanLiteral | ts.TypeFlags.NumberLiteral | ts.TypeFlags.StringLiteral)) > 0; } /** * Convert literal to a `ts.Node` type */ static fromLiteral<T extends ts.Expression>(factory: ts.NodeFactory, value: T): T; static fromLiteral(factory: ts.NodeFactory, value: undefined): ts.Identifier; static fromLiteral(factory: ts.NodeFactory, value: null): ts.NullLiteral; static fromLiteral(factory: ts.NodeFactory, value: object): ts.ObjectLiteralExpression; static fromLiteral(factory: ts.NodeFactory, value: unknown[]): ts.ArrayLiteralExpression; static fromLiteral(factory: ts.NodeFactory, value: string): ts.StringLiteral; static fromLiteral(factory: ts.NodeFactory, value: number): ts.NumericLiteral; static fromLiteral(factory: ts.NodeFactory, value: boolean): ts.BooleanLiteral; static fromLiteral(factory: ts.NodeFactory, value: unknown): ts.Node { if (isNode(value)) { // If already a node return value; } else if (Array.isArray(value)) { value = factory.createArrayLiteralExpression(value.map(element => this.fromLiteral(factory, element))); } else if (value === undefined) { value = factory.createIdentifier('undefined'); } else if (value === null) { value = factory.createNull(); } else if (typeof value === 'string') { value = factory.createStringLiteral(value); } else if (typeof value === 'number') { const number = factory.createNumericLiteral(Math.abs(value)); value = value < 0 ? factory.createPrefixMinus(number) : number; } else if (typeof value === 'boolean') { value = value ? factory.createTrue() : factory.createFalse(); } else if (value instanceof RegExp) { value = factory.createRegularExpressionLiteral(`/${value.source}/${value.flags ?? ''}`); } else if (isKnownFn(value)) { value = factory.createIdentifier(value.name); } else { const ov = value; const pairs: ts.PropertyAssignment[] = []; for (const key of TypedObject.keys(ov)) { if (ov[key] !== undefined) { pairs.push( factory.createPropertyAssignment(key, this.fromLiteral(factory, ov[key])) ); } } return factory.createObjectLiteralExpression(pairs); } return transformCast(value); } /** * Convert a `ts.Node` to a JS literal */ static toLiteral(value: ts.NullLiteral, strict?: boolean): null; static toLiteral(value: ts.NumericLiteral, strict?: boolean): number; static toLiteral(value: ts.StringLiteral, strict?: boolean): string; static toLiteral(value: ts.BooleanLiteral, strict?: boolean): boolean; static toLiteral(value: ts.ObjectLiteralExpression, strict?: boolean): object; static toLiteral(value: ts.ArrayLiteralExpression, strict?: boolean): unknown[]; static toLiteral(value: undefined, strict?: boolean): undefined; static toLiteral(value: ts.Node, strict?: boolean): unknown; static toLiteral(value?: ts.Node, strict = true): unknown { if (!value) { throw new Error('Value is not defined'); } else if (ts.isArrayLiteralExpression(value)) { return value.elements.map(item => this.toLiteral(item, strict)); } else if (ts.isIdentifier(value)) { if (value.getText() === 'undefined') { return undefined; } else if (!strict) { return value.getText(); } } else if (value.kind === ts.SyntaxKind.NullKeyword) { return null; } else if (ts.isStringLiteral(value)) { return value.text; } else if (ts.isNumericLiteral(value)) { const txt = value.text; if (txt.includes('.')) { return parseFloat(txt); } else { return parseInt(txt, 10); } } else if (ts.isPrefixUnaryExpression(value) && value.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(value.operand)) { const txt = value.operand.text; if (txt.includes('.')) { return -parseFloat(txt); } else { return -parseInt(txt, 10); } } else if (value.kind === ts.SyntaxKind.FalseKeyword) { return false; } else if (value.kind === ts.SyntaxKind.TrueKeyword) { return true; } else if (ts.isObjectLiteralExpression(value)) { const out: Record<string, unknown> = {}; for (const pair of value.properties) { if (ts.isPropertyAssignment(pair)) { out[pair.name.getText()] = this.toLiteral(pair.initializer, strict); } } return out; } if (strict) { throw new Error(`Not a valid input, should be a valid ts.Node: ${value.kind}`); } } /** * Extend object literal, whether JSON or ts.ObjectLiteralExpression */ static extendObjectLiteral(factory: ts.NodeFactory, source: object | ts.Expression, ...rest: (object | ts.Expression)[]): ts.ObjectLiteralExpression { let literal = this.fromLiteral(factory, source); if (rest.find(item => !!item)) { literal = factory.createObjectLiteralExpression([ factory.createSpreadAssignment(literal), ...(rest.filter(item => !!item).map(expression => factory.createSpreadAssignment(this.fromLiteral(factory, expression)))) ]); } return literal; } /** * Get a value from the an object expression */ static getObjectValue(node: ts.Expression | undefined, key: string): ts.Expression | undefined { if (node && ts.isObjectLiteralExpression(node) && node.properties) { for (const property of node.properties) { if (property.name!.getText() === key) { if (ts.isPropertyAssignment(property)) { return property.initializer; } else if (ts.isShorthandPropertyAssignment(property)) { return property.name; } } } } return undefined; } /** * Flatten a template literal into a regex */ static templateLiteralToRegex(template: TemplateLiteral, exact = true): string { const out: string[] = []; for (const value of template.values) { if (value === Number) { out.push('\\d+'); } else if (value === Boolean) { out.push('(?:true|false)'); } else if (value === String) { out.push('.+'); } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { out.push(`${value}`); } else { out.push(`(?:${this.templateLiteralToRegex(transformCast(value), false)})`); } } const body = out.join(template.operation === 'and' ? '' : '|'); if (exact) { return `^(?:${body})$`; } else { return body; } } }