diff --git a/src/tsUtil.test.ts b/src/tsUtil.test.ts index a7afcbc..9779f88 100644 --- a/src/tsUtil.test.ts +++ b/src/tsUtil.test.ts @@ -2,15 +2,7 @@ import { describe, expect, mock, test } from 'bun:test'; import * as fs from 'node:fs'; import { join } from 'node:path'; import ts from 'typescript'; -import { - createProgram, - extractTypeAliases, - getTypeName, - isHtmlOrBodyType, - processTypeScript, - traverseNode, - visit, -} from './tsUtil'; +import { createProgram, processTypeScript, traverseNode, visit } from './tsUtil'; // Helper function to create test TypeScript source files function createTestSourceFile(code: string, fileName: string = 'test.ts'): ts.SourceFile { @@ -71,138 +63,6 @@ describe('TypeScript Utility Functions', () => { }); }); - describe('isHtmlOrBodyType', () => { - test('should return true for Html type alias', () => { - const code = ` - import { Html } from './html'; - type MyPage = Html<"content">; - `; - - const sourceFile = createTestSourceFile(code); - let foundHtmlType = false; - - function checkNode(node: ts.Node) { - if (ts.isTypeAliasDeclaration(node) && node.name.text === 'MyPage') { - foundHtmlType = isHtmlOrBodyType(node); - } - ts.forEachChild(node, checkNode); - } - - checkNode(sourceFile); - expect(foundHtmlType).toBe(true); - }); - - test('should return true for Body type alias', () => { - const code = ` - import { Body } from './html'; - type MyBody = Body<"content">; - `; - - const sourceFile = createTestSourceFile(code); - let foundBodyType = false; - - function checkNode(node: ts.Node) { - if (ts.isTypeAliasDeclaration(node) && node.name.text === 'MyBody') { - foundBodyType = isHtmlOrBodyType(node); - } - ts.forEachChild(node, checkNode); - } - - checkNode(sourceFile); - expect(foundBodyType).toBe(true); - }); - - test('should return false for other type aliases', () => { - const code = ` - type MyDiv = Div<"content">; - type MyString = string; - `; - - const sourceFile = createTestSourceFile(code); - const results: boolean[] = []; - - function checkNode(node: ts.Node) { - if (ts.isTypeAliasDeclaration(node)) { - results.push(isHtmlOrBodyType(node)); - } - ts.forEachChild(node, checkNode); - } - - checkNode(sourceFile); - expect(results.every((result) => result === false)).toBe(true); - }); - }); - - describe('extractTypeAliases', () => { - test('should extract all type aliases from source file', () => { - const code = ` - type First = string; - type Second = number; - interface NotATypeAlias { - value: string; - } - type Third = boolean; - `; - - const sourceFile = createTestSourceFile(code); - const aliases = extractTypeAliases(sourceFile); - - expect(aliases).toHaveLength(3); - expect(aliases[0].name.text).toBe('First'); - expect(aliases[1].name.text).toBe('Second'); - expect(aliases[2].name.text).toBe('Third'); - }); - - test('should return empty array for file without type aliases', () => { - const code = ` - interface OnlyInterface { - value: string; - } - - const variable = "test"; - - function myFunction() { - return true; - } - `; - - const sourceFile = createTestSourceFile(code); - const aliases = extractTypeAliases(sourceFile); - - expect(aliases).toHaveLength(0); - }); - }); - - describe('getTypeName', () => { - test('should return type name for type reference', () => { - const code = ` - import { Html } from './html'; - type MyPage = Html<"content">; - `; - - const sourceFile = createTestSourceFile(code); - const aliases = extractTypeAliases(sourceFile); - - expect(aliases).toHaveLength(1); - const typeName = getTypeName(aliases[0]); - expect(typeName).toBe('Html'); - }); - - test('should return null for non-type-reference aliases', () => { - const code = ` - type MyString = string; - type MyObject = { value: number }; - `; - - const sourceFile = createTestSourceFile(code); - const aliases = extractTypeAliases(sourceFile); - - expect(aliases).toHaveLength(2); - expect(getTypeName(aliases[0])).toBeNull(); - expect(getTypeName(aliases[1])).toBeNull(); - }); - }); - describe('traverseNode', () => { test('should handle type literal node with brand property', () => { const code = ` @@ -351,49 +211,3 @@ describe('TypeScript Utility Functions', () => { }); }); }); - -describe('Integration Tests', () => { - test('should work with complete HTML type structure', () => { - const code = ` - import { Html, Body, Div, P } from './html'; - - type MyPage = Html< - Body< - Div< - P<"Hello World"> - > - > - >; - `; - - const sourceFile = createTestSourceFile(code); - const aliases = extractTypeAliases(sourceFile); - - expect(aliases).toHaveLength(1); - expect(aliases[0].name.text).toBe('MyPage'); - expect(isHtmlOrBodyType(aliases[0])).toBe(true); - expect(getTypeName(aliases[0])).toBe('Html'); - }); - - test('should identify multiple HTML/Body types', () => { - const code = ` - import { Html, Body, Div } from './html'; - - type Page1 = Html<"Content 1">; - type Page2 = Body<"Content 2">; - type NotHtml = Div<"Content 3">; - `; - - const sourceFile = createTestSourceFile(code); - const aliases = extractTypeAliases(sourceFile); - - expect(aliases).toHaveLength(3); - - const htmlBodyTypes = aliases.filter((alias) => isHtmlOrBodyType(alias)); - expect(htmlBodyTypes).toHaveLength(2); - - const typeNames = htmlBodyTypes.map((alias) => getTypeName(alias)); - expect(typeNames).toContain('Html'); - expect(typeNames).toContain('Body'); - }); -}); diff --git a/src/tsUtil.ts b/src/tsUtil.ts index de26f1e..09d05cd 100644 --- a/src/tsUtil.ts +++ b/src/tsUtil.ts @@ -1,7 +1,7 @@ // TypeScript AST Processing Utilities import { createWriteStream } from 'node:fs'; -import ts from 'typescript'; +import ts, { SyntaxKind } from 'typescript'; import { type HtmlJson, renderToStream } from './html'; export function traverseNode(node: ts.Node, indent: number = 0): HtmlJson { @@ -9,11 +9,7 @@ export function traverseNode(node: ts.Node, indent: number = 0): HtmlJson { let tag = ''; let children: (HtmlJson | string)[] = []; node.members.forEach((member) => { - if ( - member.name && - ts.isIdentifierOrThisTypeNode(member.name) && - ts.isPropertySignature(member) - ) { + if (member.name && ts.isPropertySignature(member) && ts.isIdentifier(member.name)) { if ( member.type && ts.isLiteralTypeNode(member.type) && @@ -37,6 +33,8 @@ export function traverseNode(node: ts.Node, indent: number = 0): HtmlJson { member.name.expression.escapedText ) { tag = member.name.expression.escapedText.replace('Brand', ''); + } else { + throw new Error('Unexpected type'); } }); return { @@ -49,7 +47,11 @@ export function traverseNode(node: ts.Node, indent: number = 0): HtmlJson { } export function visit(node: ts.Node, checker: ts.TypeChecker, outPath: string) { - if (ts.isTypeAliasDeclaration(node)) { + if (ts.isSourceFile(node)) { + ts.forEachChild(node, (childNode) => { + visit(childNode, checker, outPath); + }); + } else if (ts.isTypeAliasDeclaration(node)) { if ( ts.isTypeReferenceNode(node.type) && node.type.typeName && @@ -58,13 +60,9 @@ export function visit(node: ts.Node, checker: ts.TypeChecker, outPath: string) { ) { try { const type = checker.getTypeAtLocation(node); - const stringJSON = checker.typeToTypeNode( - type, - undefined, - ts.NodeBuilderFlags.InTypeAlias | ts.NodeBuilderFlags.NoTruncation - ); - if (stringJSON) { - const result = traverseNode(stringJSON, 0); + const typeNode = checker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.NoTruncation); + if (typeNode) { + const result = traverseNode(typeNode, 0); const writeStream = createWriteStream(outPath, { flags: 'w' }); renderToStream(result, writeStream); writeStream.end('\n'); @@ -74,7 +72,6 @@ export function visit(node: ts.Node, checker: ts.TypeChecker, outPath: string) { } } } - ts.forEachChild(node, (childNode) => visit(childNode, checker, outPath)); } export function createProgram( @@ -119,43 +116,3 @@ export function processTypeScript(filePath: string, outPath: string): boolean { return false; } } - -// TypeScript AST Helper Functions -export function isHtmlOrBodyType(node: ts.Node): boolean { - if (ts.isTypeAliasDeclaration(node)) { - if ( - ts.isTypeReferenceNode(node.type) && - node.type.typeName && - ts.isIdentifier(node.type.typeName) - ) { - const typeName = node.type.typeName.escapedText; - return typeName === 'Html' || typeName === 'Body'; - } - } - return false; -} - -export function extractTypeAliases(node: ts.Node): ts.TypeAliasDeclaration[] { - const aliases: ts.TypeAliasDeclaration[] = []; - - function collectAliases(n: ts.Node) { - if (ts.isTypeAliasDeclaration(n)) { - aliases.push(n); - } - ts.forEachChild(n, collectAliases); - } - - collectAliases(node); - return aliases; -} - -export function getTypeName(typeAlias: ts.TypeAliasDeclaration): string | null { - if ( - ts.isTypeReferenceNode(typeAlias.type) && - typeAlias.type.typeName && - ts.isIdentifier(typeAlias.type.typeName) - ) { - return typeAlias.type.typeName.escapedText.toString(); - } - return null; -}