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