Skip to content

Commit 58f1a1d

Browse files
authored
feat(typebox): add support for template literal types (#4)
The changes in this commit add support for template literal types in the TypeBox library. The new tests cover various scenarios, including: - One string literal - Multiple string literals - Concatenation with a literal at the start - Concatenation with a literal at the end - Concatenation with a numeric type - Concatenation before and after a string type These changes ensure that the TypeBox library can accurately represent and handle template literal types, which are a powerful feature in TypeScript.
1 parent 750c529 commit 58f1a1d

23 files changed

+359
-207
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
3333
# Finder (MacOS) folder config
3434
.DS_Store
3535

36-
tests/integration/wikibase/output.ts
36+
samples

ARCHITECTURE.md

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -163,15 +163,11 @@ const result = await generateCode({
163163

164164
## Utility Functions and Modules
165165

166-
- **<mcfile name="typebox-call.ts" path="src/utils/typebox-call.ts"></mcfile>**: This module contains the core logic for converting TypeScript type nodes into TypeBox `Type` expressions. Its primary function, `getTypeBoxType`, takes a TypeScript `TypeNode` as input and returns a `ts.Node` representing the equivalent TypeBox schema. This is a crucial part of the transformation process, handling various TypeScript types like primitives, arrays, objects, and unions.
167-
168-
- **<mcfile name="add-static-type-alias.ts" path="src/utils/add-static-type-alias.ts"></mcfile>**: This utility function is responsible for generating and adding the `export type [TypeName] = Static<typeof [TypeName]>` declaration to the output source file. This declaration is essential for enabling TypeScript's static type inference from the dynamically generated TypeBox schemas, ensuring type safety at compile time.
169-
170-
- **<mcfile name="typebox-codegen-utils.ts" path="src/utils/typebox-codegen-utils.ts"></mcfile>**: This file likely contains general utility functions that support the TypeBox code generation process, such as helper functions for string manipulation or AST node creation.
171-
172-
- **<mcfile name="typescript-ast-parser.ts" path="src/utils/typescript-ast-parser.ts"></mcfile>**: This module is responsible for parsing TypeScript source code and extracting relevant Abstract Syntax Tree (AST) information. It might provide functions to navigate the AST and identify specific nodes like type aliases, interfaces, or enums.
173-
174-
- **<mcfile name="typescript-ast-types.ts" path="src/utils/typescript-ast-types.ts"></mcfile>**: This file likely defines custom types or interfaces that represent the structured AST information extracted by `typescript-ast-parser.ts`, providing a consistent data model for further processing.
166+
- <mcfile name="typebox-call.ts" path="src/utils/typebox-call.ts"></mcfile>: Contains the core logic for converting TypeScript type nodes into TypeBox `Type` expressions. `getTypeBoxType` takes a `TypeNode` as input and returns a `ts.Node` representing the equivalent TypeBox schema.
167+
- <mcfile name="add-static-type-alias.ts" path="src/utils/add-static-type-alias.ts"></mcfile>: Generates and adds the `export type [TypeName] = Static<typeof [TypeName]>` declaration to the output source file. This declaration is essential for enabling TypeScript's static type inference from the dynamically generated TypeBox schemas, ensuring type safety at compile time.
168+
- <mcfile name="typebox-codegen-utils.ts" path="src/utils/typebox-codegen-utils.ts"></mcfile>: Contains general utility functions that support the TypeBox code generation process, such as helper functions for string manipulation or AST node creation.
169+
- <mcfile name="typescript-ast-parser.ts" path="src/utils/typescript-ast-parser.ts"></mcfile>: Responsible for parsing TypeScript source code and extracting relevant Abstract Syntax Tree (AST) information. It provides functions to navigate the AST and identify specific nodes like type aliases, interfaces, or enums.
170+
- <mcfile name="typescript-ast-types.ts" path="src/utils/typescript-ast-types.ts"></mcfile>: Defines custom types and interfaces that represent the structured AST information extracted by `typescript-ast-parser.ts`, providing a consistent data model for further processing.
175171

176172
### Handlers Directory
177173

@@ -189,13 +185,13 @@ This directory contains a collection of specialized handler modules, each respon
189185
- <mcfile name="record-type-handler.ts" path="src/handlers/typebox/record-type-handler.ts"></mcfile>: Handles TypeScript `Record` utility types.
190186
- <mcfile name="simple-type-handler.ts" path="src/handlers/typebox/simple-type-handler.ts"></mcfile>: Handles basic TypeScript types like `string`, `number`, `boolean`, `null`, `undefined`, `any`, `unknown`, `void`.
191187
- <mcfile name="function-type-handler.ts" path="src/handlers/typebox/function-type-handler.ts"></mcfile>: Handles TypeScript function types and function declarations, including parameter types, optional parameters, and return types.
192-
- <mcfile name="template-literal-type-handler.ts" path="src/handlers/typebox/template-literal-type-handler.ts"></mcfile>: Handles TypeScript template literal types (e.g., `` `hello-${string}` ``).
188+
- <mcfile name="template-literal-type-handler.ts" path="src/handlers/typebox/template-literal-type-handler.ts"></mcfile>: Handles TypeScript template literal types (e.g., `` `hello-${string}` ``). Parses template literals into components, handling literal text, embedded types (string, number, unions), and string/numeric literals.
193189
- <mcfile name="typeof-type-handler.ts" path="src/handlers/typebox/typeof-type-handler.ts"></mcfile>: Handles TypeScript `typeof` expressions for extracting types from values.
194190
- <mcfile name="tuple-type-handler.ts" path="src/handlers/typebox/tuple-type-handler.ts"></mcfile>: Handles TypeScript tuple types.
195191
- <mcfile name="type-operator-handler.ts" path="src/handlers/typebox/type-operator-handler.ts"></mcfile>: Handles TypeScript type operators like `keyof`, `typeof`.
196192
- <mcfile name="type-reference-handler.ts" path="src/handlers/typebox/type-reference-handler.ts"></mcfile>: Handles references to other types (e.g., `MyType`).
197193
- <mcfile name="typebox-type-handler.ts" path="src/handlers/typebox/typebox-type-handler.ts"></mcfile>: A generic handler for TypeBox types.
198-
- <mcfile name="typebox-type-handlers.ts" path="src/handlers/typebox/typebox-type-handlers.ts"></mcfile>: This file likely orchestrates the use of the individual type handlers, acting as a dispatcher based on the type of AST node encountered.
194+
- <mcfile name="typebox-type-handlers.ts" path="src/handlers/typebox/typebox-type-handlers.ts"></mcfile>: Orchestrates the use of the individual type handlers, acting as a dispatcher based on the type of AST node encountered.
199195
- <mcfile name="union-type-handler.ts" path="src/handlers/typebox/union-type-handler.ts"></mcfile>: Handles TypeScript union types (e.g., `string | number`).
200196

201197
### Parsers Directory
@@ -273,7 +269,7 @@ The optimizations maintain full backward compatibility and test reliability whil
273269

274270
### Performance Testing
275271

276-
To ensure the dependency collection system performs efficiently under various scenarios, comprehensive performance tests have been implemented in <mcfile name="dependency-collector.performance.test.ts" path="tests/ts-morph/dependency-collector.performance.test.ts"></mcfile>. These tests specifically target potential bottlenecks in dependency collection and import processing.
272+
To ensure the dependency collection system performs efficiently under various scenarios, comprehensive performance tests have been implemented in <mcfile name="dependency-collector.performance.test.ts" path="tests/dependency-collector.performance.test.ts"></mcfile>. These tests specifically target potential bottlenecks in dependency collection and import processing.
277273

278274
## Process Overview
279275

@@ -303,29 +299,27 @@ The project uses Bun as the test runner. Here are the key commands for running t
303299
bun test
304300

305301
# Run a specific test file
306-
bun test tests/ts-morph/function-types.test.ts
302+
bun test tests/handlers/typebox/function-types.test.ts
307303

308304
# Run tests in a specific directory
309-
bun test tests/ts-morph/
305+
bun test tests/
310306
```
311307

312308
### TDD Workflow for New Features
313309

314310
When implementing new type handlers or features:
315311

316-
1. **Start with Tests**: Create test cases in the appropriate test file (e.g., `tests/ts-morph/function-types.test.ts` for function-related features)
317-
2. **Run Tests First**: Execute `bun test tests/ts-morph/[test-file]` to confirm tests fail as expected
312+
1. **Start with Tests**: Create test cases in the appropriate test file (e.g., `tests/handlers/typebox/function-types.test.ts` for function-related features)
313+
2. **Run Tests First**: Execute `bun test tests/handlers/typebox/[test-file]` to confirm tests fail as expected
318314
3. **Implement Handler**: Create or modify the type handler to make tests pass
319315
4. **Verify Implementation**: Run tests again to ensure they pass
320316
5. **Integration Testing**: Run the full test suite with `bun test` to ensure no regressions
321-
6. **Manual Verification**: Test with integration examples like `tests/integration/wikibase/wikibase.ts`
322317

323318
### Test Organization
324319

325320
Tests are organized into several categories:
326321

327-
- **Unit Tests** (`tests/ts-morph/`): Test individual type handlers and parsers
328-
- **Integration Tests** (`tests/integration/`): Test end-to-end functionality with real-world examples
322+
- **Unit Tests** (`tests/handlers/`): Test individual type handlers and parsers
329323
- **Performance Tests**: Validate performance characteristics of complex operations
330324

331325
### Best Practices
@@ -337,8 +331,8 @@ Tests are organized into several categories:
337331
- Include edge cases and error conditions
338332
- Run specific tests frequently during development
339333
- Run the full test suite before committing changes
340-
- Run any specific tests with path like `bun test tests/ts-morph/function-types.test.ts`
341-
- Run any specific test cases using command like `bun test tests/ts-morph/function-types.test.ts -t function types`
334+
- Run any specific tests with path like `bun test tests/handlers/typebox/function-types.test.ts`
335+
- Run any specific test cases using command like `bun test tests/handlers/typebox/function-types.test.ts -t function types`
342336
- If tests keep failing, take help from tsc, lint commands to detect for any issues
343337

344338
## Documentation Guidelines

src/handlers/typebox/template-literal-type-handler.ts

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,78 @@ export class TemplateLiteralTypeHandler extends BaseTypeHandler {
1616
return makeTypeCall('Any')
1717
}
1818

19-
// For template literal types, we'll convert them to string patterns
20-
// This is a simplified approach - TypeBox supports template literals with T.TemplateLiteral
21-
const templateText = typeNode.getText()
19+
const parts: ts.Expression[] = []
2220

23-
// For simple cases like `Q${number}`, we can represent as a string pattern
24-
// In a more complete implementation, we might parse the template parts
25-
return makeTypeCall('TemplateLiteral', [ts.factory.createStringLiteral(templateText)])
21+
// Add the head part (literal string before first substitution)
22+
const head = typeNode.getHead()
23+
const headCompilerNode = head.compilerNode as ts.TemplateHead
24+
const headText = headCompilerNode.text
25+
if (headText) {
26+
parts.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(headText)]))
27+
}
28+
29+
// Process template spans (substitutions + following literal parts)
30+
const templateSpans = typeNode.getTemplateSpans()
31+
for (const span of templateSpans) {
32+
// Access the compiler node to get type and literal
33+
const compilerNode = span.compilerNode as ts.TemplateLiteralTypeSpan
34+
35+
// Add the type from the substitution
36+
if (compilerNode.type) {
37+
// Handle common type cases directly
38+
const typeKind = compilerNode.type.kind
39+
if (typeKind === ts.SyntaxKind.StringKeyword) {
40+
parts.push(makeTypeCall('String'))
41+
} else if (typeKind === ts.SyntaxKind.NumberKeyword) {
42+
parts.push(makeTypeCall('Number'))
43+
} else if (typeKind === ts.SyntaxKind.LiteralType) {
44+
// Handle literal types (e.g., 'A', 42, true)
45+
const literalType = compilerNode.type as ts.LiteralTypeNode
46+
if (ts.isStringLiteral(literalType.literal)) {
47+
parts.push(
48+
makeTypeCall('Literal', [ts.factory.createStringLiteral(literalType.literal.text)]),
49+
)
50+
} else if (ts.isNumericLiteral(literalType.literal)) {
51+
parts.push(
52+
makeTypeCall('Literal', [ts.factory.createNumericLiteral(literalType.literal.text)]),
53+
)
54+
} else {
55+
parts.push(makeTypeCall('String')) // fallback for other literals
56+
}
57+
} else if (typeKind === ts.SyntaxKind.UnionType) {
58+
// For union types, we need to handle each type in the union
59+
const unionType = compilerNode.type as ts.UnionTypeNode
60+
const unionParts = unionType.types.map((t) => {
61+
if (t.kind === ts.SyntaxKind.LiteralType) {
62+
const literalType = t as ts.LiteralTypeNode
63+
if (ts.isStringLiteral(literalType.literal)) {
64+
return makeTypeCall('Literal', [
65+
ts.factory.createStringLiteral(literalType.literal.text),
66+
])
67+
}
68+
}
69+
return makeTypeCall('String') // fallback
70+
})
71+
parts.push(makeTypeCall('Union', [ts.factory.createArrayLiteralExpression(unionParts)]))
72+
} else {
73+
// Fallback for other types
74+
parts.push(makeTypeCall('String'))
75+
}
76+
}
77+
78+
// Add the literal part after the substitution
79+
const literalText = compilerNode.literal?.text
80+
if (literalText) {
81+
parts.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(literalText)]))
82+
}
83+
}
84+
85+
// If no parts were found, fallback to a simple string
86+
if (parts.length === 0) {
87+
return makeTypeCall('String')
88+
}
89+
90+
// Return TemplateLiteral with array of parts
91+
return makeTypeCall('TemplateLiteral', [ts.factory.createArrayLiteralExpression(parts)])
2692
}
2793
}

src/input-handler.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@ export interface InputOptions {
1010
}
1111

1212
const hasRelativeImports = (sourceFile: SourceFile): boolean => {
13-
const hasRelativeImports = sourceFile
13+
return sourceFile
1414
.getImportDeclarations()
1515
.some((importDeclaration) => importDeclaration.isModuleSpecifierRelative())
16-
17-
return hasRelativeImports
1816
}
1917

2018
const resolveFilePath = (input: string, callerFile?: string): string => {
@@ -42,9 +40,9 @@ const resolveFilePath = (input: string, callerFile?: string): string => {
4240

4341
if (existingPaths.length > 1) {
4442
throw new Error(
45-
`Multiple resolutions found for path: ${input}. '` +
46-
`Found: ${existingPaths.join(', ')}. ' +
47-
'Please provide a more specific path.`,
43+
`Multiple resolutions found for path: ${input}. ` +
44+
`Found: ${existingPaths.join(', ')}. ` +
45+
'Please provide a more specific path.',
4846
)
4947
}
5048

@@ -73,7 +71,10 @@ export const createSourceFileFromInput = (options: InputOptions): SourceFile =>
7371
// If callerFile is provided, it means this code came from an existing SourceFile
7472
// and relative imports should be allowed
7573

76-
const sourceFile = project.createSourceFile('temp.ts', sourceCode)
74+
const virtualPath = callerFile ? resolve(dirname(callerFile), '__virtual__.ts') : 'temp.ts'
75+
const sourceFile = project.createSourceFile(virtualPath, sourceCode, {
76+
overwrite: true,
77+
})
7778

7879
if (!callerFile && hasRelativeImports(sourceFile)) {
7980
throw new Error(
@@ -83,9 +84,7 @@ export const createSourceFileFromInput = (options: InputOptions): SourceFile =>
8384
)
8485
}
8586

86-
const virtualPath = callerFile ? resolve(dirname(callerFile), '__virtual__.ts') : 'temp.ts'
87-
88-
return project.createSourceFile(virtualPath, sourceCode, { overwrite: true })
87+
return sourceFile
8988
}
9089

9190
if (filePath) {

src/ts-morph-codegen.ts

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ import { EnumParser } from '@daxserver/validation-schema-codegen/parsers/parse-e
66
import { FunctionDeclarationParser } from '@daxserver/validation-schema-codegen/parsers/parse-function-declarations'
77
import { InterfaceParser } from '@daxserver/validation-schema-codegen/parsers/parse-interfaces'
88
import { TypeAliasParser } from '@daxserver/validation-schema-codegen/parsers/parse-type-aliases'
9-
import {
10-
DependencyCollector,
11-
type TypeDependency,
12-
} from '@daxserver/validation-schema-codegen/utils/dependency-collector'
9+
import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector'
1310
import { Project, ts } from 'ts-morph'
1411

1512
export interface GenerateCodeOptions extends InputOptions {
@@ -61,36 +58,23 @@ export const generateCode = async ({
6158
const importDeclarations = sourceFile.getImportDeclarations()
6259
const localTypeAliases = sourceFile.getTypeAliases()
6360

64-
let orderedDependencies: TypeDependency[]
65-
if (exportEverything) {
66-
// When exporting everything, maintain original order
67-
orderedDependencies = dependencyCollector.collectFromImports(
68-
importDeclarations,
69-
exportEverything,
70-
)
71-
dependencyCollector.addLocalTypes(localTypeAliases, sourceFile)
72-
} else {
73-
// When not exporting everything, add local types first so filtering can detect their dependencies
74-
dependencyCollector.addLocalTypes(localTypeAliases, sourceFile)
75-
orderedDependencies = dependencyCollector.collectFromImports(
76-
importDeclarations,
77-
exportEverything,
78-
)
79-
}
61+
// Always add local types first so they can be included in topological sort
62+
dependencyCollector.addLocalTypes(localTypeAliases, sourceFile)
8063

81-
// Process all dependencies in topological order
64+
const orderedDependencies = dependencyCollector.collectFromImports(
65+
importDeclarations,
66+
exportEverything,
67+
)
68+
69+
// Process all dependencies (both imported and local) in topological order
8270
for (const dependency of orderedDependencies) {
8371
if (!processedTypes.has(dependency.typeAlias.getName())) {
8472
typeAliasParser.parseWithImportFlag(dependency.typeAlias, dependency.isImported)
8573
}
8674
}
8775

88-
// Process local types
76+
// Process any remaining local types that weren't included in the dependency graph
8977
if (exportEverything) {
90-
for (const typeAlias of localTypeAliases) {
91-
typeAliasParser.parseWithImportFlag(typeAlias, false)
92-
}
93-
} else {
9478
for (const typeAlias of localTypeAliases) {
9579
if (!processedTypes.has(typeAlias.getName())) {
9680
typeAliasParser.parseWithImportFlag(typeAlias, false)

tests/ts-morph/dependency-collector.integration.test.ts renamed to tests/dependency-collector.integration.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector'
2-
import { createSourceFile } from '@test-fixtures/ts-morph/utils'
2+
import { createSourceFile } from '@test-fixtures/utils'
33
import { beforeEach, describe, expect, test } from 'bun:test'
44
import { Project } from 'ts-morph'
55

tests/ts-morph/dependency-collector.performance.test.ts renamed to tests/dependency-collector.performance.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector'
2-
import { createSourceFile } from '@test-fixtures/ts-morph/utils'
2+
import { createSourceFile } from '@test-fixtures/utils'
33
import { beforeEach, describe, expect, test } from 'bun:test'
44
import { Project } from 'ts-morph'
55

0 commit comments

Comments
 (0)