1- import ts from 'typescript' ;
1+ import type ts from 'typescript' ;
22
33import type { SchemaWithType } from '~/plugins' ;
44import { toRef } from '~/plugins/shared/utils/refs' ;
5- import { tsc } from '~/tsc' ;
6- import { numberRegExp } from '~/utils/regexp' ;
5+ import { $ , type CallTsDsl } from '~/ts-dsl' ;
76
87import { pipesToAst } from '../../shared/pipesToAst' ;
98import type { Ast , IrSchemaToAstOptions } from '../../shared/types' ;
9+ import type { ObjectBaseResolverArgs } from '../../types' ;
1010import { identifiers } from '../constants' ;
1111import { irSchemaToAst } from '../plugin' ;
1212
13+ function defaultObjectBaseResolver ( {
14+ additional,
15+ pipes,
16+ plugin,
17+ shape,
18+ } : ObjectBaseResolverArgs ) : number {
19+ const v = plugin . referenceSymbol ( {
20+ category : 'external' ,
21+ resource : 'valibot.v' ,
22+ } ) ;
23+
24+ // Handle `additionalProperties: { type: 'never' }` → v.strictObject()
25+ if ( additional === null ) {
26+ return pipes . push (
27+ $ ( v . placeholder ) . attr ( identifiers . schemas . strictObject ) . call ( shape ) ,
28+ ) ;
29+ }
30+
31+ // Handle additionalProperties as schema → v.record() or v.objectWithRest()
32+ if ( additional ) {
33+ if ( shape . isEmpty ) {
34+ return pipes . push (
35+ $ ( v . placeholder )
36+ . attr ( identifiers . schemas . record )
37+ . call (
38+ $ ( v . placeholder ) . attr ( identifiers . schemas . string ) . call ( ) ,
39+ additional ,
40+ ) ,
41+ ) ;
42+ }
43+
44+ // If there are named properties, use v.objectWithRest() to validate both
45+ return pipes . push (
46+ $ ( v . placeholder )
47+ . attr ( identifiers . schemas . objectWithRest )
48+ . call ( shape , additional ) ,
49+ ) ;
50+ }
51+
52+ // Default case → v.object()
53+ return pipes . push (
54+ $ ( v . placeholder ) . attr ( identifiers . schemas . object ) . call ( shape ) ,
55+ ) ;
56+ }
57+
1358export const objectToAst = ( {
1459 plugin,
1560 schema,
@@ -18,10 +63,11 @@ export const objectToAst = ({
1863 schema : SchemaWithType < 'object' > ;
1964} ) : Omit < Ast , 'typeName' > => {
2065 const result : Partial < Omit < Ast , 'typeName' > > = { } ;
66+ const pipes : Array < CallTsDsl > = [ ] ;
2167
2268 // TODO: parser - handle constants
23- const properties : Array < ts . PropertyAssignment > = [ ] ;
2469
70+ const shape = $ . object ( ) . pretty ( ) ;
2571 const required = schema . required ?? [ ] ;
2672
2773 for ( const name in schema . properties ) {
@@ -37,130 +83,40 @@ export const objectToAst = ({
3783 path : toRef ( [ ...state . path . value , 'properties' , name ] ) ,
3884 } ,
3985 } ) ;
40- if ( propertyAst . hasLazyExpression ) {
41- result . hasLazyExpression = true ;
42- }
86+ if ( propertyAst . hasLazyExpression ) result . hasLazyExpression = true ;
4387
44- numberRegExp . lastIndex = 0 ;
45- let propertyName ;
46- if ( numberRegExp . test ( name ) ) {
47- // For numeric literals, we'll handle negative numbers by using a string literal
48- // instead of trying to use a PrefixUnaryExpression
49- propertyName = name . startsWith ( '-' )
50- ? ts . factory . createStringLiteral ( name )
51- : ts . factory . createNumericLiteral ( name ) ;
52- } else {
53- propertyName = name ;
54- }
55- // TODO: parser - abstract safe property name logic
56- if (
57- ( ( name . match ( / ^ [ 0 - 9 ] / ) && name . match ( / \D + / g) ) || name . match ( / \W / g) ) &&
58- ! name . startsWith ( "'" ) &&
59- ! name . endsWith ( "'" )
60- ) {
61- propertyName = `'${ name } '` ;
62- }
63- properties . push (
64- tsc . propertyAssignment ( {
65- initializer : pipesToAst ( { pipes : propertyAst . pipes , plugin } ) ,
66- name : propertyName ,
67- } ) ,
68- ) ;
69- }
70-
71- const v = plugin . referenceSymbol ( {
72- category : 'external' ,
73- resource : 'valibot.v' ,
74- } ) ;
75-
76- // Handle additionalProperties: false (which becomes type: 'never' in IR)
77- // Use v.strictObject() to forbid additional properties
78- if (
79- schema . additionalProperties &&
80- typeof schema . additionalProperties === 'object' &&
81- schema . additionalProperties . type === 'never'
82- ) {
83- result . pipes = [
84- tsc . callExpression ( {
85- functionName : tsc . propertyAccessExpression ( {
86- expression : v . placeholder ,
87- name : identifiers . schemas . strictObject ,
88- } ) ,
89- parameters : [
90- ts . factory . createObjectLiteralExpression ( properties , true ) ,
91- ] ,
92- } ) ,
93- ] ;
94- return result as Omit < Ast , 'typeName' > ;
88+ shape . prop ( name , pipesToAst ( { pipes : propertyAst . pipes , plugin } ) ) ;
9589 }
9690
97- // Handle additionalProperties with a schema (not just true/false)
98- // This supports objects with dynamic keys (e.g., Record<string, T>)
99- if (
100- schema . additionalProperties &&
101- typeof schema . additionalProperties === 'object' &&
102- schema . additionalProperties . type !== undefined
103- ) {
104- const additionalAst = irSchemaToAst ( {
105- plugin,
106- schema : schema . additionalProperties ,
107- state : {
108- ...state ,
109- path : toRef ( [ ...state . path . value , 'additionalProperties' ] ) ,
110- } ,
111- } ) ;
112- if ( additionalAst . hasLazyExpression ) {
113- result . hasLazyExpression = true ;
114- }
115-
116- // If there are no named properties, use v.record() directly
117- if ( ! Object . keys ( properties ) . length ) {
118- result . pipes = [
119- tsc . callExpression ( {
120- functionName : tsc . propertyAccessExpression ( {
121- expression : v . placeholder ,
122- name : identifiers . schemas . record ,
123- } ) ,
124- parameters : [
125- tsc . callExpression ( {
126- functionName : tsc . propertyAccessExpression ( {
127- expression : v . placeholder ,
128- name : identifiers . schemas . string ,
129- } ) ,
130- parameters : [ ] ,
131- } ) ,
132- pipesToAst ( { pipes : additionalAst . pipes , plugin } ) ,
133- ] ,
134- } ) ,
135- ] ;
136- return result as Omit < Ast , 'typeName' > ;
91+ let additional : ts . Expression | null | undefined ;
92+ if ( schema . additionalProperties && schema . additionalProperties . type ) {
93+ if ( schema . additionalProperties . type === 'never' ) {
94+ additional = null ;
95+ } else {
96+ const additionalAst = irSchemaToAst ( {
97+ plugin,
98+ schema : schema . additionalProperties ,
99+ state : {
100+ ...state ,
101+ path : toRef ( [ ...state . path . value , 'additionalProperties' ] ) ,
102+ } ,
103+ } ) ;
104+ if ( additionalAst . hasLazyExpression ) result . hasLazyExpression = true ;
105+ additional = pipesToAst ( { pipes : additionalAst . pipes , plugin } ) ;
137106 }
138-
139- // If there are named properties, use v.objectWithRest() to validate both
140- // The rest parameter is the schema for each additional property value
141- result . pipes = [
142- tsc . callExpression ( {
143- functionName : tsc . propertyAccessExpression ( {
144- expression : v . placeholder ,
145- name : identifiers . schemas . objectWithRest ,
146- } ) ,
147- parameters : [
148- ts . factory . createObjectLiteralExpression ( properties , true ) ,
149- pipesToAst ( { pipes : additionalAst . pipes , plugin } ) ,
150- ] ,
151- } ) ,
152- ] ;
153- return result as Omit < Ast , 'typeName' > ;
154107 }
155108
156- result . pipes = [
157- tsc . callExpression ( {
158- functionName : tsc . propertyAccessExpression ( {
159- expression : v . placeholder ,
160- name : identifiers . schemas . object ,
161- } ) ,
162- parameters : [ ts . factory . createObjectLiteralExpression ( properties , true ) ] ,
163- } ) ,
164- ] ;
109+ const args : ObjectBaseResolverArgs = {
110+ $,
111+ additional,
112+ pipes,
113+ plugin,
114+ schema,
115+ shape,
116+ } ;
117+ const resolver = plugin . config [ '~resolvers' ] ?. object ?. base ;
118+ if ( ! resolver ?.( args ) ) defaultObjectBaseResolver ( args ) ;
119+
120+ result . pipes = [ pipesToAst ( { pipes, plugin } ) ] ;
165121 return result as Omit < Ast , 'typeName' > ;
166122} ;
0 commit comments