@@ -19,6 +19,7 @@ import {isEqual, isObject} from 'lodash'
1919import { Rule } from '../legacy/Rule'
2020import { OWN_PROPS_NAME } from '../legacy/types/constants'
2121import {
22+ type ArrayElement ,
2223 type ArrayTypeDef ,
2324 type CommonTypeDef ,
2425 type CoreTypeDef ,
@@ -62,10 +63,42 @@ export class DescriptorConverter {
6263 let value = this . cache . get ( schema )
6364 if ( value ) return value
6465
65- const builder = new SetBuilder ( )
66- for ( const name of schema . getLocalTypeNames ( ) ) {
67- const typeDef = convertTypeDef ( schema . get ( name ) ! , { } )
68- builder . addObject ( 'sanity.schema.namedType' , { name, typeDef} )
66+ const options : Options = {
67+ fields : new Map ( ) ,
68+ duplicateFields : new Map ( ) ,
69+ arrayElements : new Map ( ) ,
70+ duplicateArrayElements : new Map ( ) ,
71+ }
72+
73+ const namedTypes = schema . getLocalTypeNames ( ) . map ( ( name ) => {
74+ const typeDef = convertTypeDef ( schema . get ( name ) ! , name , options )
75+ return { name, typeDef}
76+ } )
77+
78+ const rewriteMap = new Map < EncodableObject , EncodableObject > ( )
79+
80+ // First we populate the rewrite map with the duplications:
81+ for ( const [ fieldDef , key ] of options . duplicateFields . entries ( ) ) {
82+ rewriteMap . set ( fieldDef , { __type : 'hoisted' , key} )
83+ }
84+
85+ for ( const [ arrayElem , key ] of options . duplicateArrayElements . entries ( ) ) {
86+ rewriteMap . set ( arrayElem , { __type : 'hoisted' , key} )
87+ }
88+
89+ const builder = new SetBuilder ( { rewriteMap} )
90+
91+ // Now we can build the de-duplicated objects:
92+ for ( const [ fieldDef , key ] of options . duplicateFields . entries ( ) ) {
93+ builder . addObject ( 'sanity.schema.hoisted' , { key, value : { ...fieldDef } } )
94+ }
95+
96+ for ( const [ arrayElem , key ] of options . duplicateArrayElements . entries ( ) ) {
97+ builder . addObject ( 'sanity.schema.hoisted' , { key, value : { ...arrayElem } } )
98+ }
99+
100+ for ( const namedType of namedTypes ) {
101+ builder . addObject ( 'sanity.schema.namedType' , namedType )
69102 }
70103
71104 if ( schema . parent ) {
@@ -78,21 +111,35 @@ export class DescriptorConverter {
78111 }
79112}
80113
81- function convertCommonTypeDef ( schemaType : SchemaType , opts : Options ) : CommonTypeDef {
114+ function convertCommonTypeDef ( schemaType : SchemaType , path : string , opts : Options ) : CommonTypeDef {
82115 // Note that OWN_PROPS_NAME is only set on subtypes, not the core types.
83116 // We might consider setting OWN_PROPS_NAME on _all_ types to avoid this branch.
84117 const ownProps = OWN_PROPS_NAME in schemaType ? ( schemaType as any ) [ OWN_PROPS_NAME ] : schemaType
85118
86119 let fields : ObjectField [ ] | undefined
87120 if ( Array . isArray ( ownProps . fields ) ) {
88- fields = ( ownProps . fields as ObjectSchemaType [ 'fields' ] ) . map (
89- ( { name, group, fieldset, type} ) => ( {
121+ fields = ( ownProps . fields as ObjectSchemaType [ 'fields' ] ) . map ( ( field ) => {
122+ const fieldPath = `${ path } .${ field . name } `
123+ const value = opts . fields . get ( field )
124+ if ( value ) {
125+ // We've seen it before. Mark it as duplicate.
126+ const otherPath = opts . duplicateFields . get ( value )
127+ // We always keep the _smallest_ path around.
128+ if ( ! otherPath || isLessCanonicalName ( fieldPath , otherPath ) )
129+ opts . duplicateFields . set ( value , fieldPath )
130+ return value
131+ }
132+
133+ const { name, group, fieldset, type} = field
134+ const converted : ObjectField = {
90135 name,
91- typeDef : convertTypeDef ( type , opts ) ,
136+ typeDef : convertTypeDef ( type , fieldPath , opts ) ,
92137 groups : arrayifyString ( group ) ,
93138 fieldset,
94- } ) ,
95- )
139+ }
140+ opts . fields . set ( field , converted )
141+ return converted
142+ } )
96143 }
97144
98145 let fieldsets : ObjectFieldset [ ] | undefined
@@ -157,15 +204,28 @@ function convertCommonTypeDef(schemaType: SchemaType, opts: Options): CommonType
157204 }
158205}
159206
160- /**
161- * Options used when converting the schema.
162- *
163- * We know we need this in order to handle validations.
164- **/
165- export type Options = Record < never , never >
207+ type Options = {
208+ /** Mapping of fields to descriptor value. Used for de-duping. */
209+ fields : Map < object , ObjectField >
210+
211+ /**
212+ * Once a field has been seen twice it's inserted into this map.
213+ * The value here is the canonical name.
214+ **/
215+ duplicateFields : Map < ObjectField , string >
216+
217+ /** Mapping of array element to descriptor value. Used for de-duping. */
218+ arrayElements : Map < object , ArrayElement >
219+
220+ /**
221+ * Once an array element has been seen twice it's inserted into this map.
222+ * The value here is the canonical name.
223+ **/
224+ duplicateArrayElements : Map < ArrayElement , string >
225+ }
166226
167- export function convertTypeDef ( schemaType : SchemaType , opts : Options ) : TypeDef {
168- const common = convertCommonTypeDef ( schemaType , opts )
227+ export function convertTypeDef ( schemaType : SchemaType , path : string , opts : Options ) : TypeDef {
228+ const common = convertCommonTypeDef ( schemaType , path , opts )
169229
170230 if ( ! schemaType . type ) {
171231 return {
@@ -183,10 +243,24 @@ export function convertTypeDef(schemaType: SchemaType, opts: Options): TypeDef {
183243 case 'array' : {
184244 return {
185245 extends : 'array' ,
186- of : ( schemaType as ArraySchemaType ) . of . map ( ( ofType ) => ( {
187- name : ofType . name ,
188- typeDef : convertTypeDef ( ofType , opts ) ,
189- } ) ) ,
246+ of : ( schemaType as ArraySchemaType ) . of . map ( ( ofType , idx ) => {
247+ const itemPath = `${ path } .${ ofType . name } `
248+ const value = opts . arrayElements . get ( ofType )
249+ if ( value ) {
250+ // We've seen it before. Mark it as duplicate.
251+ const otherPath = opts . duplicateArrayElements . get ( value )
252+ // We always keep the _smallest_ path around.
253+ if ( ! otherPath || isLessCanonicalName ( itemPath , otherPath ) )
254+ opts . duplicateArrayElements . set ( value , itemPath )
255+ return value
256+ }
257+ const converted : ArrayElement = {
258+ name : ofType . name ,
259+ typeDef : convertTypeDef ( ofType , `${ path } .${ ofType . name } ` , opts ) ,
260+ }
261+ opts . arrayElements . set ( ofType , converted )
262+ return converted
263+ } ) ,
190264 ...common ,
191265 } satisfies ArrayTypeDef
192266 }
@@ -727,3 +801,10 @@ function maybeOrderingBy(val: unknown): ObjectOrderingBy | undefined {
727801
728802 return { field, direction}
729803}
804+
805+ /**
806+ * Checks if `a` is smaller than `b` for determining a canonical name.
807+ */
808+ function isLessCanonicalName ( a : string , b : string ) : boolean {
809+ return a . length < b . length || ( a . length === b . length && a < b )
810+ }
0 commit comments