Skip to content

Commit a782eb5

Browse files
authored
Merge pull request #15 from rmorse/develop-0.0.9
Develop 0.0.9
2 parents c48ba0f + a540be2 commit a782eb5

File tree

10 files changed

+807
-451
lines changed

10 files changed

+807
-451
lines changed

controller.js

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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

Comments
 (0)