// Copyright (c) 2019 Shellyl_N and Authors // license: ISC // https://github.com/shellyln import { TypeAssertion, PrimitiveTypeAssertion, PrimitiveValueTypeAssertion, RepeatedAssertion, SpreadAssertion, SequenceAssertion, OneOfAssertion, OptionalAssertion, EnumAssertion, AdditionalPropsKey, ObjectAssertion, TypeAssertionMap, CodegenContext } from '../../types'; import { escapeString } from '../../lib/escape'; import { SymbolPattern } from '../../lib/util'; function formatTypeName(typeName: string) { if (typeName.includes('.')) { const z = typeName.split('.'); let s = z[0]; for (let i = 1; i < z.length; i++) { s += `['${escapeString(z[i])}']` } return `(${s})`; } return typeName; } function formatTypeScriptCodeDocComment(ty: TypeAssertion | string, nestLevel: number) { let code = ''; const indent = ' '.repeat(nestLevel); const docComment = typeof ty === 'string' ? ty : ty.docComment; if (docComment) { if (0 <= docComment.indexOf('\n')) { code += `${indent}/**\n${indent} ${ docComment .split('\n') .map(x => x.trimLeft()) .join(`\n${indent} `)}\n${indent} */\n`; } else { code += `${indent}/** ${docComment} */\n`; } } return code; } function generateTypeScriptCodePrimitive(ty: PrimitiveTypeAssertion, ctx: CodegenContext) { // TODO: Function, DateStr, DateTimeStr switch (ty.primitiveName) { case 'integer': return 'number'; default: return ty.primitiveName; } } function generateTypeScriptCodePrimitiveValue(ty: PrimitiveValueTypeAssertion, ctx: CodegenContext) { if (ty.value === null) { return 'null'; } if (ty.value === void 0) { return 'undefined'; } switch (typeof ty.value) { case 'string': return `'${escapeString(ty.value)}'`; case 'bigint': return `${ty.value.toString()}n`; default: return ty.value.toString(); } } function generateTypeScriptCodeRepeated(ty: RepeatedAssertion, ctx: CodegenContext) { return (ty.repeated.kind === 'primitive' || ty.repeated.kind === 'never' || ty.repeated.kind === 'any' || ty.repeated.kind === 'unknown' || ty.repeated.kind === 'object' || ty.repeated.kind === 'symlink' || (ty.repeated.kind === 'one-of' && ty.repeated.typeName) ? `${ty.repeated.typeName ? formatTypeName(ty.repeated.typeName) : generateTypeScriptCodeInner(ty.repeated, false, ctx)}[]` : `Array<${ty.repeated.typeName ? formatTypeName(ty.repeated.typeName) : generateTypeScriptCodeInner(ty.repeated, false, ctx)}>` ); } function generateTypeScriptCodeSpread(ty: SpreadAssertion, ctx: CodegenContext) { return ''; } function generateTypeScriptCodeSequence(ty: SequenceAssertion, ctx: CodegenContext) { if (0 < ty.sequence.filter(x => x.kind === 'spread' || x.kind === 'optional').length) { return 'any[]'; } return `[${ ty.sequence .filter(x => x.kind !== 'spread' && x.kind !== 'optional') .map(x => x.typeName ? formatTypeName(x.typeName) : generateTypeScriptCodeInner(x, false, {...ctx, nestLevel: ctx.nestLevel + 1})) .join(', ')}]`; } function generateTypeScriptCodeOneOf(ty: OneOfAssertion, ctx: CodegenContext) { return `(${ty.oneOf .map(x => x.typeName ? formatTypeName(x.typeName) : generateTypeScriptCodeInner(x, false, ctx)).join(' | ')})`; } function generateTypeScriptCodeOptional(ty: OptionalAssertion, ctx: CodegenContext) { return generateTypeScriptCodeInner(ty.optional, false, ctx); } function generateTypeScriptCodeEnum(ty: EnumAssertion, ctx: CodegenContext) { return `(${ty.values.map(x => `${x[1]}`).join(' | ')})`; } function formatAdditionalPropsName(ak: AdditionalPropsKey, i: number) { return (`[propName${i}: ${ak.map(x => typeof x === 'string' ? x : 'string').join(' | ')}]`); } function generateTypeScriptCodeObject(ty: ObjectAssertion, isInterface: boolean, ctx: CodegenContext) { if (ty.members.filter(x => !(x[2])).length === 0 && ty.additionalProps?.filter(x => !(x[2])).length === 0) { return '{}'; } // NOTE: Semicolon is always preferred. // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-delimiter-style.md const sep = isInterface ? ';\n' : ';\n'; const memberLines = ty.members.filter(x => !(x[2])) .map(x => `${formatTypeScriptCodeDocComment(x[3] || '', ctx.nestLevel + 1)}${ ' '.repeat(ctx.nestLevel + 1)}${ SymbolPattern.test(x[0]) ? x[0] : `'${escapeString(x[0])}'`}${ x[1].kind === 'optional' ? '?' : ''}: ${ x[1].typeName ? formatTypeName(x[1].typeName) : generateTypeScriptCodeInner(x[1], false, {...ctx, nestLevel: ctx.nestLevel + 1})}`); const additionalPropsLines = ty.additionalProps?.filter(x => !(x[2])) .map((x, i) => `${formatTypeScriptCodeDocComment(x[3] || '', ctx.nestLevel + 1)}${ ' '.repeat(ctx.nestLevel + 1)}${ formatAdditionalPropsName(x[0], i)}${x[1].kind === 'optional' ? '?' : ''}: ${ x[1].typeName ? formatTypeName(x[1].typeName) : generateTypeScriptCodeInner(x[1], false, {...ctx, nestLevel: ctx.nestLevel + 1})}`) || []; const propsLines = memberLines.concat(additionalPropsLines); if (propsLines.length === 0) { return '{}'; } return ( `{\n${propsLines.join(sep)}${sep}${' '.repeat(ctx.nestLevel)}}` ); } function generateTypeScriptCodeInner(ty: TypeAssertion, isInterface: boolean, ctx: CodegenContext): string { switch (ty.kind) { case 'never': return 'never'; case 'any': return 'any'; case 'unknown': return 'unknown'; case 'primitive': return generateTypeScriptCodePrimitive(ty, ctx); case 'primitive-value': return generateTypeScriptCodePrimitiveValue(ty, ctx); case 'repeated': return generateTypeScriptCodeRepeated(ty, ctx); case 'spread': return generateTypeScriptCodeSpread(ty, ctx); case 'sequence': return generateTypeScriptCodeSequence(ty, ctx); case 'one-of': return generateTypeScriptCodeOneOf(ty, ctx); case 'optional': return generateTypeScriptCodeOptional(ty, ctx); case 'enum': return generateTypeScriptCodeEnum(ty, ctx); case 'object': return generateTypeScriptCodeObject(ty, isInterface, ctx); case 'symlink': return ty.symlinkTargetName; case 'operator': throw new Error(`Unexpected type assertion: ${(ty as any).kind}`); default: throw new Error(`Unknown type assertion: ${(ty as any).kind}`); } } export function generateTypeScriptCode(types: TypeAssertionMap): string { let code = ''; const ctx = {nestLevel: 0}; for (const ty of types.entries()) { if (ty[1].ty.noOutput) { continue; } code += formatTypeScriptCodeDocComment(ty[1].ty, ctx.nestLevel); if (ty[1].exported) { code += 'export '; } if (ty[1].isDeclare) { code += 'declare '; } if (ty[1].ty.kind === 'object') { code += `interface ${ty[0]}${ ty[1].ty.baseTypes && ty[1].ty.baseTypes.length ? ` extends ${ ty[1].ty.baseTypes .filter(x => x.typeName) .map(x => formatTypeName(x.typeName as string)) .join(', ')}` : ''} ${ generateTypeScriptCodeInner(ty[1].ty, true, ctx)}\n\n`; } else if (ty[1].ty.kind === 'enum') { const indent0 = ' '.repeat(ctx.nestLevel); const indent1 = ' '.repeat(ctx.nestLevel + 1); let value: number | null = 0; code += `${ty[1].ty.isConst ? 'const ' : ''}enum ${ty[0]} {\n${ ty[1].ty.values .map(x => `${ formatTypeScriptCodeDocComment(x[2] || '', ctx.nestLevel + 1)}${ indent1}${(() => { if (value !== null && x[1] === value) { value++; return `${x[0]}`; } else { if (typeof x[1] === 'number') { value = x[1] + 1; return `${x[0]} = ${x[1]}`; } else { return `${x[0]} = '${escapeString(x[1])}'`; } } })()},\n`) .join('')}${indent0}}\n\n`; } else if (ty[1].ty.kind === 'never' && ty[1].ty.passThruCodeBlock) { code += `${ty[1].ty.passThruCodeBlock}\n\n`; } else { code += `type ${ty[0]} = ${ (ty[1].ty.originalTypeName ? formatTypeName(ty[1].ty.originalTypeName) : void 0) || generateTypeScriptCodeInner(ty[1].ty, false, ctx)};\n\n`; } } return code; }