diff --git a/packages/js/src/cli/actions/genDiff.ts b/packages/js/src/cli/actions/genDiff.ts index 61be2f6..7be779a 100644 --- a/packages/js/src/cli/actions/genDiff.ts +++ b/packages/js/src/cli/actions/genDiff.ts @@ -20,7 +20,6 @@ export class GenDiffAction implements IAction { } const spec = specResult.data spec.designSystems = [] - spec.features = [] const code = this.container.fsUtils.readGeneratedFiles(outputDir) const codeSpec = this.container.specGenerator.fromCode(code) if (this.container.specUtils.isEqual(spec, codeSpec)) { diff --git a/packages/js/src/generators/code/base.ts b/packages/js/src/generators/code/base.ts index 58fb1c0..73efca0 100644 --- a/packages/js/src/generators/code/base.ts +++ b/packages/js/src/generators/code/base.ts @@ -3,7 +3,7 @@ import type { TechSpec } from '../../spec/types' export abstract class BaseCodeGenerator { abstract genCode (items: T[]): ts.Node[] - buildVariable ( + protected buildVariable ( name: string, value: ts.Expression, isExported: boolean = false ): ts.VariableStatement { const modifiers: ts.ModifierLike[] = [] @@ -26,7 +26,7 @@ export abstract class BaseCodeGenerator { ) } - buildAsConst (value: ts.Expression): ts.Expression { + protected buildAsConst (value: ts.Expression): ts.AsExpression { return ts.factory.createAsExpression( value, ts.factory.createTypeReferenceNode( @@ -36,31 +36,41 @@ export abstract class BaseCodeGenerator { ) } - buildObject ( + protected buildStringOrNumber ( + value: string | number + ): ts.StringLiteral | ts.NumericLiteral { + if (typeof value === 'string') return this.buildString(value) + return this.buildNumber(value) + } + + protected buildObject ( properties: ts.PropertyAssignment[], multiline: boolean = true ): ts.ObjectLiteralExpression { return ts.factory.createObjectLiteralExpression(properties, multiline) } - buildProperty (name: string, value: ts.Expression): ts.PropertyAssignment { + protected buildProperty ( + name: string, + value: ts.Expression + ): ts.PropertyAssignment { return ts.factory.createPropertyAssignment( ts.factory.createIdentifier(name), value ) } - buildStringProperties ( + protected buildStringProperties ( items: Array<[string, ts.Expression]> ): ts.PropertyAssignment[] { return items.map(([key, value]) => this.buildProperty(key, value)) } - buildBoolean (value: boolean): ts.BooleanLiteral { + protected buildBoolean (value: boolean): ts.BooleanLiteral { return value ? ts.factory.createTrue() : ts.factory.createFalse() } - buildStringOrNull ( + protected buildStringOrNull ( value: string | null ): ts.StringLiteral | ts.NullLiteral { return value !== null @@ -68,15 +78,15 @@ export abstract class BaseCodeGenerator { : ts.factory.createNull() } - buildString (value: string): ts.StringLiteral { + protected buildString (value: string): ts.StringLiteral { return ts.factory.createStringLiteral(value) } - buildNumber (value: number): ts.NumericLiteral { + protected buildNumber (value: number): ts.NumericLiteral { return ts.factory.createNumericLiteral(value) } - buildNumberOrNull ( + protected buildNumberOrNull ( value: number | null ): ts.NumericLiteral | ts.NullLiteral { return value !== null @@ -84,7 +94,7 @@ export abstract class BaseCodeGenerator { : ts.factory.createNull() } - buildImportStatement ( + protected buildImportStatement ( imports: string[], from: string ): ts.ImportDeclaration { return ts.factory.createImportDeclaration( @@ -107,15 +117,15 @@ export abstract class BaseCodeGenerator { ) } - buildArray (items: ts.Expression[]): ts.ArrayLiteralExpression { + protected buildArray (items: ts.Expression[]): ts.ArrayLiteralExpression { return ts.factory.createArrayLiteralExpression(items) } - buildStringArray (items: string[]): ts.ArrayLiteralExpression { + protected buildStringArray (items: string[]): ts.ArrayLiteralExpression { return this.buildArray(items.map(this.buildString)) } - buildStringArrayOrNull ( + protected buildStringArrayOrNull ( items: string[] | null ): ts.ArrayLiteralExpression | ts.NullLiteral { return items === null diff --git a/packages/js/src/generators/code/features.ts b/packages/js/src/generators/code/features.ts new file mode 100644 index 0000000..3deb8fd --- /dev/null +++ b/packages/js/src/generators/code/features.ts @@ -0,0 +1,51 @@ +import type ts from 'typescript' +import { type Feature } from '../../spec/types' +import { BaseCodeGenerator } from './base' +import type { FeatureFieldSpec, FeatureSpec } from '../../spec/types/feature' + +export class FeaturesCodeGenerator extends BaseCodeGenerator { + genCode (items: Feature[]): ts.Node[] { + return items + .map(item => this.genFeatureVariable(item)) + .filter((item): item is ts.VariableStatement => item !== null) + } + + private genFeatureVariable ( + item: Feature + ): ts.VariableStatement | null { + return this.buildVariable( + this.featureNameToCodeName(item.metadata.name), + this.buildFeatureValue(item.spec), + true + ) + } + + private featureNameToCodeName (name: string): string { + return name + 'Feature' + } + + private buildFeatureValue (spec: FeatureSpec): ts.AsExpression { + return this.buildAsConst( + this.buildObject( + this.buildStringProperties( + Object.entries(spec) + .map(([pName, pValue]): [ + string, ts.ObjectLiteralExpression + ] => ([pName, this.buildFeatureFieldValue(pValue)])) + ), + true + ) + ) + } + + private buildFeatureFieldValue ( + spec: FeatureFieldSpec + ): ts.ObjectLiteralExpression { + return this.buildObject( + this.buildStringProperties([ + ['type', this.buildString(spec.type)], + ['value', this.buildStringOrNumber(spec.value)] + ]), true + ) + } +} diff --git a/packages/js/src/generators/code/main.ts b/packages/js/src/generators/code/main.ts index 57998f1..80170d0 100644 --- a/packages/js/src/generators/code/main.ts +++ b/packages/js/src/generators/code/main.ts @@ -3,6 +3,7 @@ import type { TechSpec, TechSpecContainer } from '../../spec/types' import { FormsCodeGenerator } from '../code/forms' import { DesignSystemCodeGenerator } from './designSystem' import { TypesCodeGenerator } from './types' +import { FeaturesCodeGenerator } from './features' export class CodeFactory { private readonly printer: Printer @@ -10,6 +11,7 @@ export class CodeFactory { private readonly forms: FormsCodeGenerator private readonly designSystems: DesignSystemCodeGenerator private readonly types: TypesCodeGenerator + private readonly features: FeaturesCodeGenerator constructor () { this.printer = ts.createPrinter({ }) this.sourceFile = ts.createSourceFile( @@ -22,6 +24,7 @@ export class CodeFactory { this.types = new TypesCodeGenerator() this.forms = new FormsCodeGenerator(this.types) this.designSystems = new DesignSystemCodeGenerator() + this.features = new FeaturesCodeGenerator() } generate ( @@ -32,21 +35,23 @@ export class CodeFactory { DesignSystem: this.render( this.designSystems.genCode(spec.designSystems) ), - feature: undefined, + feature: this.render( + this.features.genCode(spec.features) + ), type: this.render(this.types.genCode(spec.types)) } } protected render (nodes: ts.Node[]): string | undefined { if (nodes.length === 0) return undefined - const string = this.printer.printList( + const sourceCode = this.printer.printList( ts.ListFormat.MultiLine, ts.factory.createNodeArray(nodes), this.sourceFile ) return JSON.parse( JSON.stringify( - string.replace(/\\u/g, '%u') + sourceCode.replace(/\\u/g, '%u') ).replace(/%u/g, '\\u') ) } diff --git a/packages/js/src/generators/spec/generators/base.ts b/packages/js/src/generators/spec/generators/base.ts index 0ed758b..cc6cef1 100644 --- a/packages/js/src/generators/spec/generators/base.ts +++ b/packages/js/src/generators/spec/generators/base.ts @@ -5,6 +5,10 @@ export abstract class BaseSpecGenerator { abstract getSpec (nodes: ts.Node[]): T[] protected abstract codeNameToSpecName (codeName: string): string + protected buildFieldErrorMessage (field: string): string { + return `"${field}" property is not defined or has invalid type` + } + protected extractRegex (node: ts.RegularExpressionLiteral): RegExp { return new RegExp(node.text.slice(1, node.text.length - 1)) } @@ -42,7 +46,7 @@ export abstract class BaseSpecGenerator { if ( declaration.initializer?.kind !== ts.SyntaxKind.AsExpression && - (declaration.initializer as ts.AsExpression).expression.kind !== + (declaration.initializer as ts.AsExpression).expression?.kind !== ts.SyntaxKind.ObjectLiteralExpression ) return null const value = ( @@ -92,4 +96,29 @@ export abstract class BaseSpecGenerator { case ts.SyntaxKind.NullKeyword: return null } } + + protected extractKeyFromProperties ( + properties: Array<[string, ts.Expression]>, + key: string, + kind: T['kind'] + ): T | null { + for (const [pName, pValue] of properties) { + if (pName === key && pValue.kind === kind) return pValue as T + } + return null + } + + protected filterObjectProperties ( + properties: ts.NodeArray + ): Array<[string, ts.Expression]> { + return properties + .filter((el): el is ts.PropertyAssignment => ( + el.kind === ts.SyntaxKind.PropertyAssignment + )) + .map((el): [string, ts.Expression] | null => { + if (el.name.kind !== ts.SyntaxKind.Identifier) return null + return [el.name.text, el.initializer] + }) + .filter((el): el is [string, ts.Expression] => el !== null) + } } diff --git a/packages/js/src/generators/spec/generators/features.ts b/packages/js/src/generators/spec/generators/features.ts new file mode 100644 index 0000000..eec3935 --- /dev/null +++ b/packages/js/src/generators/spec/generators/features.ts @@ -0,0 +1,128 @@ +import ts from 'typescript' +import type { Feature } from '../../../spec/types' +import { BaseSpecGenerator } from './base' +import type { + FeatureFieldSpec, + FeatureNumberFieldSpec, + FeatureNumberFieldType, + FeatureSpec, + FeatureStringFieldSpec, + FeatureStringFieldType +} from '../../../spec/types/feature' +import { featureSpecTypes } from './types' + +export class FeaturesSpecGenerator extends BaseSpecGenerator { + getSpec (nodes: ts.Node[]): Feature[] { + return nodes + .map(node => this.extractNameAndValueFromVariable(node)) + .filter((f): f is [string, ts.ObjectLiteralExpression] => ( + f !== null + )) + .map(([formName, formValue]) => ( + this.genFeature(formName, formValue) + )) + } + + protected codeNameToSpecName (codeName: string): string { + return codeName.slice(0, codeName.length - 'Feature'.length) + } + + private genFeature ( + name: string, + fValue: ts.ObjectLiteralExpression + ): Feature { + const spec = this.buildFeatureSpec(fValue) + if (typeof spec === 'string') { + throw new Error( + `feature:${name}: ${this.buildFieldErrorMessage(spec)}` + ) + } + return { + type: 'feature', + metadata: { name }, + spec + } + } + + private buildFeatureSpec ( + value: ts.ObjectLiteralExpression + ): FeatureSpec | string { + const properties = this.filterObjectProperties(value.properties) + .filter((p): p is [string, ts.ObjectLiteralExpression] => ( + ts.isObjectLiteralExpression(p[1]) + )) + const spec: FeatureSpec = {} + for (const [fName, fValue] of properties) { + const fieldSpec = this.buildFeatureFieldSpec(fValue) + if (typeof fieldSpec === 'string') { + return fName + '.' + fieldSpec + } + spec[fName] = fieldSpec + } + return spec + } + + private buildFeatureFieldSpec ( + value: ts.ObjectLiteralExpression + ): FeatureFieldSpec | 'type' | 'value' { + const properties = this.filterObjectProperties(value.properties) + const typeNode = this.extractKeyFromProperties( + properties, + 'type', + ts.SyntaxKind.StringLiteral + ) + if (typeNode === null) return 'type' + if (!featureSpecTypes.includes(typeNode.text as any)) return 'type' + const type = typeNode.text as FeatureFieldSpec['type'] + switch (type) { + case 'uuid': + case 'link': + case 'date': + case 'time': + case 'date-time': + case 'duration': + case 'string': return this.buildStringSpec(type, properties) + case 'int': + case 'float': + case 'uint': return this.buildNumberSpec(type, properties) + } + } + + private buildStringSpec ( + type: FeatureStringFieldType, + properties: Array<[string, ts.Expression]> + ): FeatureStringFieldSpec | 'value' { + const node = this.extractKeyFromProperties( + properties, + 'value', + ts.SyntaxKind.StringLiteral + ) + if (node === null) return 'value' + return { type, value: node.text } + } + + private buildNumberSpec ( + type: FeatureNumberFieldType, + properties: Array<[string, ts.Expression]> + ): FeatureNumberFieldSpec | 'value' { + const node = this.extractKeyFromProperties( + properties, + 'value', + ts.SyntaxKind.NumericLiteral + ) + if (node === null) return 'value' + switch (type) { + case 'int': + case 'uint': + return { + type, + value: this.extractInt(node) + } + case 'float': + return { + type, + value: this.extractFloat(node) + } + } + } +} diff --git a/packages/js/src/generators/spec/generators/main.ts b/packages/js/src/generators/spec/generators/main.ts index cbe3d52..118bd89 100644 --- a/packages/js/src/generators/spec/generators/main.ts +++ b/packages/js/src/generators/spec/generators/main.ts @@ -2,13 +2,16 @@ import ts from 'typescript' import type { TechSpec, TechSpecContainer } from '../../../spec/types' import { FormsSpecGenerator } from './forms' import { TypesSpecGenerator } from './types-gen' +import { FeaturesSpecGenerator } from './features' export class SpecGenerator { private readonly forms: FormsSpecGenerator private readonly types: TypesSpecGenerator + private readonly features: FeaturesSpecGenerator constructor () { this.forms = new FormsSpecGenerator() this.types = new TypesSpecGenerator() + this.features = new FeaturesSpecGenerator() } fromCode ( @@ -17,7 +20,9 @@ export class SpecGenerator { return { forms: this.forms.getSpec(this.extractNodesFromCode(code.form)), designSystems: [], - features: [], + features: this.features.getSpec( + this.extractNodesFromCode(code.feature) + ), types: this.types.getSpec(this.extractNodesFromCode(code.type)) } } diff --git a/packages/js/src/generators/spec/generators/types-gen.ts b/packages/js/src/generators/spec/generators/types-gen.ts index fe214e1..dece2ac 100644 --- a/packages/js/src/generators/spec/generators/types-gen.ts +++ b/packages/js/src/generators/spec/generators/types-gen.ts @@ -157,17 +157,13 @@ export class TypesSpecGenerator extends BaseSpecGenerator { } } - private buildTypeErrorMessage (field: string): string { - return `"${field}" property is not defined or has invalid type` - } - private genTypeAst ( expression: ts.ObjectLiteralExpression ): TypeAst | string { const properties = this.filterObjectProperties(expression.properties) const typeString = this.extractTypeFromProperties(properties) if (typeString === null) { - return this.buildTypeErrorMessage('type') + return this.buildFieldErrorMessage('type') } switch (typeString) { case 'string': return this.buildStringTypeAst(properties) @@ -184,20 +180,6 @@ export class TypesSpecGenerator extends BaseSpecGenerator { } } - private filterObjectProperties ( - properties: ts.NodeArray - ): Array<[string, ts.Expression]> { - return properties - .filter((el): el is ts.PropertyAssignment => ( - el.kind === ts.SyntaxKind.PropertyAssignment - )) - .map((el): [string, ts.Expression] | null => { - if (el.name.kind !== ts.SyntaxKind.Identifier) return null - return [el.name.text, el.initializer] - }) - .filter((el): el is [string, ts.Expression] => el !== null) - } - private buildStringTypeAst ( properties: Array<[string, ts.Expression]> ): StringTypeAst | string { @@ -212,7 +194,7 @@ export class TypesSpecGenerator extends BaseSpecGenerator { } } } - return this.buildTypeErrorMessage('regex') + return this.buildFieldErrorMessage('regex') } private buildNumericTypeAst ( @@ -230,8 +212,8 @@ export class TypesSpecGenerator extends BaseSpecGenerator { if (pName === 'max') max = pValue as NumericTypeAst['max'] } } - if (min === null) return this.buildTypeErrorMessage('min') - if (max === null) return this.buildTypeErrorMessage('max') + if (min === null) return this.buildFieldErrorMessage('min') + if (max === null) return this.buildFieldErrorMessage('max') return { type: typeName, min, @@ -257,7 +239,7 @@ export class TypesSpecGenerator extends BaseSpecGenerator { } } } - return this.buildTypeErrorMessage('allowOnly') + return this.buildFieldErrorMessage('allowOnly') } private buildFileTypeAst ( @@ -268,7 +250,7 @@ export class TypesSpecGenerator extends BaseSpecGenerator { for (const field of ['minSize', 'maxSize', 'allowedMimeTypes']) { if ( ast[field as keyof FileTypeAst] === undefined - ) return this.buildTypeErrorMessage(field) + ) return this.buildFieldErrorMessage(field) } return ast as FileTypeAst } @@ -304,7 +286,7 @@ export class TypesSpecGenerator extends BaseSpecGenerator { if (ts.isObjectLiteralExpression(pValue)) { const size = this.buildFileSizeAst(pValue) if (typeof size === 'string') { - return this.buildTypeErrorMessage(`${pName}.${size}`) + return this.buildFieldErrorMessage(`${pName}.${size}`) } ast[pName] = size } else if (pValue.kind === ts.SyntaxKind.NullKeyword) { @@ -345,7 +327,7 @@ export class TypesSpecGenerator extends BaseSpecGenerator { if (typeof ast === 'string') return ast for (const pName of imageTypeSpecPropertyNames) { if (ast[pName] === undefined) { - return this.buildTypeErrorMessage(pName) + return this.buildFieldErrorMessage(pName) } } return ast as ImageTypeAst @@ -424,8 +406,8 @@ export class TypesSpecGenerator extends BaseSpecGenerator { } } } - if (items === null) return this.buildTypeErrorMessage('items') - if (itemType === null) return this.buildTypeErrorMessage('itemType') + if (items === null) return this.buildFieldErrorMessage('items') + if (itemType === null) return this.buildFieldErrorMessage('itemType') return { type: 'enum', items, @@ -441,7 +423,7 @@ export class TypesSpecGenerator extends BaseSpecGenerator { if (!ts.isArrayLiteralExpression(pValue)) continue return { type: 'union', types: pValue } } - return this.buildTypeErrorMessage('types') + return this.buildFieldErrorMessage('types') } private setAspectRatioAst ( @@ -471,19 +453,13 @@ export class TypesSpecGenerator extends BaseSpecGenerator { private extractTypeFromProperties ( properties: Array<[string, ts.Expression]> ): TypeSpec['type'] | null { - let t: TypeSpec['type'] | null = null - properties - .forEach(([pName, pValue]) => { - if ( - pName === 'type' && - pValue.kind === ts.SyntaxKind.StringLiteral && - typeSpecTypes.includes( - (pValue as ts.StringLiteral).text as any - ) - ) { - t = (pValue as ts.StringLiteral).text as TypeSpec['type'] - } - }) - return t + const typeNode = this.extractKeyFromProperties( + properties, + 'type', + ts.SyntaxKind.StringLiteral + ) + if (typeNode === null) return null + if (!typeSpecTypes.includes(typeNode.text as any)) return null + return typeNode.text as TypeSpec['type'] } } diff --git a/packages/js/src/generators/spec/generators/types.ts b/packages/js/src/generators/spec/generators/types.ts index 44df632..6a84216 100644 --- a/packages/js/src/generators/spec/generators/types.ts +++ b/packages/js/src/generators/spec/generators/types.ts @@ -14,6 +14,7 @@ import type { UnionTypeSpec } from '../../../spec/types/type' import { stringUnionToArray } from '../../../utils' +import { type FeatureFieldSpec } from '../../../spec/types/feature' export interface FormFieldAst { type: ts.StringLiteral @@ -126,3 +127,17 @@ export const imageTypeSpecPropertyNames = stringUnionToArray< 'minHeight', 'maxHeight' ) + +export const featureSpecTypes = stringUnionToArray()( + 'string', + 'uuid', + 'string', + 'link', + 'date', + 'time', + 'date-time', + 'duration', + 'int', + 'uint', + 'float' +)