import { Analysis , ResolvedConfiguration as CSSBlocksConfiguration , StyleMapping , charInFile , } from "@css-blocks/core" ; import { PluginObj } from "babel-core" ; import { NodePath } from "babel-traverse" ; import { AssignmentExpression , Expression , Identifier , ImportDeclaration , JSXAttribute , JSXOpeningElement , Node , Statement , identifier , importDeclaration , importDefaultSpecifier , isIdentifier , isJSXExpressionContainer , jSXAttribute , jSXExpressionContainer , jSXIdentifier , stringLiteral , } from "babel-types" ; import * as debugGenerator from "debug" ; import { deprecate } from "util" ; import { TEMPLATE_TYPE } from "../Analyzer/Template" ; import { JSXElementAnalyzer } from "../Analyzer/visitors/element" ; import { isBlockFilename } from "../utils/isBlockFilename" ; import { isConsoleLogStatement } from "../utils/isConsoleLogStatement" ; import { HELPER_FN_NAME , classnamesHelper as generateClassName } from "./classNameGenerator" ; import { CSSBlocksJSXTransformer as Rewriter } from "./index" ; const debug = debugGenerator ( "css-blocks:jsx:rewriter" ) ; let { parse } = require ( "path" ) ; export interface CssBlocksVisitor { dynamicStylesFound : boolean ; importsToRemove : Array < NodePath < ImportDeclaration > > ; statementsToRemove : Array < NodePath < Statement > > ; elementAnalyzer : JSXElementAnalyzer ; filename : string ; mapping : StyleMapping < TEMPLATE_TYPE > ; analysis : Analysis < TEMPLATE_TYPE > | undefined ; cssBlockOptions : CSSBlocksConfiguration ; shouldProcess : boolean ; } const CAN_PARSE_EXTENSIONS = { ".js" : true , ".tsx" : true , ".jsx" : true , } ; interface BabelFile { opts : { filename : string ; } ; } export function babelPlugin ( transformOpts ) { const rewriter = transformOpts . rewriter ; debug ( `Made Rewriter` ) ; return function transform ( ) : PluginObj < CssBlocksVisitor > { return { pre ( file ) { debug ( `Encountered file for rewrite: ${ file }` ) ; this . dynamicStylesFound = false ; this . importsToRemove = new Array < NodePath < ImportDeclaration > > ( ) ; this . statementsToRemove = new Array < NodePath < Statement > > ( ) ; this . filename = file . opts . filename ; this . mapping = rewriter . blocks [ this . filename ] ; if ( this . mapping && this . mapping . analyses ) { this . analysis = this . mapping . analyses . find ( a => a . template . identifier === this . filename ) ; } else { this . shouldProcess = false ; } let ext = parse ( this . filename ) . ext ; this . shouldProcess = CAN_PARSE_EXTENSIONS [ ext ] && this . analysis && this . analysis . blockCount ( ) > 0 || false ; if ( this . analysis && this . shouldProcess ) { debug ( `Rewriting discovered dependency ${ this . filename }` ) ; this . elementAnalyzer = new JSXElementAnalyzer ( this . analysis , true ) ; } } , post ( ) { for ( let nodePath of this . statementsToRemove ) { if ( nodePath . removed ) { continue ; } nodePath . remove ( ) ; } if ( this . dynamicStylesFound ) { let firstImport = this . importsToRemove . shift ( ) ! ; detectStrayReferenceToImport ( firstImport , this . filename ) ; let importDecl = importDeclaration ( [ importDefaultSpecifier ( identifier ( HELPER_FN_NAME ) ) ] , stringLiteral ( "@css-blocks/runtime" ) ) ; firstImport . replaceWith ( importDecl ) ; } for ( let nodePath of this . importsToRemove ) { if ( nodePath . removed ) { continue ; } detectStrayReferenceToImport ( nodePath , this . filename ) ; nodePath . remove ( ) ; } } , visitor : { ImportDeclaration ( nodePath ) { if ( isBlockFilename ( nodePath . node . source . value ) ) { debug ( `will remove import import of ${ nodePath . node . source . value }` ) ; this . importsToRemove . push ( nodePath ) ; } } , AssignmentExpression ( path ) { if ( ! this . shouldProcess ) return ; let elementAnalysis = this . elementAnalyzer . analyzeAssignment ( path ) ; if ( elementAnalysis ) { elementAnalysis . seal ( ) ; let classMapping = this . mapping . simpleRewriteMapping ( elementAnalysis ) ; let className = undefined ; if ( classMapping . dynamicClasses . length > 0 ) { className = generateClassName ( classMapping , elementAnalysis , HELPER_FN_NAME , true ) ; } else { className = stringLiteral ( classMapping . staticClasses . join ( " " ) ) ; } let right = path . get ( "right" ) ; if ( isIdentifier ( right . node ) ) { let binding = right . scope . getBinding ( right . node . name ) ; if ( binding && binding . path . isVariableDeclarator ( ) ) { let init = binding . path . get ( "init" ) ; init . replaceWith ( className ) ; return ; } } right . replaceWith ( className ) ; } } , JSXOpeningElement ( path ) { if ( ! this . shouldProcess ) { return ; } let elementAnalysis = this . elementAnalyzer . analyzeJSXElement ( path ) ; if ( elementAnalysis ) { elementAnalysis . seal ( ) ; let classMapping = this . mapping . simpleRewriteMapping ( elementAnalysis ) ; let attributeValue = undefined ; let newClassAttr = undefined ; if ( classMapping . dynamicClasses . length > 0 ) { this . dynamicStylesFound = true ; attributeValue = jSXExpressionContainer ( generateClassName ( classMapping , elementAnalysis , HELPER_FN_NAME , true ) ) ; } else if ( classMapping . staticClasses . length > 0 ) { attributeValue = stringLiteral ( classMapping . staticClasses . join ( " " ) ) ; } if ( attributeValue ) { newClassAttr = jSXAttribute ( jSXIdentifier ( "className" ) , attributeValue ) ; } let classAttrs = this . elementAnalyzer . classAttributePaths ( path ) ; for ( let attrPath of classAttrs ) { let binding = this . elementAnalyzer . styleVariableBinding ( attrPath ) ; if ( binding ) { this . statementsToRemove . push ( binding . path as NodePath < Statement > ) ; for ( let ref of binding . referencePaths ) { if ( ! isJSXExpressionContainer ( ref . parentPath . node ) && ! isConsoleLogStatement ( ref . node ) ) { this . statementsToRemove . push ( ref . getStatementParent ( ) ) ; } } } } if ( newClassAttr ) { let firstClass = classAttrs . shift ( ) ! ; firstClass . insertAfter ( newClassAttr ) ; firstClass . remove ( ) ; } for ( let attrPath of classAttrs ) { attrPath . remove ( ) ; } } } , } , } ; } ; } deprecated export const makePlugin = deprecate ( babelPlugin , "makePlugin has been renamed babelPlugin" ) ; function detectStrayReferenceToImport ( importDeclPath , filename ) { if ( ! importDeclPath || ! importDeclPath . node ) { return ; } for ( let specifier of importDeclPath . node . specifiers ) { let binding = importDeclPath . scope . getBinding ( specifier . local . name ) ; if ( binding ) { for ( let ref of binding . referencePaths ) { if ( ref . type === "Identifier" && ( < Identifier > ref . node ) . name === specifier . local . name && ! isRemoved ( ref ) ) { if ( ! filename . endsWith ( "x" ) ) { console . warn ( `WARNING: For performance reasons, only jsx and tsx files are properly analyzed for block dependencies and rewritten. Consider renaming ${ filename } to ${ filename }x as well as any leading to importing it from the entry point.` ) ; } console . warn ( `WARNING: Stray reference to block import (${ specifier . local . name }). Imports are removed during rewrite so this will probably be a runtime error. (${ charInFile ( filename , ref . node . loc . start ) })` ) ; console . warn ( `WARNING: This usually happens when a style reference is incorrectly used outside a jsx expression. But sometimes when an application is improperly configured. Be sure that only files ending in jsx or tsx are involved in the importing of components using css blocks.` ) ; } } } } } function isRemoved ( path ) { let p = path ; while ( p && p . type !== "Program" ) { if ( p . removed || p . parentPath . removed ) return true ; if ( p . inList ) { let list = p . parentPath . get ( p . listKey ) ; if ( ! Array . isArray ( list ) ) return true ; let element = list [ p . key ] ; if ( ! element ) return true ; if ( element . node !== p . node ) return true ; } else { let parent = p . parentPath . get ( p . parentKey ) ; if ( Array . isArray ( parent ) || parent . node !== p . node ) { return true ; } } p = p . parentPath ; } return false ; }