-
Notifications
You must be signed in to change notification settings - Fork 0
feat(compiler-config): Implement automatic script target detection and identifier validation #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| # Compiler Configuration | ||
|
|
||
| The codegen system automatically adapts to TypeScript compiler options to ensure generated code is compatible with the target JavaScript environment. | ||
|
|
||
| ## Script Target Detection | ||
|
|
||
| The system automatically determines the appropriate TypeScript script target using a two-tier approach: | ||
|
|
||
| 1. **Project Configuration** - Uses the target specified in the ts-morph Project's compiler options | ||
| 2. **Environment Detection** - When no explicit target is found, detects the appropriate target based on the runtime TypeScript version | ||
|
|
||
| ## Identifier Validation | ||
|
|
||
| Property names in generated TypeBox objects are validated using TypeScript's built-in utilities: | ||
|
|
||
| - `ts.isIdentifierStart()` - validates first character | ||
| - `ts.isIdentifierPart()` - validates remaining characters | ||
|
|
||
| The validation respects the detected script target to ensure compatibility: | ||
|
|
||
| ```typescript | ||
| // With ES5 target | ||
| interface Example { | ||
| validName: string // → validName: Type.String() | ||
| 'invalid-name': number // → 'invalid-name': Type.Number() | ||
| '123invalid': boolean // → '123invalid': Type.Boolean() | ||
| } | ||
| ``` | ||
|
|
||
| ## Configuration Management | ||
|
|
||
| The `CompilerConfig` singleton manages script target configuration: | ||
|
|
||
| - **Singleton Pattern** - Ensures consistent configuration across the application | ||
| - **Environment Detection** - Automatically detects appropriate targets from TypeScript version | ||
| - **Project Override** - Respects explicit targets from ts-morph Project configuration | ||
| - **Runtime Configuration** - Allows manual target specification when needed | ||
|
|
||
| ## Environment-Based Target Detection | ||
|
|
||
| When no explicit target is specified in the project configuration, the system automatically detects an appropriate target based on the TypeScript version: | ||
|
|
||
| - **TypeScript 5.2+** → ES2023 | ||
| - **TypeScript 5.0+** → ES2022 | ||
| - **TypeScript 4.9+** → ES2022 | ||
| - **TypeScript 4.7+** → ES2021 | ||
| - **TypeScript 4.5+** → ES2020 | ||
| - **TypeScript 4.2+** → ES2019 | ||
| - **TypeScript 4.1+** → ES2018 | ||
| - **TypeScript 4.0+** → ES2017 | ||
| - **TypeScript 3.8+** → ES2017 | ||
| - **TypeScript 3.6+** → ES2016 | ||
| - **TypeScript 3.4+** → ES2015 | ||
| - **Older versions** → ES5 | ||
|
|
||
| This ensures generated code uses language features that are supported by the available TypeScript compiler, avoiding compatibility issues. | ||
|
|
||
| ## Integration Points | ||
|
|
||
| The configuration system integrates with: | ||
|
|
||
| - **Input Handler** - Initializes config when creating source files | ||
| - **Code Generation** - Uses config for output file creation | ||
| - **Identifier Utils** - Validates property names with correct target | ||
| - **Object Handlers** - Determines property name formatting |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,8 +9,7 @@ export class DateTypeHandler extends BaseTypeHandler { | |
| return Node.isIdentifier(typeName) && typeName.getText() === 'Date' | ||
| } | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| handle(_node: TypeReferenceNode): ts.Expression { | ||
| handle(): ts.Expression { | ||
| return makeTypeCall('Date') | ||
| } | ||
|
Comment on lines
+12
to
14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainConfirm base signature: handler.handle now parameterless Assuming Run: 🏁 Script executed: #!/bin/bash
rg -nP "class\s+BaseTypeHandler\b.*?\bhandle\s*\(" -U -C2Length of output: 59 🏁 Script executed: #!/bin/bash
# Locate the BaseTypeHandler class definition
rg -nP "class\s+BaseTypeHandler\b" -n -C3
# Inspect BaseTypeHandler for handle signature
rg -nP "BaseTypeHandler[^}]*handle\s*\(" -n -C3Length of output: 865 Ensure handle override matches BaseTypeHandler signature handle(): ts.Expression {
return makeTypeCall('Date')
}to handle(node: Node): ts.Expression {
return makeTypeCall('Date')
}to satisfy abstract handle(node: Node): ts.Expressionin BaseTypeHandler. 🤖 Prompt for AI Agents |
||
| } | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,16 @@ | ||
| import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' | ||
| import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' | ||
| import { Node, SyntaxKind, ts } from 'ts-morph' | ||
| import { LiteralTypeNode, Node, SyntaxKind, ts } from 'ts-morph' | ||
|
|
||
| export class LiteralTypeHandler extends BaseTypeHandler { | ||
| canHandle(node: Node): boolean { | ||
| return Node.isLiteralTypeNode(node) || Node.isTrueLiteral(node) || Node.isFalseLiteral(node) | ||
| } | ||
|
Comment on lines
6
to
8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix canHandle/handle mismatch (can cause runtime errors). canHandle accepts True/False nodes, but handle expects a LiteralTypeNode and immediately calls getLiteral(). If a bare True/False node ever routes here, this will blow up. Apply this diff to restrict canHandle to literal type nodes only: - canHandle(node: Node): boolean {
- return Node.isLiteralTypeNode(node) || Node.isTrueLiteral(node) || Node.isFalseLiteral(node)
- }
+ canHandle(node: Node): boolean {
+ return Node.isLiteralTypeNode(node)
+ }🤖 Prompt for AI Agents |
||
|
|
||
| handle(node: Node): ts.Expression { | ||
| if (!Node.isLiteralTypeNode(node)) { | ||
| return makeTypeCall('Any') | ||
| } | ||
|
|
||
| handle(node: LiteralTypeNode): ts.Expression { | ||
| const literal = node.getLiteral() | ||
| const literalKind = literal.getKind() | ||
|
|
||
| switch (literalKind) { | ||
| switch (literal.getKind()) { | ||
| case SyntaxKind.StringLiteral: | ||
| return makeTypeCall('Literal', [ | ||
| ts.factory.createStringLiteral(literal.getText().slice(1, -1)), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,44 +8,19 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler { | |
| } | ||
|
|
||
| handle(node: InterfaceDeclaration): ts.Expression { | ||
| const typeParameters = node.getTypeParameters() | ||
| const heritageClauses = node.getHeritageClauses() | ||
| const baseObjectType = this.createObjectType(this.processProperties(node.getProperties())) | ||
|
|
||
| // For generic interfaces, return raw TypeBox expression | ||
| // The parser will handle wrapping it in an arrow function using GenericTypeUtils | ||
| if (typeParameters.length > 0) { | ||
| // For generic interfaces, handle inheritance here and return raw expression | ||
| if (heritageClauses.length === 0) { | ||
| return baseObjectType | ||
| } | ||
|
|
||
| const extendedTypes = this.collectExtendedTypes(heritageClauses) | ||
|
|
||
| if (extendedTypes.length === 0) { | ||
| return baseObjectType | ||
| } | ||
|
|
||
| // Create composite with extended types first, then the current interface | ||
| const allTypes = [...extendedTypes, baseObjectType] | ||
| return makeTypeCall('Composite', [ts.factory.createArrayLiteralExpression(allTypes, true)]) | ||
| } | ||
|
|
||
| // For non-generic interfaces, handle as before | ||
| if (heritageClauses.length === 0) { | ||
| return baseObjectType | ||
| } | ||
| if (heritageClauses.length === 0) return baseObjectType | ||
|
|
||
| const extendedTypes = this.collectExtendedTypes(heritageClauses) | ||
|
|
||
| if (extendedTypes.length === 0) { | ||
| return baseObjectType | ||
| } | ||
| if (extendedTypes.length === 0) return baseObjectType | ||
|
|
||
| // Create composite with extended types first, then the current interface | ||
| const allTypes = [...extendedTypes, baseObjectType] | ||
| const expression = ts.factory.createArrayLiteralExpression(allTypes, true) | ||
|
|
||
| return makeTypeCall('Composite', [ts.factory.createArrayLiteralExpression(allTypes, true)]) | ||
| return makeTypeCall('Composite', [expression]) | ||
| } | ||
|
Comment on lines
+21
to
24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: creating identifiers from generic/qualified type text yields invalid AST; use node-driven conversion instead.
- const expression = ts.factory.createArrayLiteralExpression(allTypes, true)
+ const expression = ts.factory.createArrayLiteralExpression(allTypes, true)- const typeText = typeNode.getText()
- extendedTypes.push(
- this.parseGenericTypeCall(typeText) ?? ts.factory.createIdentifier(typeText),
- )
+ extendedTypes.push(getTypeBoxType(typeNode))Add import (outside the changed hunk): import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'Then remove Also applies to: 60-66 🤖 Prompt for AI Agents |
||
|
|
||
| private parseGenericTypeCall(typeText: string): ts.Expression | null { | ||
|
|
@@ -82,9 +57,7 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler { | |
| const extendedTypes: ts.Expression[] = [] | ||
|
|
||
| for (const heritageClause of heritageClauses) { | ||
| if (heritageClause.getToken() !== ts.SyntaxKind.ExtendsKeyword) { | ||
| continue | ||
| } | ||
| if (heritageClause.getToken() !== ts.SyntaxKind.ExtendsKeyword) continue | ||
|
|
||
| for (const typeNode of heritageClause.getTypeNodes()) { | ||
| const typeText = typeNode.getText() | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,20 +1,18 @@ | ||||||||||||||||||
| import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' | ||||||||||||||||||
| import { isValidIdentifier } from '@daxserver/validation-schema-codegen/utils/identifier-utils' | ||||||||||||||||||
| import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' | ||||||||||||||||||
| import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' | ||||||||||||||||||
| import { PropertySignature, ts } from 'ts-morph' | ||||||||||||||||||
| import { Node, PropertySignature, ts } from 'ts-morph' | ||||||||||||||||||
|
|
||||||||||||||||||
| export abstract class ObjectLikeBaseHandler extends BaseTypeHandler { | ||||||||||||||||||
| protected processProperties(properties: PropertySignature[]): ts.PropertyAssignment[] { | ||||||||||||||||||
| const propertyAssignments: ts.PropertyAssignment[] = [] | ||||||||||||||||||
|
|
||||||||||||||||||
| for (const prop of properties) { | ||||||||||||||||||
| const propName = prop.getName() | ||||||||||||||||||
| const propTypeNode = prop.getTypeNode() | ||||||||||||||||||
| if (!propTypeNode) continue | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!propTypeNode) { | ||||||||||||||||||
| continue | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| const outputNameNode = this.extractPropertyNameInfo(prop) | ||||||||||||||||||
| const valueExpr = getTypeBoxType(propTypeNode) | ||||||||||||||||||
| const isAlreadyOptional = | ||||||||||||||||||
| ts.isCallExpression(valueExpr) && | ||||||||||||||||||
|
|
@@ -26,11 +24,7 @@ export abstract class ObjectLikeBaseHandler extends BaseTypeHandler { | |||||||||||||||||
| ? makeTypeCall('Optional', [valueExpr]) | ||||||||||||||||||
| : valueExpr | ||||||||||||||||||
|
|
||||||||||||||||||
| const nameNode = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(propName) | ||||||||||||||||||
| ? ts.factory.createIdentifier(propName) | ||||||||||||||||||
| : ts.factory.createStringLiteral(propName) | ||||||||||||||||||
|
|
||||||||||||||||||
| propertyAssignments.push(ts.factory.createPropertyAssignment(nameNode, maybeOptional)) | ||||||||||||||||||
| propertyAssignments.push(ts.factory.createPropertyAssignment(outputNameNode, maybeOptional)) | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| return propertyAssignments | ||||||||||||||||||
|
|
@@ -41,4 +35,32 @@ export abstract class ObjectLikeBaseHandler extends BaseTypeHandler { | |||||||||||||||||
|
|
||||||||||||||||||
| return makeTypeCall('Object', [objectLiteral]) | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| private extractPropertyNameInfo(prop: PropertySignature): ts.PropertyName { | ||||||||||||||||||
| const nameNode = prop.getNameNode() | ||||||||||||||||||
| let propName: string | ||||||||||||||||||
| let shouldUseIdentifier: boolean | ||||||||||||||||||
|
|
||||||||||||||||||
| if (Node.isIdentifier(nameNode)) { | ||||||||||||||||||
| // If it was originally an identifier, keep it as an identifier | ||||||||||||||||||
| propName = nameNode.getText() | ||||||||||||||||||
| shouldUseIdentifier = true | ||||||||||||||||||
| } else if (Node.isStringLiteral(nameNode)) { | ||||||||||||||||||
| // For quoted properties, get the literal value and check if it can be an identifier | ||||||||||||||||||
| propName = nameNode.getLiteralValue() | ||||||||||||||||||
| shouldUseIdentifier = isValidIdentifier(propName) | ||||||||||||||||||
| } else if (Node.isNumericLiteral(nameNode)) { | ||||||||||||||||||
| // Numeric properties can be used as identifiers | ||||||||||||||||||
| propName = nameNode.getLiteralValue().toString() | ||||||||||||||||||
| shouldUseIdentifier = true | ||||||||||||||||||
| } else { | ||||||||||||||||||
|
Comment on lines
+53
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: numeric keys emitted as Identifier nodes Creating an Identifier with text "123" is invalid; numeric keys must be a NumericLiteral node. This can lead to malformed AST and potential printer/syntax issues. Apply this diff: - } else if (Node.isNumericLiteral(nameNode)) {
- // Numeric properties can be used as identifiers
- propName = nameNode.getLiteralValue().toString()
- shouldUseIdentifier = true
+ } else if (Node.isNumericLiteral(nameNode)) {
+ // Preserve numeric keys as numeric literals
+ return ts.factory.createNumericLiteral(nameNode.getLiteralValue().toString())📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| // Fallback for any other cases | ||||||||||||||||||
| propName = prop.getName() | ||||||||||||||||||
| shouldUseIdentifier = isValidIdentifier(propName) | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| return shouldUseIdentifier | ||||||||||||||||||
| ? ts.factory.createIdentifier(propName) | ||||||||||||||||||
| : ts.factory.createStringLiteral(propName) | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correctness and clarity: numeric key handling and list punctuation
Apply this diff:
📝 Committable suggestion
🧰 Tools
🪛 LanguageTool
[grammar] ~35-~35: There might be a mistake here.
Context: ....getText()
and preserved as identifiers - **String literals** ('prop-name',"prop...(QB_NEW_EN)
[grammar] ~36-~36: There might be a mistake here.
Context: ...d validated for identifier compatibility - Numeric literals (
123) - extracted u...(QB_NEW_EN)
🤖 Prompt for AI Agents