-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvisitor.js
186 lines (167 loc) · 6.09 KB
/
visitor.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
/**
* Main visitor works by replacing variables throughout a component with Handlebars tags.
*
* Add `templateVars` to a component definition to specify which props are dynamic and need
* to be exposed as Handlebars tags - to later be rendered with data from a server.
*
* Currently supports three types of variables:
*
* - *replace* - assumes the variable needs to be replaced with a template tag like `{{name}}`
* - *control* - a variable that controls output/generated html (such as showing/hiding content)
* - limited to variables used in JSX expressions - `{ isSelected && <> ... </> }`
* Working on:
* - *list* - lists signify repeatable content and will add list tags to the html output
*
* ----
*
* Outline
* - Look for `templateVars`
* - Categorise into types (replace, control, list)
* - Locate + visit the component definition - assumes it is the previous path ( sibling ( -1 ) ).
*
* Process "replace" type vars
* - Declare new identifiers (with new values) for all `replace` type template props at the top of the component
* - Replace occurences of the old identifiers with the new ones
* (exclude variable declarations and watch out for nested props)
*
* Process "control" type vars
* - Look for the template var in JSX expressions (TODO: support more expression types)
* - Remove the condition so the expression is always completed (showing the related JSX)
* - Wrap JSX in handlebars tags using custom helpers to recreate the conditions
*
* Process "list" type vars
* - Declare new arrays with a template style version - eg `[ '{{.}}' ]` or `[ { value: '{{value}}', label: '{{label}}' } ]`
* for objects.
* - The new arrays will always have a length of 1.
* - Look for any member expressions in the component definition that use the identifier + a `.map()` and track the new
* identifier name / assignment as well as the original identifier name.
* - Look for the list vars (and any new identifiers from an earlier `.map()`) in JSX expressions - either on their
* own as an identifier or combined with `.map()` and wrap them in template tags.
* - Also check for any control variables in JSX expressions which use list variables on the right of the experssion
* and wrap them in template tags.
*/
const {
getArrayFromExpression,
} = require( './utils' );
const templateVarsController = require( './controller' );
/**
* Ensure the config prop is an array of two elements, with the first item being the var name and the second being the var config.
*
* @param {Array|String} prop - The prop to normalise
* @returns
*/
function normaliseConfigProp( prop ) {
if ( ! Array.isArray( prop ) ) {
return [ prop, {} ];
}
return prop;
}
const defaultLanguage = 'handlebars';
/**
* Gets the template vars from the property definition.
*
* @param {Object} expression The expression
* @param {Object} types The babel types object
*
* @returns
*/
function getTemplateVarsFromExpression( expression, types ) {
const left = expression.left;
const right = expression.right;
if ( ! left || ! right ) {
return false;
}
const { object, property } = left;
// Make sure the property being set is `templateVars`
if ( ! types.isIdentifier( object ) ) {
return false;
}
if ( ! types.isIdentifier( property ) ) {
return false;
}
const objectName = object.name;
const propertyName = property.name;
if ( propertyName === 'templateVars' ) {
let templatePropsValue = [];
// Now process the right part of the expression
// .templateVars = *right* and build our config object.
if ( right && right.type === 'ArrayExpression' ) {
// Then we have an array to process the props.
templatePropsValue = getArrayFromExpression( right );
}
const templateVars = {
replace: [],
control: [],
list: [],
}
// Build template prop queues for processing at different times.
templatePropsValue.forEach( ( prop ) => {
const normalisedProp = normaliseConfigProp( prop );
const [ varName, varConfig ] = normalisedProp;
// If the type is not set assume it is `replace`
if ( varConfig.type === 'replace' || ! varConfig.type ) {
templateVars.replace.push( normalisedProp );
} else if ( varConfig.type === 'control' ) {
templateVars.control.push( normalisedProp );
} else if ( varConfig.type === 'list' ) {
templateVars.list.push( normalisedProp );
}
} );
return templateVars;
}
return false;
}
/**
* The main visitor for the plugin.
*
* @param {Object} param0 Babel instance.
* @param {Object} config Plugin config.
* @returns
*/
function templateVarsVisitor( babel, config ) {
const { types } = babel;
const tidyOnly = config.tidyOnly ?? false;
return {
ExpressionStatement( path, state ) {
// Try to look for the property assignment of `templateVars` and:
// - Process the template vars for later
// - Remove `templateVars` from the source code
const { expression } = path.node;
// Process the expression and get template vars as an object
const templateVars = getTemplateVarsFromExpression( expression, types );
if ( ! templateVars ) {
return;
}
// We know this exists because it was checked in getTemplateVarsFromExpression
const componentName = path.node.expression.left.object.name;
// Find the component path by name
const componentPath = getComponentPath( path.parentPath, componentName );
// Remove templateVars from the source
path.remove();
// If tidyOnly is set, exit here (immediately after the removal of the templateVars).
if ( tidyOnly ) {
return;
}
// If the component path is not found, exit here.
if ( ! componentPath ) {
return;
}
templateVarsController.init( templateVars, componentPath, babel );
}
}
};
// Find and return a component (variable declaration) path via traversal by its name.
function getComponentPath( path, componentName ) {
let componentPath;
path.traverse( {
VariableDeclaration( subPath ) {
const declarationName = subPath.node.declarations[0].id.name
if ( declarationName === componentName ) {
componentPath = subPath;
}
subPath.skip();
}
} );
return componentPath;
}
module.exports = templateVarsVisitor;