@@ -25,7 +25,7 @@ import {
25
25
isValidSpacingMultiplier ,
26
26
} from '../../../../tailwindcss/src/utils/infer-data-type'
27
27
import { findStaticPlugins , type StaticPluginOptions } from '../../utils/extract-static-plugins'
28
- import { highlight , info , relative } from '../../utils/renderer'
28
+ import { highlight , info , relative , warn } from '../../utils/renderer'
29
29
30
30
const __filename = fileURLToPath ( import . meta. url )
31
31
const __dirname = path . dirname ( __filename )
@@ -48,11 +48,15 @@ export async function migrateJsConfig(
48
48
fs . readFile ( fullConfigPath , 'utf-8' ) ,
49
49
] )
50
50
51
- if ( ! canMigrateConfig ( unresolvedConfig , source ) ) {
51
+ let canMigrateConfigResult = canMigrateConfig ( unresolvedConfig , source )
52
+ if ( ! canMigrateConfigResult . valid ) {
52
53
info (
53
54
`The configuration file at ${ highlight ( relative ( fullConfigPath , base ) ) } could not be automatically migrated to the new CSS configuration format, so your CSS has been updated to load your existing configuration file.` ,
54
55
{ prefix : '↳ ' } ,
55
56
)
57
+ for ( let msg of canMigrateConfigResult . errors ) {
58
+ warn ( msg , { prefix : ' ↳ ' } )
59
+ }
56
60
return null
57
61
}
58
62
@@ -299,22 +303,34 @@ async function migrateContent(
299
303
return sources
300
304
}
301
305
302
- // Applies heuristics to determine if we can attempt to migrate the config
303
- function canMigrateConfig ( unresolvedConfig : Config , source : string ) : boolean {
304
- // The file may not contain non-serializable values
305
- function isSimpleValue ( value : unknown ) : boolean {
306
- if ( typeof value === 'function' ) return false
307
- if ( Array . isArray ( value ) ) return value . every ( isSimpleValue )
308
- if ( typeof value === 'object' && value !== null ) {
309
- return Object . values ( value ) . every ( isSimpleValue )
306
+ const JS_IDENTIFIER_REGEX = / ^ [ a - z A - Z _ $ ] [ a - z A - Z 0 - 9 _ $ ] * $ /
307
+ function stringifyPath ( path : ( string | number ) [ ] ) : string {
308
+ let result = ''
309
+
310
+ for ( let segment of path ) {
311
+ if ( typeof segment === 'number' ) {
312
+ result += `[${ segment } ]`
313
+ } else if ( ! JS_IDENTIFIER_REGEX . test ( segment ) ) {
314
+ result += `[\`${ segment } \`]`
315
+ } else {
316
+ result += result . length > 0 ? `.${ segment } ` : segment
310
317
}
311
- return [ 'string' , 'number' , 'boolean' , 'undefined' ] . includes ( typeof value )
312
318
}
313
319
314
- // `theme` and `plugins` are handled separately and allowed to be more complex
315
- let { plugins, theme, ...remainder } = unresolvedConfig
316
- if ( ! isSimpleValue ( remainder ) ) {
317
- return false
320
+ return result
321
+ }
322
+
323
+ // Applies heuristics to determine if we can attempt to migrate the config
324
+ function canMigrateConfig (
325
+ unresolvedConfig : Config ,
326
+ source : string ,
327
+ ) : { valid : true } | { valid : false ; errors : string [ ] } {
328
+ let theme = unresolvedConfig . theme
329
+ let errors : string [ ] = [ ]
330
+
331
+ // Migrating presets are not supported
332
+ if ( unresolvedConfig . presets && unresolvedConfig . presets . length > 0 ) {
333
+ errors . push ( 'Cannot migrate config files that use presets' )
318
334
}
319
335
320
336
// The file may only contain known-migratable top-level properties
@@ -328,27 +344,28 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean {
328
344
'corePlugins' ,
329
345
]
330
346
331
- if ( Object . keys ( unresolvedConfig ) . some ( ( key ) => ! knownProperties . includes ( key ) ) ) {
332
- return false
333
- }
334
-
335
- if ( findStaticPlugins ( source ) === null ) {
336
- return false
337
- }
338
-
339
- if ( unresolvedConfig . presets && unresolvedConfig . presets . length > 0 ) {
340
- return false
347
+ for ( let key of Object . keys ( unresolvedConfig ) ) {
348
+ if ( ! knownProperties . includes ( key ) ) {
349
+ errors . push ( `Cannot migrate unknown top-level key: \`${ key } \`` )
350
+ }
341
351
}
342
352
343
353
// Only migrate the config file if all top-level theme keys are allowed to be
344
354
// migrated
345
355
if ( theme && typeof theme === 'object' ) {
346
- if ( theme . extend && ! onlyAllowedThemeValues ( theme . extend ) ) return false
347
- let { extend : _extend , ...themeCopy } = theme
348
- if ( ! onlyAllowedThemeValues ( themeCopy ) ) return false
356
+ let { extend, ...themeCopy } = theme
357
+ errors . push ( ...onlyAllowedThemeValues ( themeCopy , [ 'theme' ] ) )
358
+
359
+ if ( extend ) {
360
+ errors . push ( ...onlyAllowedThemeValues ( extend , [ 'theme' , 'extend' ] ) )
361
+ }
349
362
}
350
363
351
- return true
364
+ // TODO: findStaticPlugins already logs errors for unsupported plugins, maybe
365
+ // it should return them instead?
366
+ findStaticPlugins ( source )
367
+
368
+ return errors . length <= 0 ? { valid : true } : { valid : false , errors }
352
369
}
353
370
354
371
const ALLOWED_THEME_KEYS = [
@@ -357,24 +374,30 @@ const ALLOWED_THEME_KEYS = [
357
374
'containers' ,
358
375
]
359
376
const BLOCKED_THEME_KEYS = [ 'supports' , 'data' , 'aria' ]
360
- function onlyAllowedThemeValues ( theme : ThemeConfig ) : boolean {
377
+ function onlyAllowedThemeValues ( theme : ThemeConfig , path : ( string | number ) [ ] ) : string [ ] {
378
+ let errors : string [ ] = [ ]
379
+
361
380
for ( let key of Object . keys ( theme ) ) {
362
381
if ( ! ALLOWED_THEME_KEYS . includes ( key ) ) {
363
- return false
382
+ errors . push ( `Cannot migrate theme key: \` ${ stringifyPath ( [ ... path , key ] ) } \`` )
364
383
}
384
+
365
385
if ( BLOCKED_THEME_KEYS . includes ( key ) ) {
366
- return false
386
+ errors . push ( `Cannot migrate theme key: \` ${ stringifyPath ( [ ... path , key ] ) } \`` )
367
387
}
368
388
}
369
389
370
390
if ( 'screens' in theme && typeof theme . screens === 'object' && theme . screens !== null ) {
371
- for ( let screen of Object . values ( theme . screens ) ) {
391
+ for ( let [ name , screen ] of Object . entries ( theme . screens ) ) {
372
392
if ( typeof screen === 'object' && screen !== null && ( 'max' in screen || 'raw' in screen ) ) {
373
- return false
393
+ errors . push (
394
+ `Cannot migrate complex screen definition: \`${ stringifyPath ( [ ...path , 'screens' , name ] ) } \`` ,
395
+ )
374
396
}
375
397
}
376
398
}
377
- return true
399
+
400
+ return errors
378
401
}
379
402
380
403
function keyframesToCss ( keyframes : Record < string , unknown > ) : string {
0 commit comments