@@ -26,7 +26,7 @@ import {
2626} from '../../../../tailwindcss/src/utils/infer-data-type'
2727import * as ValueParser from '../../../../tailwindcss/src/value-parser'
2828import { findStaticPlugins , type StaticPluginOptions } from '../../utils/extract-static-plugins'
29- import { highlight , info , relative } from '../../utils/renderer'
29+ import { highlight , info , relative , warn } from '../../utils/renderer'
3030
3131const __filename = fileURLToPath ( import . meta. url )
3232const __dirname = path . dirname ( __filename )
@@ -49,11 +49,15 @@ export async function migrateJsConfig(
4949 fs . readFile ( fullConfigPath , 'utf-8' ) ,
5050 ] )
5151
52- if ( ! canMigrateConfig ( unresolvedConfig , source ) ) {
52+ let canMigrateConfigResult = canMigrateConfig ( unresolvedConfig , source )
53+ if ( ! canMigrateConfigResult . valid ) {
5354 info (
5455 `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.` ,
5556 { prefix : '↳ ' } ,
5657 )
58+ for ( let msg of canMigrateConfigResult . errors ) {
59+ warn ( msg , { prefix : ' ↳ ' } )
60+ }
5761 return null
5862 }
5963
@@ -384,22 +388,34 @@ async function migrateContent(
384388 return sources
385389}
386390
387- // Applies heuristics to determine if we can attempt to migrate the config
388- function canMigrateConfig ( unresolvedConfig : Config , source : string ) : boolean {
389- // The file may not contain non-serializable values
390- function isSimpleValue ( value : unknown ) : boolean {
391- if ( typeof value === 'function' ) return false
392- if ( Array . isArray ( value ) ) return value . every ( isSimpleValue )
393- if ( typeof value === 'object' && value !== null ) {
394- return Object . values ( value ) . every ( isSimpleValue )
391+ const JS_IDENTIFIER_REGEX = / ^ [ a - z A - Z _ $ ] [ a - z A - Z 0 - 9 _ $ ] * $ /
392+ function stringifyPath ( path : ( string | number ) [ ] ) : string {
393+ let result = ''
394+
395+ for ( let segment of path ) {
396+ if ( typeof segment === 'number' ) {
397+ result += `[${ segment } ]`
398+ } else if ( ! JS_IDENTIFIER_REGEX . test ( segment ) ) {
399+ result += `[\`${ segment } \`]`
400+ } else {
401+ result += result . length > 0 ? `.${ segment } ` : segment
395402 }
396- return [ 'string' , 'number' , 'boolean' , 'undefined' ] . includes ( typeof value )
397403 }
398404
399- // `theme` and `plugins` are handled separately and allowed to be more complex
400- let { plugins, theme, ...remainder } = unresolvedConfig
401- if ( ! isSimpleValue ( remainder ) ) {
402- return false
405+ return result
406+ }
407+
408+ // Applies heuristics to determine if we can attempt to migrate the config
409+ function canMigrateConfig (
410+ unresolvedConfig : Config ,
411+ source : string ,
412+ ) : { valid : true } | { valid : false ; errors : string [ ] } {
413+ let theme = unresolvedConfig . theme
414+ let errors : string [ ] = [ ]
415+
416+ // Migrating presets are not supported
417+ if ( unresolvedConfig . presets && unresolvedConfig . presets . length > 0 ) {
418+ errors . push ( 'Cannot migrate config files that use presets' )
403419 }
404420
405421 // The file may only contain known-migratable top-level properties
@@ -413,49 +429,55 @@ function canMigrateConfig(unresolvedConfig: Config, source: string): boolean {
413429 'corePlugins' ,
414430 ]
415431
416- if ( Object . keys ( unresolvedConfig ) . some ( ( key ) => ! knownProperties . includes ( key ) ) ) {
417- return false
418- }
419-
420- if ( findStaticPlugins ( source ) === null ) {
421- return false
422- }
423-
424- if ( unresolvedConfig . presets && unresolvedConfig . presets . length > 0 ) {
425- return false
432+ for ( let key of Object . keys ( unresolvedConfig ) ) {
433+ if ( ! knownProperties . includes ( key ) ) {
434+ errors . push ( `Cannot migrate unknown top-level key: \`${ key } \`` )
435+ }
426436 }
427437
428438 // Only migrate the config file if all top-level theme keys are allowed to be
429439 // migrated
430440 if ( theme && typeof theme === 'object' ) {
431- if ( theme . extend && ! onlyAllowedThemeValues ( theme . extend ) ) return false
432- let { extend : _extend , ...themeCopy } = theme
433- if ( ! onlyAllowedThemeValues ( themeCopy ) ) return false
441+ let { extend, ...themeCopy } = theme
442+ errors . push ( ...onlyAllowedThemeValues ( themeCopy , [ 'theme' ] ) )
443+
444+ if ( extend ) {
445+ errors . push ( ...onlyAllowedThemeValues ( extend , [ 'theme' , 'extend' ] ) )
446+ }
434447 }
435448
436- return true
449+ // TODO: findStaticPlugins already logs errors for unsupported plugins, maybe
450+ // it should return them instead?
451+ findStaticPlugins ( source )
452+
453+ return errors . length <= 0 ? { valid : true } : { valid : false , errors }
437454}
438455
439456const ALLOWED_THEME_KEYS = [
440457 ...Object . keys ( defaultTheme ) ,
441458 // Used by @tailwindcss /container-queries
442459 'containers' ,
443460]
444- function onlyAllowedThemeValues ( theme : ThemeConfig ) : boolean {
461+ function onlyAllowedThemeValues ( theme : ThemeConfig , path : ( string | number ) [ ] ) : string [ ] {
462+ let errors : string [ ] = [ ]
463+
445464 for ( let key of Object . keys ( theme ) ) {
446465 if ( ! ALLOWED_THEME_KEYS . includes ( key ) ) {
447- return false
466+ errors . push ( `Cannot migrate theme key: \` ${ stringifyPath ( [ ... path , key ] ) } \`` )
448467 }
449468 }
450469
451470 if ( 'screens' in theme && typeof theme . screens === 'object' && theme . screens !== null ) {
452- for ( let screen of Object . values ( theme . screens ) ) {
471+ for ( let [ name , screen ] of Object . entries ( theme . screens ) ) {
453472 if ( typeof screen === 'object' && screen !== null && ( 'max' in screen || 'raw' in screen ) ) {
454- return false
473+ errors . push (
474+ `Cannot migrate complex screen definition: \`${ stringifyPath ( [ ...path , 'screens' , name ] ) } \`` ,
475+ )
455476 }
456477 }
457478 }
458- return true
479+
480+ return errors
459481}
460482
461483function keyframesToCss ( keyframes : Record < string , unknown > ) : string {
0 commit comments