|
| 1 | + |
| 2 | +const { |
| 3 | + isJSXElementComponent, |
| 4 | + isJSXElementTextInput, |
| 5 | +} = require( './utils' ); |
| 6 | + |
| 7 | +const { ReplaceController } = require( './controllers/replace' ); |
| 8 | +const { ListController } = require( './controllers/list' ); |
| 9 | +const { ControlController } = require( './controllers/control' ); |
| 10 | +/** |
| 11 | + * Generate new uids for the provided scope. |
| 12 | + * |
| 13 | + * @param {Object} scope The current scope. |
| 14 | + * @param {Object} vars The vars to generate uids for. |
| 15 | + * @returns |
| 16 | + */ |
| 17 | +function generateVarTypeUids( scope, vars ) { |
| 18 | + const varMap = {}; |
| 19 | + const varNames = []; |
| 20 | + vars.forEach( ( [ varName, varConfig ] ) => { |
| 21 | + const newIdentifier = scope.generateUidIdentifier("uid"); |
| 22 | + varMap[ varName ] = newIdentifier.name; |
| 23 | + varNames.push( varName ); |
| 24 | + } ); |
| 25 | + |
| 26 | + return [ varMap, varNames ]; |
| 27 | +} |
| 28 | + |
| 29 | + |
| 30 | + |
| 31 | +const templateVarsController = { |
| 32 | + babel: {}, |
| 33 | + vars: { |
| 34 | + replace: {}, |
| 35 | + control: {}, |
| 36 | + list: {}, |
| 37 | + }, |
| 38 | + contextIdentifier: null, |
| 39 | + init: function( templateVars, componentPath, babel ) { |
| 40 | + this.babel = babel; |
| 41 | + const { types, parse } = babel; |
| 42 | + // Get the three types of template vars. |
| 43 | + const { replace: replaceVars, control: controlVars, list: listVars } = templateVars; |
| 44 | + |
| 45 | + // Build the map of vars to replace. |
| 46 | + const replaceVarsParsed = generateVarTypeUids( componentPath.scope, replaceVars ); |
| 47 | + this.vars.replace = { |
| 48 | + raw: replaceVars, |
| 49 | + mapped: replaceVarsParsed[0], |
| 50 | + mapInv: Object.fromEntries(Object.entries(replaceVarsParsed[0]).map(a => a.reverse())), |
| 51 | + names: replaceVarsParsed[1], |
| 52 | + } |
| 53 | + //replaceVarsInv |
| 54 | + |
| 55 | + // Get the control vars names |
| 56 | + const [ controlVarsMap, controlVarsNames ] = generateVarTypeUids( componentPath.scope, controlVars ); |
| 57 | + this.vars.control = { |
| 58 | + raw: controlVars, |
| 59 | + mapped: controlVarsMap, |
| 60 | + names: controlVarsNames, |
| 61 | + } |
| 62 | + |
| 63 | + // Build the map of var lists. |
| 64 | + const [ listVarsMap, listVarsNames ] = generateVarTypeUids( componentPath.scope, listVars ); |
| 65 | + this.vars.list = { |
| 66 | + raw: listVars, |
| 67 | + mapped: listVarsMap, |
| 68 | + names: listVarsNames, |
| 69 | + toTag: {}, |
| 70 | + } |
| 71 | + |
| 72 | + |
| 73 | + // All the list variable names we need to look for in JSX expressions |
| 74 | + const self = this; |
| 75 | + // Start the main traversal of component |
| 76 | + |
| 77 | + // TODO - we should look through the params and apply the same logic... |
| 78 | + const componentParam = componentPath.node.declarations[0].init.params[0]; |
| 79 | + |
| 80 | + let propsName = null; |
| 81 | + // If the param is an object pattern, we want to add `__context__` as a property to it. |
| 82 | + if ( componentPath.node.declarations[0].init.params.length === 0 ) { |
| 83 | + // Then there are no params, so lets add an object pattern with one param, __context__. |
| 84 | + componentPath.node.declarations[0].init.params.push( types.objectPattern( [ types.objectProperty( types.identifier( '__context__' ), types.identifier( '__context__' ), false, true ) ] ) ); |
| 85 | + } else if ( types.isObjectPattern( componentParam ) ) { |
| 86 | + // Then we at the first param - which is *probably* props passed through as an object. |
| 87 | + // For now lets assume it is, but this means we likely can't work with HOC components which have multiple params. |
| 88 | + // TODO - maybe we should test again the last param as it is usually the props object in HOCs. |
| 89 | + |
| 90 | + // Add __context__ as a property to the object. |
| 91 | + componentParam.properties.push( types.objectProperty( types.identifier( '__context__' ), types.identifier( '__context__' ), false, true ) ); |
| 92 | + } else if ( types.isIdentifier( componentParam ) ) { |
| 93 | + // If it's an identifier we need to declare it in the block statement. |
| 94 | + propsName = componentParam.name; |
| 95 | + } |
| 96 | + |
| 97 | + this.contextIdentifier = componentPath.scope.generateUidIdentifier("uid"); |
| 98 | + let blockStatementDepth = 0; // make sure we only update the correct block statement. |
| 99 | + |
| 100 | + const replaceController = new ReplaceController( this.vars.replace, this.contextIdentifier.name, babel ); |
| 101 | + const listController = new ListController( this.vars.list, this.contextIdentifier.name, babel ); |
| 102 | + const controlController = new ControlController( this.vars.control, this.contextIdentifier.name, babel ); |
| 103 | + |
| 104 | + |
| 105 | + componentPath.traverse( { |
| 106 | + // Inject context into all components |
| 107 | + JSXElement(subPath){ |
| 108 | + // If we find a JSX element, check to see if it's a component, |
| 109 | + // and if so, inject a `__context__` JSXAttribute. |
| 110 | + if ( isJSXElementComponent( subPath ) ) { |
| 111 | + let expression; |
| 112 | + // check if the component is inside a `map` and increase the context by 1 |
| 113 | + if ( parentPathHasMap( subPath, types ) ) { |
| 114 | + expression = types.binaryExpression( '+', self.contextIdentifier, types.numericLiteral( 1 ) ); |
| 115 | + } else { |
| 116 | + expression = types.identifier( self.contextIdentifier.name ); |
| 117 | + } |
| 118 | + const contextAttribute = types.jSXAttribute( types.jSXIdentifier( '__context__' ), types.jSXExpressionContainer( expression ) ); |
| 119 | + subPath.node.openingElement.attributes.push( contextAttribute ); |
| 120 | + } |
| 121 | + |
| 122 | + /** |
| 123 | + * We also need to track some special exceptions to html elements. |
| 124 | + * Because the idea of this transform is that the rendered html is later scraped and saved to a file, |
| 125 | + * we need to work around some known browser rendering "bugs". |
| 126 | + */ |
| 127 | + /** |
| 128 | + * Chrome (and other browsers) will not add an accurate `value` attribute to <input> (text) elements, |
| 129 | + * They are usually moved to the shadow dom, which means when we scrape the page, anything in `value` |
| 130 | + * will be lost. |
| 131 | + * eg: |
| 132 | + * <input value="test" /> |
| 133 | + * would become: |
| 134 | + * <input /> |
| 135 | + * |
| 136 | + * Our workaround will be to copy the value attribute, to a custom attribute with the prefix `jsxtv_`. |
| 137 | + * When we later scrape this page, it will then need to be converted back to the correct html attribute. |
| 138 | + */ |
| 139 | + |
| 140 | + if ( isJSXElementTextInput( subPath ) ) { |
| 141 | + // Now get the value attribute from the jsx element. |
| 142 | + const valueAttribute = subPath.node.openingElement.attributes.find( attr => attr?.name?.name === 'value' ); |
| 143 | + |
| 144 | + if ( valueAttribute ) { |
| 145 | + // Create a new attribute `jsxtv_value` and copy the value from the valueAttribute |
| 146 | + const jsxtValueAttribute = types.jSXAttribute( types.jSXIdentifier( 'jsxtv_value' ), valueAttribute.value ); |
| 147 | + |
| 148 | + // And add it to the existing attributes. |
| 149 | + subPath.node.openingElement.attributes.push( jsxtValueAttribute ); |
| 150 | + } |
| 151 | + |
| 152 | + } |
| 153 | + |
| 154 | + }, |
| 155 | + BlockStatement( statementPath ) { |
| 156 | + // TODO: Hacky way of making sure we only catch the first block statement - we should be able to check |
| 157 | + // something on the parent to make this more reliable. |
| 158 | + if ( blockStatementDepth !== 0 ) { |
| 159 | + return; |
| 160 | + } |
| 161 | + blockStatementDepth++; |
| 162 | + |
| 163 | + // Add replace vars to path. |
| 164 | + replaceController.initVars( statementPath ); |
| 165 | + // Add list vars to path. |
| 166 | + listController.initVars( statementPath ); |
| 167 | + |
| 168 | + |
| 169 | + // Figure out if we need to add a __context__ variable to the local scope. |
| 170 | + const nodesToAdd = []; |
| 171 | + if ( propsName ) { |
| 172 | + nodesToAdd.push( parse(`let ${ self.contextIdentifier.name } = typeof ${ propsName }.__context__ === 'number' ? ${ propsName }.__context__ : 0;` ) ); |
| 173 | + } else { |
| 174 | + nodesToAdd.push( parse(`let ${ self.contextIdentifier.name } = typeof __context__ === 'number' ? __context__ : 0;` ) ); |
| 175 | + } |
| 176 | + nodesToAdd.reverse(); |
| 177 | + nodesToAdd.forEach( ( node ) => { |
| 178 | + statementPath.node.body.unshift( node ); |
| 179 | + } ); |
| 180 | + }, |
| 181 | + Identifier( subPath ) { |
| 182 | + |
| 183 | + // Update and Ternary conditions before parsing the other var types (so we can use their names |
| 184 | + // before they're updated). |
| 185 | + controlController.updateTernaryConditions( subPath ); |
| 186 | + |
| 187 | + // Now replace any replace or list vars identifier names with the new ones |
| 188 | + // we created earlier. |
| 189 | + replaceController.updateIdentifierNames( subPath ); |
| 190 | + listController.updateIdentifierNames( subPath ); |
| 191 | + }, |
| 192 | + // Track vars in JSX expressions in case we need have any control vars to process |
| 193 | + JSXExpressionContainer( subPath ) { |
| 194 | + const { expression: containerExpression } = subPath.node; |
| 195 | + |
| 196 | + // Update any control vars in expressions in JSX |
| 197 | + controlController.updateJSXExpressions( containerExpression, subPath, self.vars.list.toTag ); |
| 198 | + |
| 199 | + // And tag and update any list vars in we find in JSX |
| 200 | + listController.updateJSXListExpressions( containerExpression, subPath ); |
| 201 | + }, |
| 202 | + } ); |
| 203 | + } |
| 204 | +} |
| 205 | + |
| 206 | +// check if any parent paths contain a map call |
| 207 | +function parentPathHasMap( path, types ) { |
| 208 | + let parentPath = path.parentPath; |
| 209 | + while ( parentPath ) { |
| 210 | + if ( types.isCallExpression( parentPath.node ) && types.isMemberExpression( parentPath.node.callee ) ) { |
| 211 | + const memberExpression = parentPath.node.callee; |
| 212 | + if ( types.isIdentifier( memberExpression.property ) && memberExpression.property.name === 'map' ) { |
| 213 | + return true; |
| 214 | + } |
| 215 | + } |
| 216 | + parentPath = parentPath.parentPath; |
| 217 | + } |
| 218 | + return false; |
| 219 | +} |
| 220 | + |
| 221 | + |
| 222 | + |
| 223 | +module.exports = templateVarsController; |
0 commit comments