diff --git a/packages/scripts/src/commands/index.ts b/packages/scripts/src/commands/index.ts index 7f564a3..7dee872 100644 --- a/packages/scripts/src/commands/index.ts +++ b/packages/scripts/src/commands/index.ts @@ -1,4 +1,3 @@ export * from './build' export * from './newlib' export * from './intergen' -export * from './typecheck' diff --git a/packages/scripts/src/commands/intergen.test.ts b/packages/scripts/src/commands/intergen.test.ts new file mode 100644 index 0000000..db158fe --- /dev/null +++ b/packages/scripts/src/commands/intergen.test.ts @@ -0,0 +1,160 @@ +jest.mock('../log') + +import {intergen} from './intergen' +import * as fs from 'fs' +import * as path from 'path' +import * as os from 'os' + +describe('intergen', () => { + + const tmpdir = os.tmpdir() + const templateName = 'intergen.tmp' + + let sourceFiles: string[] = [] + let i = 0 + function createSourceFile(contents: string) { + i++ + const sourceFile = path.join(tmpdir, templateName + i + '.ts') + fs.writeFileSync(sourceFile, contents) + sourceFiles.push(sourceFile) + return sourceFile + } + + function execute(source: string): string { + const file = createSourceFile(source) + return intergen('intergen', '-i', file) + } + + afterEach(() => { + sourceFiles.forEach(f => fs.unlinkSync(f)) + sourceFiles = [] + }) + + it('converts exported class into interface', () => { + const result = execute(`export class Value { + amount: number +}`) + expect(result).toEqual(`export interface Value { + amount: number +}`) + }) + + it('does nothing when class is not exported', () => { + const result = execute(`class Value { + amount: number +}`) + expect(result).toEqual(``) + }) + + it('converts inherited classes into interfaces', () => { + const result = execute(`class A { + a: number +} +export class B extends A { + b: string +}`) + expect(result).toEqual(`export interface B { + b: string + a: number +}`) + }) + + it('converts referenced classes into interfaces', () => { + const result = execute(`class A { + a: number +} +export class B { + a: A +}`) + expect(result).toEqual(`export interface B { + a: A +} + +export interface A { + a: number +}`) + }) + + it('correctly converts union properties', () => { + const result = execute(`export class A { + a: 'b' | 'c' +}`) + expect(result).toEqual(`export interface A { + a: "b" | "c" +}`) + }) + + it('correctly converts array properties', () => { + const result = execute(`class Name { + firstName: string +} + +export class Names { + names1: Name[] + names2: Array + names3: Name[][] + names4: Array> +`) + expect(result).toEqual(`export interface Names { + names1: Name[] + names2: Name[] + names3: Name[][] + names4: Name[][] +} + +export interface Name { + firstName: string +}`) + }) + + it('correctly converts intersections', () => { + const result = execute(`export class C { + c: A & B +} + +class A { + a: string +} + +class B { + b: number +}`) + expect(result).toEqual(`export interface C { + c: A & B +} + +export interface A { + a: string +} + +export interface B { + b: number +}`) + }) + + it('correctly converts inline property definitions', () => { + const result = execute(`export class A { + b: {a: string, c: number} +}`) + expect(result).toEqual(`export interface A { + b: { a: string; c: number; } +}`) + }) + + it('correctly converts inline defs w/ references', () => { + const result = execute(`class A { + a: number +} +export class B { + b: { a: A; c: number; } +}`) + expect(result).toEqual(`export interface B { + b: { a: A; c: number; } +} + +export interface A { + a: number +}`) + }) + +}) diff --git a/packages/scripts/src/commands/intergen.ts b/packages/scripts/src/commands/intergen.ts index 1095118..5c6014d 100644 --- a/packages/scripts/src/commands/intergen.ts +++ b/packages/scripts/src/commands/intergen.ts @@ -11,6 +11,11 @@ function isTypeReference(type: ts.ObjectType): type is ts.TypeReference { return !!(type.objectFlags & ts.ObjectFlags.Reference) } +function isAnonymous(type: ts.Type): boolean { + return isObjectType(type) && !!( + type.objectFlags & ts.ObjectFlags.Anonymous) +} + function filterInvisibleProperties(type: ts.Symbol): boolean { const flags = ts.getCombinedModifierFlags(type.valueDeclaration) return !(flags & ts.ModifierFlags.NonPublicAccessibilityModifier) @@ -51,7 +56,7 @@ interface IClassDefinition { * */ -export function typecheck(...argv: string[]) { +export function intergen(...argv: string[]): string { const args = argparse({ input: arg('string', {alias: 'i', required: true}), debug: arg('boolean'), @@ -191,8 +196,8 @@ export function typecheck(...argv: string[]) { function isNodeExported(node: ts.Node): boolean { return ( (ts.getCombinedModifierFlags(node as any) & - ts.ModifierFlags.Export) !== 0 || - (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) + ts.ModifierFlags.Export) !== 0 + // (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) ) } @@ -296,7 +301,10 @@ export function typecheck(...argv: string[]) { properties: classProperties, } - classDefs.push(classDef) + if (!isAnonymous(type)) { + // Prevent defining anonymous declarations as interfaces + classDefs.push(classDef) + } typeDefinitions.set(type, classDef) classDef.allRelevantTypes.forEach(handleType) @@ -306,6 +314,11 @@ export function typecheck(...argv: string[]) { * Visit nodes finding exported classes */ function visit(node: ts.Node) { + console.log(node.getText(), + isNodeExported(node), + ts.getCombinedModifierFlags(node as any), + !!node.parent, + node.parent.kind === ts.SyntaxKind.SourceFile) // Only consider exported nodes if (!isNodeExported(node)) { return @@ -325,8 +338,12 @@ export function typecheck(...argv: string[]) { } function setTypeName(type: ts.Type, mappings: Map) { + if (isAnonymous(type)) { + return + } const name = typeToString(type) - mappings.set(type, `I${name}`) + // (type as any).symbol.name = 'I' + type.symbol.name + mappings.set(type, `${name}`) } const nameMappings = new Map() @@ -339,7 +356,7 @@ export function typecheck(...argv: string[]) { function createInterface(classDef: IClassDefinition): string { const name = nameMappings.get(classDef.type)! - const start = `interface ${name} {` + const start = `export interface ${name} {` const properties = classDef.properties.map(p => { return ` ${p.name}: ${nameMappings.get(p.type) || p.typeString}` }) @@ -362,4 +379,5 @@ export function typecheck(...argv: string[]) { } else { fs.writeFileSync(args.output, value) } + return value } diff --git a/packages/scripts/src/log.ts b/packages/scripts/src/log.ts index 7ff2ffe..ee76989 100644 --- a/packages/scripts/src/log.ts +++ b/packages/scripts/src/log.ts @@ -1,12 +1,9 @@ import {format} from 'util' -const stdout: NodeJS.WriteStream = process.stdout -const stderr: NodeJS.WriteStream = process.stderr - export function error(message: string, ...values: any[]) { - stderr.write(format(message + '\n', ...values)) + process.stderr.write(format(message + '\n', ...values)) } export function info(message: string, ...values: any[]) { - stdout.write(format(message + '\n', ...values)) + process.stdout.write(format(message + '\n', ...values)) }