@@ -194,6 +194,53 @@ function serializeValue(v) {
194194 return String ( v ) ; // numbers, booleans
195195}
196196
197+ /**
198+ * Schema `values` entries wrapped in `/.../` are regexes, not literals. The
199+ * enumerated `validateFunction` in src/lib/coerce.js unwraps and tests them.
200+ * Each one maps to a template literal type in src/types/lib/common.d.ts that
201+ * accepts the same strings.
202+ *
203+ * A regex stands in for a value set that the static schema cannot list,
204+ * because the real set depends on how many axes a figure declares.
205+ */
206+ const REGEX_VALUE_TYPES = new Map ( [
207+ [ '/^x([2-9]|[1-9][0-9]+)?( domain)?$/' , 'XAxisName' ] ,
208+ [ '/^y([2-9]|[1-9][0-9]+)?( domain)?$/' , 'YAxisName' ] ,
209+ [ '/^x([2-9]|[1-9][0-9]+)?y([2-9]|[1-9][0-9]+)?$/' , 'CartesianSubplotId' ]
210+ ] ) ;
211+
212+ /** Test whether a schema value uses the `/.../` regex convention. */
213+ function isRegexValue ( v ) {
214+ return typeof v === 'string' && v . length > 1 && v . startsWith ( '/' ) && v . endsWith ( '/' ) ;
215+ }
216+
217+ /**
218+ * Serialize one `values` entry from a string or enumerated attribute.
219+ *
220+ * Plain values become string literals. Regex values become the named type
221+ * that accepts the same strings.
222+ *
223+ * @param {string|number|boolean } v - The schema value
224+ * @param {string } attrPath - Dotted attribute path, used in the error message
225+ * @returns {string }
226+ * @throws If `v` is a regex with no entry in `REGEX_VALUE_TYPES`. Emitting the
227+ * regex source as a string literal would produce a type that only accepts
228+ * the pattern text, so failing here is deliberate.
229+ */
230+ function enumValueToTS ( v , attrPath ) {
231+ if ( ! isRegexValue ( v ) ) return serializeValue ( v ) ;
232+
233+ const typeName = REGEX_VALUE_TYPES . get ( v ) ;
234+ if ( ! typeName ) {
235+ throw new Error (
236+ `Unmapped regex value '${ v } ' at '${ attrPath } '. Add an entry to REGEX_VALUE_TYPES ` +
237+ `pointing at a template literal type in src/types/lib/common.d.ts that accepts ` +
238+ `the same strings.`
239+ ) ;
240+ }
241+ return typeName ;
242+ }
243+
197244/**
198245 * Try to match a values array to a known common type.
199246 * Returns the type name string or null.
@@ -215,32 +262,42 @@ function matchCommonType(values) {
215262 * When all elements share the same valType we can emit a precise tuple
216263 * or array type instead of `unknown[]`.
217264 */
218- function infoArrayToTS ( attr ) {
265+ function infoArrayToTS ( attr , attrPath ) {
219266 if ( ! attr . items ) return 'any[]' ;
220267
221268 // items can be an array of item descriptors or a single descriptor
222269 // (single descriptor = freeLength homogeneous array)
223270 if ( ! Array . isArray ( attr . items ) ) {
224- const elemType = simpleValType ( attr . items . valType ) ;
225- return `${ elemType } []` ;
271+ return `${ asArrayElement ( simpleValType ( attr . items , attrPath ) ) } []` ;
226272 }
227273
228- const elemTypes = attr . items . map ( ( item ) => simpleValType ( item . valType ) ) ;
274+ const elemTypes = attr . items . map ( ( item ) => simpleValType ( item , attrPath ) ) ;
229275
230276 if ( attr . freeLength ) {
231277 // Variable-length — use array of the union of element types
232278 const unique = [ ...new Set ( elemTypes ) ] ;
233279 const union = unique . length === 1 ? unique [ 0 ] : unique . join ( ' | ' ) ;
234- return `${ union } []` ;
280+ return `${ asArrayElement ( union ) } []` ;
235281 }
236282
237283 // Fixed-length — emit tuple
238284 return `[${ elemTypes . join ( ', ' ) } ]` ;
239285}
240286
241- /** Map a valType string to a simple TS type (no arrayOk handling). */
242- function simpleValType ( valType ) {
243- switch ( valType ) {
287+ /** Parenthesize a union so that appending `[]` binds to the whole union. */
288+ function asArrayElement ( type ) {
289+ return type . includes ( '|' ) ? `(${ type } )` : type ;
290+ }
291+
292+ /**
293+ * Map an info_array item descriptor to a simple TS type (no arrayOk handling).
294+ *
295+ * @param {object } item - The item descriptor, carrying at least `valType`
296+ * @param {string } attrPath - Dotted path of the owning attribute
297+ * @returns {string }
298+ */
299+ function simpleValType ( item , attrPath ) {
300+ switch ( item . valType ) {
244301 case 'number' :
245302 case 'integer' :
246303 return 'number' ;
@@ -251,6 +308,9 @@ function simpleValType(valType) {
251308 return 'boolean' ;
252309 case 'color' :
253310 return 'Color' ;
311+ case 'enumerated' :
312+ if ( ! Array . isArray ( item . values ) ) return 'any' ;
313+ return item . values . map ( ( v ) => enumValueToTS ( v , attrPath ) ) . join ( ' | ' ) ;
254314 default :
255315 return 'any' ;
256316 }
@@ -368,7 +428,7 @@ function valTypeToTS(attr, attrPath) {
368428 base = common ;
369429 break ;
370430 }
371- base = attr . values . map ( serializeValue ) . join ( ' | ' ) ;
431+ base = attr . values . map ( ( v ) => enumValueToTS ( v , attrPath ) ) . join ( ' | ' ) ;
372432 } else {
373433 base = 'string' ;
374434 }
@@ -405,7 +465,7 @@ function valTypeToTS(attr, attrPath) {
405465 base = common ;
406466 break ;
407467 }
408- base = attr . values . map ( serializeValue ) . join ( ' | ' ) ;
468+ base = attr . values . map ( ( v ) => enumValueToTS ( v , attrPath ) ) . join ( ' | ' ) ;
409469 } else {
410470 base = 'any' ;
411471 }
@@ -432,7 +492,7 @@ function valTypeToTS(attr, attrPath) {
432492 }
433493
434494 case 'info_array' :
435- return infoArrayToTS ( attr ) ;
495+ return infoArrayToTS ( attr , attrPath ) ;
436496
437497 case 'any' :
438498 return 'any' ;
@@ -1080,7 +1140,16 @@ export function generateSchemaTypes(schema, outputPath) {
10801140 ' * Do not edit by hand — run `npm run schema` to regenerate.' ,
10811141 ' */' ,
10821142 '' ,
1083- "import type { Color, ColorScale, Datum, MarkerSymbol, TypedArray } from '../lib/common';" ,
1143+ 'import type {' ,
1144+ ' CartesianSubplotId,' ,
1145+ ' Color,' ,
1146+ ' ColorScale,' ,
1147+ ' Datum,' ,
1148+ ' MarkerSymbol,' ,
1149+ ' TypedArray,' ,
1150+ ' XAxisName,' ,
1151+ ' YAxisName' ,
1152+ "} from '../lib/common';" ,
10841153 ''
10851154 ] ;
10861155
@@ -1265,7 +1334,21 @@ export function generateSchemaTypes(schema, outputPath) {
12651334 }
12661335 }
12671336
1268- fs . writeFileSync ( outputPath , chunks . join ( '\n' ) ) ;
1337+ const output = chunks . join ( '\n' ) ;
1338+
1339+ // Backstop for the `/.../` regex convention. `enumValueToTS` already fails
1340+ // on an unmapped regex and names the attribute, but it only sees `values`
1341+ // entries. This catches a regex that reaches the output by another route.
1342+ const leakedRegexes = output . match ( / ' \/ \^ [ ^ ' ] * ' / g) ;
1343+ if ( leakedRegexes ) {
1344+ throw new Error (
1345+ `Generated types contain regex string literals: ${ [ ...new Set ( leakedRegexes ) ] . join ( ', ' ) } . ` +
1346+ `A schema regex reached the output as a literal type, which only accepts the pattern ` +
1347+ `text itself. Map it in REGEX_VALUE_TYPES.`
1348+ ) ;
1349+ }
1350+
1351+ fs . writeFileSync ( outputPath , output ) ;
12691352
12701353 const sharedCount = sharedList . length ;
12711354 const layoutCount = subplotGroups . size + arrayItems . size + 1 ; // +1 for Layout itself
0 commit comments