Skip to content

Commit 44a13e5

Browse files
authored
feat(compiler-config): Implement automatic script target detection and identifier validation (#19)
The changes in this commit introduce a new `CompilerConfig` singleton that manages the TypeScript compiler options and script target configuration for the codegen system. The key changes are: 1. Automatic script target detection: The system now automatically determines the appropriate TypeScript script target from the `ts-morph` Project's compiler options. 2. Identifier validation: Property names in generated TypeBox objects are validated using TypeScript's built-in utilities (`ts.isIdentifierStart()` and `ts.isIdentifierPart()`). This ensures compatibility with the detected script target. 3. Configuration management: The `CompilerConfig` singleton provides a centralized way to manage the script target configuration, allowing it to be overridden per-project as needed. 4. Default behavior: When no explicit target is specified, the system falls back to `ts.ScriptTarget.Latest`, providing maximum compatibility with modern JavaScript features. 5. Integration points: The configuration system is integrated with various components of the codegen system, including the Input Handler, Code Generation, Identifier Utils, and Object Handlers. These changes improve the overall robustness and flexibility of the codegen system, ensuring that the generated code is compatible with the target JavaScript environment.
1 parent 12ca1af commit 44a13e5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+807
-283
lines changed

docs/compiler-configuration.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Compiler Configuration
2+
3+
The codegen system automatically adapts to TypeScript compiler options to ensure generated code is compatible with the target JavaScript environment.
4+
5+
## Script Target Detection
6+
7+
The system automatically determines the appropriate TypeScript script target using a two-tier approach:
8+
9+
1. **Project Configuration** - Uses the target specified in the ts-morph Project's compiler options
10+
2. **Environment Detection** - When no explicit target is found, detects the appropriate target based on the runtime TypeScript version
11+
12+
## Identifier Validation
13+
14+
Property names in generated TypeBox objects are validated using TypeScript's built-in utilities:
15+
16+
- `ts.isIdentifierStart()` - validates first character
17+
- `ts.isIdentifierPart()` - validates remaining characters
18+
19+
The validation respects the detected script target to ensure compatibility:
20+
21+
```typescript
22+
// With ES5 target
23+
interface Example {
24+
validName: string // → validName: Type.String()
25+
'invalid-name': number // → 'invalid-name': Type.Number()
26+
'123invalid': boolean // → '123invalid': Type.Boolean()
27+
}
28+
```
29+
30+
## Configuration Management
31+
32+
The `CompilerConfig` singleton manages script target configuration:
33+
34+
- **Singleton Pattern** - Ensures consistent configuration across the application
35+
- **Environment Detection** - Automatically detects appropriate targets from TypeScript version
36+
- **Project Override** - Respects explicit targets from ts-morph Project configuration
37+
- **Runtime Configuration** - Allows manual target specification when needed
38+
39+
## Environment-Based Target Detection
40+
41+
When no explicit target is specified in the project configuration, the system automatically detects an appropriate target based on the TypeScript version:
42+
43+
- **TypeScript 5.2+** → ES2023
44+
- **TypeScript 5.0+** → ES2022
45+
- **TypeScript 4.9+** → ES2022
46+
- **TypeScript 4.7+** → ES2021
47+
- **TypeScript 4.5+** → ES2020
48+
- **TypeScript 4.2+** → ES2019
49+
- **TypeScript 4.1+** → ES2018
50+
- **TypeScript 4.0+** → ES2017
51+
- **TypeScript 3.8+** → ES2017
52+
- **TypeScript 3.6+** → ES2016
53+
- **TypeScript 3.4+** → ES2015
54+
- **Older versions** → ES5
55+
56+
This ensures generated code uses language features that are supported by the available TypeScript compiler, avoiding compatibility issues.
57+
58+
## Integration Points
59+
60+
The configuration system integrates with:
61+
62+
- **Input Handler** - Initializes config when creating source files
63+
- **Code Generation** - Uses config for output file creation
64+
- **Identifier Utils** - Validates property names with correct target
65+
- **Object Handlers** - Determines property name formatting

docs/handler-system.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ export abstract class BaseTypeHandler {
3030
- `ObjectTypeHandler` - { prop: T }
3131
- `InterfaceTypeHandler` - interface references
3232

33+
Object property names are extracted using the TypeScript compiler API through `PropertySignature.getNameNode()`. The system handles different property name formats:
34+
35+
- **Identifiers** (`prop`) - extracted using `nameNode.getText()` and preserved as identifiers
36+
- **String literals** (`'prop-name'`, `"prop name"`) - extracted using `nameNode.getLiteralValue()` and validated for identifier compatibility
37+
- **Numeric literals** (`123`) - extracted using `nameNode.getLiteralValue().toString()` and treated as identifiers
38+
39+
The system uses TypeScript's built-in character validation utilities (`ts.isIdentifierStart` and `ts.isIdentifierPart`) with runtime-determined script targets to determine if property names can be safely used as unquoted identifiers in the generated code. The script target is automatically determined from the ts-morph Project's compiler options, ensuring compatibility with the target JavaScript environment while maintaining optimal output format.
40+
3341
### Utility Types
3442

3543
- `PartialTypeHandler` - Partial<T>

docs/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,6 @@ export type User = Static<typeof User>
4646
- [architecture.md](./architecture.md) - System architecture
4747
- [parser-system.md](./parser-system.md) - TypeScript parsing
4848
- [handler-system.md](./handler-system.md) - Type conversion
49+
- [compiler-configuration.md](./compiler-configuration.md) - Compiler options and script targets
4950
- [dependency-management.md](./dependency-management.md) - Dependency analysis
5051
- [testing.md](./testing.md) - Testing

docs/utilities.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ NodeTypeUtils.isTypeReference(node, 'Partial') // Check if node is Partial<T>
5050
NodeTypeUtils.isReadonlyArrayType(node) // Check if readonly T[]
5151
```
5252

53+
### Identifier Validation
54+
55+
`src/utils/identifier-utils.ts` - JavaScript identifier validation:
56+
57+
```typescript
58+
isValidIdentifier('validName') // true
59+
isValidIdentifier('123invalid') // false
60+
isValidIdentifier('𝒜') // true - supports Unicode characters
61+
```
62+
63+
Validates JavaScript identifiers using TypeScript's built-in utilities with full Unicode support, including characters outside the Basic Multilingual Plane.
64+
5365
### Template Literal Processing
5466

5567
`src/utils/template-literal-type-processor.ts` - Processes template literal types:
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { CollectionBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/collection/collection-base-handler'
2+
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
3+
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
24
import { ArrayTypeNode, Node, ts } from 'ts-morph'
35

46
export class ArrayTypeHandler extends CollectionBaseHandler {
@@ -7,6 +9,8 @@ export class ArrayTypeHandler extends CollectionBaseHandler {
79
}
810

911
handle(node: ArrayTypeNode): ts.Expression {
10-
return this.processSingleType(node.getElementTypeNode(), 'Array')
12+
const typeboxType = getTypeBoxType(node.getElementTypeNode())
13+
14+
return makeTypeCall('Array', [typeboxType])
1115
}
1216
}

src/handlers/typebox/collection/collection-base-handler.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,4 @@ export abstract class CollectionBaseHandler extends BaseTypeHandler {
1010

1111
return makeTypeCall(typeBoxFunction, [arrayLiteral])
1212
}
13-
14-
protected processSingleType(node: Node, typeBoxFunction: string): ts.Expression {
15-
return makeTypeCall(typeBoxFunction, [getTypeBoxType(node)])
16-
}
17-
18-
protected validateNonEmptyCollection(nodes: Node[], typeName: string): void {
19-
if (nodes.length === 0) {
20-
throw new Error(`${typeName} must have at least one type`)
21-
}
22-
}
2313
}

src/handlers/typebox/date-type-handler.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ export class DateTypeHandler extends BaseTypeHandler {
99
return Node.isIdentifier(typeName) && typeName.getText() === 'Date'
1010
}
1111

12-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
13-
handle(_node: TypeReferenceNode): ts.Expression {
12+
handle(): ts.Expression {
1413
return makeTypeCall('Date')
1514
}
1615
}

src/handlers/typebox/indexed-access-type-handler.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler'
22
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
33
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
4-
import { IndexedAccessTypeNode, Node, ts } from 'ts-morph'
4+
import { IndexedAccessTypeNode, Node, ts, TypeNode } from 'ts-morph'
55

66
export class IndexedAccessTypeHandler extends BaseTypeHandler {
77
canHandle(node: Node): boolean {
@@ -14,8 +14,8 @@ export class IndexedAccessTypeHandler extends BaseTypeHandler {
1414

1515
// Handle special case: typeof A[number] where A is a readonly tuple
1616
if (
17-
objectType?.isKind(ts.SyntaxKind.TypeQuery) &&
18-
indexType?.isKind(ts.SyntaxKind.NumberKeyword)
17+
objectType.isKind(ts.SyntaxKind.TypeQuery) &&
18+
indexType.isKind(ts.SyntaxKind.NumberKeyword)
1919
) {
2020
return this.handleTypeofArrayAccess(objectType, node)
2121
}
@@ -42,31 +42,28 @@ export class IndexedAccessTypeHandler extends BaseTypeHandler {
4242
const typeAlias = sourceFile.getTypeAlias(typeName)
4343
if (typeAlias) {
4444
const tupleUnion = this.extractTupleUnion(typeAlias.getTypeNode())
45-
if (tupleUnion) {
46-
return tupleUnion
47-
}
45+
if (tupleUnion) return tupleUnion
4846
}
4947

5048
// Then try to find a variable declaration
5149
const variableDeclaration = sourceFile.getVariableDeclaration(typeName)
5250
if (variableDeclaration) {
5351
const tupleUnion = this.extractTupleUnion(variableDeclaration.getTypeNode())
54-
if (tupleUnion) {
55-
return tupleUnion
56-
}
52+
if (tupleUnion) return tupleUnion
5753
}
5854
}
5955

6056
// Fallback to default Index behavior
6157
const typeboxObjectType = getTypeBoxType(typeQuery)
6258
const typeboxIndexType = getTypeBoxType(indexedAccessType.getIndexTypeNode())
59+
6360
return makeTypeCall('Index', [typeboxObjectType, typeboxIndexType])
6461
}
6562

66-
private extractTupleUnion(typeNode: Node | undefined): ts.Expression | null {
63+
private extractTupleUnion(typeNode: TypeNode | undefined): ts.Expression | null {
6764
if (!typeNode) return null
6865

69-
let actualTupleType: Node | undefined = typeNode
66+
let actualTupleType: TypeNode = typeNode
7067

7168
// Handle readonly modifier (TypeOperator)
7269
if (typeNode.isKind(ts.SyntaxKind.TypeOperator)) {
@@ -75,7 +72,7 @@ export class IndexedAccessTypeHandler extends BaseTypeHandler {
7572
}
7673

7774
// Check if it's a tuple type
78-
if (actualTupleType?.isKind(ts.SyntaxKind.TupleType)) {
75+
if (actualTupleType.isKind(ts.SyntaxKind.TupleType)) {
7976
const tupleType = actualTupleType.asKindOrThrow(ts.SyntaxKind.TupleType)
8077
const elements = tupleType.getElements()
8178

src/handlers/typebox/keyof-type-handler.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler'
22
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
3-
import { Node, SyntaxKind, ts } from 'ts-morph'
3+
import { LiteralTypeNode, Node, SyntaxKind, ts } from 'ts-morph'
44

55
export class LiteralTypeHandler extends BaseTypeHandler {
66
canHandle(node: Node): boolean {
77
return Node.isLiteralTypeNode(node) || Node.isTrueLiteral(node) || Node.isFalseLiteral(node)
88
}
99

10-
handle(node: Node): ts.Expression {
11-
if (!Node.isLiteralTypeNode(node)) {
12-
return makeTypeCall('Any')
13-
}
14-
10+
handle(node: LiteralTypeNode): ts.Expression {
1511
const literal = node.getLiteral()
16-
const literalKind = literal.getKind()
1712

18-
switch (literalKind) {
13+
switch (literal.getKind()) {
1914
case SyntaxKind.StringLiteral:
2015
return makeTypeCall('Literal', [
2116
ts.factory.createStringLiteral(literal.getText().slice(1, -1)),

0 commit comments

Comments
 (0)