babel-plugin-wallace 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/lib/_config/directives.js +356 -0
- package/lib/_config/index.js +2 -0
- package/lib/_config/loader.js +43 -0
- package/lib/_config/parse_directives.js +63 -0
- package/lib/config.js +263 -0
- package/lib/convert.js +27 -0
- package/lib/definitions/constants.js +73 -0
- package/lib/definitions/node_data.js +183 -0
- package/lib/definitions/watcher.js +26 -0
- package/lib/generate/code_generator.js +333 -0
- package/lib/generate/statement_builders.js +130 -0
- package/lib/help-system/browser-code.js +50 -0
- package/lib/help-system/entry.js +92 -0
- package/lib/help-system/error-display.js +20 -0
- package/lib/help-system/styles.css +25 -0
- package/lib/import-checker.js +35 -0
- package/lib/index.js +39 -5
- package/lib/jsx/component_dom.js +40 -0
- package/lib/jsx/contexts.js +121 -0
- package/lib/jsx/extract.js +214 -0
- package/lib/jsx/parse.js +98 -0
- package/lib/parse/component_templates.js +44 -0
- package/lib/parse/inline_directives.js +318 -0
- package/lib/parse/parse_node.js +73 -0
- package/lib/polyfills.js +10 -0
- package/lib/utils/babel.js +100 -0
- package/lib/utils/dom.js +174 -0
- package/lib/utils/misc.js +152 -0
- package/lib_old/handlers.js +52 -0
- package/lib_old/index.js +145 -0
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -1,12 +1,46 @@
|
|
|
1
|
+
require('./polyfills')
|
|
2
|
+
const {config} = require('./config')
|
|
3
|
+
const {jsxContextClasses} = require('./jsx/contexts')
|
|
4
|
+
const {importChecker} = require('./import-checker')
|
|
1
5
|
|
|
2
|
-
|
|
6
|
+
|
|
7
|
+
module.exports = ({ types: t }) => {
|
|
3
8
|
return {
|
|
4
9
|
visitor: {
|
|
10
|
+
Program: {
|
|
11
|
+
enter(path, state) {
|
|
12
|
+
config.configure(state.opts)
|
|
13
|
+
},
|
|
14
|
+
exit(path) {
|
|
15
|
+
importChecker.addMissingImports(path, t)
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
ImportDeclaration(path) {
|
|
19
|
+
// Maybe use this to identify files with Component import so it's not imported twice?
|
|
20
|
+
// importChecker.hasComponentImport(path.hub.file.opts.filename)
|
|
21
|
+
},
|
|
22
|
+
/**
|
|
23
|
+
* This node will be removed by handling code, which means:
|
|
24
|
+
* - We only ever catch the root JSXElement here.
|
|
25
|
+
* - We need to be mindful if we decide to add more visitors.
|
|
26
|
+
*/
|
|
5
27
|
JSXElement(path) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
28
|
+
const contextMatches = jsxContextClasses.map(cls => new cls(path)).filter(context => context.matches())
|
|
29
|
+
if (contextMatches.length == 1) {
|
|
30
|
+
contextMatches[0].handle()
|
|
31
|
+
}
|
|
32
|
+
if (contextMatches.length === 0) {
|
|
33
|
+
throw path.buildCodeFrameError("Found JSX in a place Wallace doesn't know how to handle.")
|
|
34
|
+
}
|
|
35
|
+
if (contextMatches.length > 1) {
|
|
36
|
+
const contextNames = contextMatches.map(cls => cls.name)
|
|
37
|
+
throw path.buildCodeFrameError(
|
|
38
|
+
`JSX matches multiple contexts (${contextNames}). Please report this to wallace!`
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
// TODO: can I just use path.hub?
|
|
42
|
+
const program = path.findParent((path) => path.type == 'Program')
|
|
43
|
+
importChecker.requiresComponentImport(program.hub.file.opts.filename)
|
|
10
44
|
}
|
|
11
45
|
}
|
|
12
46
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const {stripHtml, preprocessHTML} = require('../utils/dom')
|
|
2
|
+
const {escapeSingleQuotes} = require('../utils/misc')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build the component's DOM as the JSX is parsed, tracking the address of the current
|
|
6
|
+
* node.
|
|
7
|
+
*/
|
|
8
|
+
class ComponentDOM {
|
|
9
|
+
constructor() {
|
|
10
|
+
// The address of the current node processed e.g. [1, 3, 0]
|
|
11
|
+
this._nodeTreeAddress = []
|
|
12
|
+
this._rootElement = undefined
|
|
13
|
+
}
|
|
14
|
+
attach (element) {
|
|
15
|
+
if (this._rootElement) {
|
|
16
|
+
const relativePath = this._nodeTreeAddress.slice(0, -1)
|
|
17
|
+
const parentNode = relativePath.reduce((acc, index) => acc.childNodes[index], this._rootElement)
|
|
18
|
+
parentNode.appendChild(element)
|
|
19
|
+
} else {
|
|
20
|
+
this._rootElement = element
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
getHtmlString() {
|
|
24
|
+
return escapeSingleQuotes(stripHtml(preprocessHTML(this._rootElement.outerHTML)))
|
|
25
|
+
}
|
|
26
|
+
getCurrentAddress () {
|
|
27
|
+
return this._nodeTreeAddress.slice()
|
|
28
|
+
}
|
|
29
|
+
push(i) {
|
|
30
|
+
if (i !== undefined) {
|
|
31
|
+
this._nodeTreeAddress.push(i)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
pop() {
|
|
35
|
+
this._nodeTreeAddress.pop()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
module.exports = {ComponentDOM}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classes for processing JSX in allowed contexts.
|
|
3
|
+
* Each context is handled by a different subclass.
|
|
4
|
+
* Each class is responsible for modifying the surrounding code.
|
|
5
|
+
*/
|
|
6
|
+
const { convertJSX } = require('../convert.js')
|
|
7
|
+
const { insertStatementsAfter } = require('../utils/babel')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseJSXContextHandler {
|
|
11
|
+
constructor(path) {
|
|
12
|
+
this.path = path
|
|
13
|
+
}
|
|
14
|
+
matches() {
|
|
15
|
+
// TODO create internal wallace error.
|
|
16
|
+
throw new Error('Not implemented')
|
|
17
|
+
}
|
|
18
|
+
handle() {
|
|
19
|
+
throw new Error('Not implemented')
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Handles JSX as direct assignment:
|
|
26
|
+
*
|
|
27
|
+
* const MyComponent = <div>...</div>
|
|
28
|
+
*/
|
|
29
|
+
class JSXInDirectAssignment extends BaseJSXContextHandler {
|
|
30
|
+
matches() {
|
|
31
|
+
return this.path.parent?.type === 'VariableDeclarator'
|
|
32
|
+
}
|
|
33
|
+
handle() {
|
|
34
|
+
const nodeToInsertAfter = this.path.parentPath.parentPath
|
|
35
|
+
const componentName = this.path.parentPath.node.id.name
|
|
36
|
+
const statements = convertJSX(this.path, componentName)
|
|
37
|
+
this.path.replaceWithSourceString(`Component.extend({})`)
|
|
38
|
+
insertStatementsAfter(nodeToInsertAfter, statements)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Handles JSXElement in call to Component.extend:
|
|
45
|
+
*
|
|
46
|
+
* const MyComponent = Component.extend(JSXElement)
|
|
47
|
+
* const MyComponent = Component.extend({...}, JSXElement)
|
|
48
|
+
*
|
|
49
|
+
*/
|
|
50
|
+
class JSXInComponentDefineCallArg extends BaseJSXContextHandler {
|
|
51
|
+
matches() {
|
|
52
|
+
const parent = this.path.parent
|
|
53
|
+
if (
|
|
54
|
+
parent?.type == 'CallExpression' &&
|
|
55
|
+
parent.callee?.type == 'MemberExpression' &&
|
|
56
|
+
parent.callee?.object?.name == 'Component' &&
|
|
57
|
+
parent.callee?.property?.name == 'extend'
|
|
58
|
+
) {
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
handle() {
|
|
64
|
+
const JSXElementPath = this.path
|
|
65
|
+
const CallExpressionPath = JSXElementPath.parentPath
|
|
66
|
+
const VariableDeclaratorPath = CallExpressionPath.parentPath
|
|
67
|
+
const nodeToInsertAfter = VariableDeclaratorPath.parentPath
|
|
68
|
+
const componentName = VariableDeclaratorPath.node.id.name
|
|
69
|
+
|
|
70
|
+
// TODO: change to extend({}, internalOpts)
|
|
71
|
+
const statements = convertJSX(this.path, componentName)
|
|
72
|
+
if (CallExpressionPath.node.arguments.length == 1) {
|
|
73
|
+
JSXElementPath.replaceWithSourceString('{}')
|
|
74
|
+
} else {
|
|
75
|
+
JSXElementPath.remove()
|
|
76
|
+
}
|
|
77
|
+
insertStatementsAfter(nodeToInsertAfter, statements)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handles JSX in a class definition:
|
|
84
|
+
*
|
|
85
|
+
* class Foo extends Component {
|
|
86
|
+
* html = <div>...</div>
|
|
87
|
+
* }
|
|
88
|
+
*/
|
|
89
|
+
class JSXInClassDefinition extends BaseJSXContextHandler {
|
|
90
|
+
matches() {
|
|
91
|
+
const parent = this.path.parent
|
|
92
|
+
return (
|
|
93
|
+
parent?.type == 'CallExpression' &&
|
|
94
|
+
parent.callee?.type == "Identifier" &&
|
|
95
|
+
parent.callee.name == "_defineProperty" &&
|
|
96
|
+
parent.arguments[1]?.value == "html"
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
handle() {
|
|
100
|
+
const JSXElementPath = this.path
|
|
101
|
+
const CallExpressionPath = JSXElementPath.parentPath
|
|
102
|
+
const ExpressionStatement = CallExpressionPath.parentPath
|
|
103
|
+
const BlockStatement = ExpressionStatement.parentPath
|
|
104
|
+
const FunctionDeclaration = BlockStatement.parentPath
|
|
105
|
+
const componentName = FunctionDeclaration.node.id.name
|
|
106
|
+
const nodeToInsertAfter = FunctionDeclaration.parentPath.parentPath.parentPath.parentPath.parentPath
|
|
107
|
+
// console.log(ExpressionStatement.node)
|
|
108
|
+
ExpressionStatement.remove()
|
|
109
|
+
const statements = convertJSX(this.path, componentName)
|
|
110
|
+
insertStatementsAfter(nodeToInsertAfter, statements)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
jsxContextClasses: [
|
|
117
|
+
JSXInDirectAssignment,
|
|
118
|
+
JSXInComponentDefineCallArg,
|
|
119
|
+
JSXInClassDefinition,
|
|
120
|
+
]
|
|
121
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
const {JSDOM} = require( 'jsdom')
|
|
2
|
+
const {readCode} = require('../utils/babel')
|
|
3
|
+
const {config} = require('../config')
|
|
4
|
+
const {NodeData} = require('../definitions/node_data')
|
|
5
|
+
const {addInlineWatches} = require('../parse/inline_directives')
|
|
6
|
+
|
|
7
|
+
const dom = new JSDOM('<!DOCTYPE html>')
|
|
8
|
+
const doc = dom.window.document
|
|
9
|
+
const splitter = "-----"
|
|
10
|
+
const directives = config.directives
|
|
11
|
+
const isCapitalized = word => word[0] === word[0].toUpperCase()
|
|
12
|
+
const onlyHas = (arr, item) => arr.length === 0 || arr.length === 1 && arr[0] === item
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseConverter {
|
|
16
|
+
constructor(componentName, path, astNode, parentNodeData, nodeTreeAddress) {
|
|
17
|
+
this.componentName = componentName
|
|
18
|
+
this.path = path
|
|
19
|
+
this.astNode = astNode
|
|
20
|
+
this.parentNodeData = parentNodeData
|
|
21
|
+
this.nodeTreeAddress = nodeTreeAddress
|
|
22
|
+
this.nodeData = new NodeData(componentName, path, parentNodeData, nodeTreeAddress)
|
|
23
|
+
this.element = undefined
|
|
24
|
+
}
|
|
25
|
+
readCode(astNode) {
|
|
26
|
+
return readCode(this.path, astNode)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class JSXTextConverter extends BaseConverter {
|
|
32
|
+
convert() {
|
|
33
|
+
// expressions in text become JSXExpressionContainers, so there is never dynamic
|
|
34
|
+
// data inside JSText.
|
|
35
|
+
// Except that's currently not true due to how we squash. TODO: fix this.
|
|
36
|
+
const text = this.astNode.value
|
|
37
|
+
this.element = doc.createTextNode(text)
|
|
38
|
+
// console.log(text)
|
|
39
|
+
addInlineWatches(this.nodeData, text, 'text', true)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class JSXExpressionConverter extends BaseConverter {
|
|
45
|
+
convert() {
|
|
46
|
+
// This only treats expressions in innerText, not attribute values.
|
|
47
|
+
this.element = doc.createElement('text')
|
|
48
|
+
this.element.textContent = '.'
|
|
49
|
+
const code = this.readCode(this.astNode.expression)
|
|
50
|
+
addInlineWatches(this.nodeData, `{${code}}`, 'text', true)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class JSXElementConverter extends BaseConverter {
|
|
56
|
+
convert() {
|
|
57
|
+
const openingElement = this.astNode.openingElement
|
|
58
|
+
let tagName = this.getTagName()
|
|
59
|
+
|
|
60
|
+
// Can't put error against attribute. Maybe I need to visit?
|
|
61
|
+
this.nodeData.attributes = openingElement.attributes.map(attr => new AttributeInfo(this.path, attr))
|
|
62
|
+
this.nodeData.attributeNames = this.nodeData.attributes.map(attr => attr.name)
|
|
63
|
+
|
|
64
|
+
this.element = doc.createElement(tagName)
|
|
65
|
+
|
|
66
|
+
if (isCapitalized(tagName) || tagName.includes(".")) {
|
|
67
|
+
this.nodeData.nestedClass = tagName
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.nodeData.attributes.forEach(attr => this.processAttribute(attr))
|
|
71
|
+
|
|
72
|
+
// TODO: check siblings and children here or in directive (or NodeData.assertXYZ()..)
|
|
73
|
+
if (this.nodeData.nestedClass) {
|
|
74
|
+
if (this.nodeData.isRepeat) {
|
|
75
|
+
this.element = null
|
|
76
|
+
} else {
|
|
77
|
+
this.element = doc.createElement("br")
|
|
78
|
+
this.nodeData.replaceWith = tagName
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
getTagName() {
|
|
83
|
+
// could be 'JSXMemberExpression' or 'JSXIdentifier', so just read as string.
|
|
84
|
+
return this.readCode(this.astNode.openingElement.name)
|
|
85
|
+
}
|
|
86
|
+
processAttribute = (attr) => {
|
|
87
|
+
const {argType, name, value} = attr
|
|
88
|
+
const nameLowerCase = name.toLowerCase()
|
|
89
|
+
const directive = directives.hasOwnProperty(nameLowerCase) && directives[nameLowerCase]
|
|
90
|
+
if (directive && ! attr.escaped) {
|
|
91
|
+
this.processDirective(directive, nameLowerCase, attr)
|
|
92
|
+
} else {
|
|
93
|
+
switch (argType) {
|
|
94
|
+
case "expr":
|
|
95
|
+
addInlineWatches(this.nodeData, value, `@${attr.fullName}`, false)
|
|
96
|
+
break
|
|
97
|
+
case "null":
|
|
98
|
+
// TODO: this shows as <div disabled=""></div> which works in Chrome etc...
|
|
99
|
+
// but its annoying for tests and compactness. Seems to be a JSDOM issue?
|
|
100
|
+
this.element.setAttribute(attr.fullName, "")
|
|
101
|
+
// this.element.setAttribute(name, null)
|
|
102
|
+
// this.element[name] = true
|
|
103
|
+
break
|
|
104
|
+
case "str":
|
|
105
|
+
this.element.setAttribute(attr.fullName, value)
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
processDirective = (directive, directiveName, attr) => {
|
|
111
|
+
const allowedArgTypes = directive.allow.split("|")
|
|
112
|
+
if (!allowedArgTypes.includes(attr.argType)) {
|
|
113
|
+
throw this.path.buildCodeFrameError(
|
|
114
|
+
`Directive [${directiveName}] does not allow argument type: "${attr.argType}"`
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
if (directive.qualifier === "no" && attr.qualifier) {
|
|
118
|
+
throw this.path.buildCodeFrameError(
|
|
119
|
+
`Directive [${directiveName}] does not allow a qualifier.`
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
if (directive.qualifier === "yes" && !attr.qualifier) {
|
|
123
|
+
throw this.path.buildCodeFrameError(
|
|
124
|
+
`Directive [${directiveName}] requires a qualifier.`
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
// TODO: parse args better.
|
|
128
|
+
directive.handle(this.nodeData, attr)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* An attribute could look like:
|
|
135
|
+
*
|
|
136
|
+
* <div name>
|
|
137
|
+
* <div name="value">
|
|
138
|
+
* <div name={expr}>
|
|
139
|
+
* <div namespace:name>
|
|
140
|
+
* <div _name>
|
|
141
|
+
*
|
|
142
|
+
*/
|
|
143
|
+
class AttributeInfo {
|
|
144
|
+
constructor(path, attr) {
|
|
145
|
+
this.path = path
|
|
146
|
+
this.astAttribute = attr
|
|
147
|
+
this.name = undefined
|
|
148
|
+
this.nestedClass = undefined
|
|
149
|
+
this.qualifier = undefined
|
|
150
|
+
this.escaped = false
|
|
151
|
+
this.argType = undefined
|
|
152
|
+
this.value = undefined
|
|
153
|
+
this.arg = undefined
|
|
154
|
+
this.args = undefined
|
|
155
|
+
this.processName()
|
|
156
|
+
this.processValue()
|
|
157
|
+
}
|
|
158
|
+
processName() {
|
|
159
|
+
const name = this.astAttribute.name
|
|
160
|
+
if (name.type === "JSXIdentifier") {
|
|
161
|
+
this.name = name.name
|
|
162
|
+
} else if (name.type === "JSXNamespacedName") {
|
|
163
|
+
this.name = name.namespace.name
|
|
164
|
+
this.qualifier = name.name.name
|
|
165
|
+
} else {
|
|
166
|
+
throw this.path.buildCodeFrameError(`Unknown attribute name type: ${name.type}`)
|
|
167
|
+
}
|
|
168
|
+
if (this.name.startsWith("_")) {
|
|
169
|
+
this.name = this.name.slice(1)
|
|
170
|
+
this.escaped = true
|
|
171
|
+
}
|
|
172
|
+
this.fullName = this.name + (this.qualifier ? `:${this.qualifier}` : "")
|
|
173
|
+
}
|
|
174
|
+
processValue() {
|
|
175
|
+
const value = this.astAttribute.value
|
|
176
|
+
if (value === null) {
|
|
177
|
+
this.arg = null
|
|
178
|
+
this.argType = "null"
|
|
179
|
+
}
|
|
180
|
+
else if (value.type === "StringLiteral") {
|
|
181
|
+
this.value = value.value
|
|
182
|
+
this.argType = "str"
|
|
183
|
+
}
|
|
184
|
+
else if (value.type === "JSXExpressionContainer") {
|
|
185
|
+
this.argType = "expr"
|
|
186
|
+
// Detecting BinaryExpression through AST is a PITA. Strings are much simpler.
|
|
187
|
+
const code = readCode(this.path, value.expression).replaceAll("||", splitter)
|
|
188
|
+
this.args = code.split("|").map(x => x.replaceAll(splitter, "||").trim())
|
|
189
|
+
this.arg = this.args[0]
|
|
190
|
+
this.value = `{${readCode(this.path, value.expression)}}`
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
const converterClasses = {
|
|
197
|
+
JSXText: JSXTextConverter,
|
|
198
|
+
JSXExpressionContainer: JSXExpressionConverter,
|
|
199
|
+
JSXElement: JSXElementConverter
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
const extractNodeData = (componentName, path, astNode, parentNodeData, nodeTreeAddress) => {
|
|
204
|
+
const cls = converterClasses[astNode.type]
|
|
205
|
+
if (!cls) {
|
|
206
|
+
throw this.path.buildCodeFrameError(`Unexpected node type: ${astNode.type}`)
|
|
207
|
+
}
|
|
208
|
+
const converter = new cls(componentName, path, astNode, parentNodeData, nodeTreeAddress)
|
|
209
|
+
converter.convert()
|
|
210
|
+
return {element: converter.element, nodeData: converter.nodeData}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
module.exports = {extractNodeData}
|
package/lib/jsx/parse.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const { JSXText } = require('@babel/types')
|
|
2
|
+
const {readCode} = require('../utils/babel')
|
|
3
|
+
const {extractNodeData} = require('./extract')
|
|
4
|
+
const {ComponentDOM} = require('./component_dom')
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This parses the JSX code to collect dynamicNodes and the html string.
|
|
9
|
+
*/
|
|
10
|
+
class JSXParser {
|
|
11
|
+
constructor(path, componentName) {
|
|
12
|
+
this._path = path
|
|
13
|
+
this._componentName = componentName
|
|
14
|
+
this._dom = new ComponentDOM()
|
|
15
|
+
this._nodes = []
|
|
16
|
+
this.dynamicNodes = []
|
|
17
|
+
this.html = undefined
|
|
18
|
+
}
|
|
19
|
+
parse() {
|
|
20
|
+
this._walkJSXTree(this._path.node, undefined)
|
|
21
|
+
// Determine this at the end rather than as we go, as directives may turn parent nodes dynamic.
|
|
22
|
+
this.dynamicNodes = this._nodes.filter(node => node.isDynamic())
|
|
23
|
+
this.html = this._dom.getHtmlString()
|
|
24
|
+
}
|
|
25
|
+
_walkJSXTree(astNode, i, parentNodeData) {
|
|
26
|
+
this._dom.push(i)
|
|
27
|
+
const {element, nodeData} = extractNodeData(
|
|
28
|
+
this._componentName,
|
|
29
|
+
this._path,
|
|
30
|
+
astNode,
|
|
31
|
+
parentNodeData,
|
|
32
|
+
this._dom.getCurrentAddress()
|
|
33
|
+
)
|
|
34
|
+
if (element) {
|
|
35
|
+
this._dom.attach(element)
|
|
36
|
+
}
|
|
37
|
+
this._nodes.push(nodeData)
|
|
38
|
+
const childAstNodes = this._squashChildren(astNode)
|
|
39
|
+
childAstNodes.forEach((node, i) => this._walkJSXTree(node, i, nodeData))
|
|
40
|
+
this._dom.pop()
|
|
41
|
+
}
|
|
42
|
+
_squashChildren(astNode) {
|
|
43
|
+
if (astNode.type != 'JSXElement') {
|
|
44
|
+
return []
|
|
45
|
+
}
|
|
46
|
+
const children = []
|
|
47
|
+
const sequence = []
|
|
48
|
+
|
|
49
|
+
const flush = () => {
|
|
50
|
+
if (sequence.length === 1) {
|
|
51
|
+
const node = sequence[0]
|
|
52
|
+
if (node.type == 'JSXExpressionContainer') {
|
|
53
|
+
const text = readCode(this._path, node)
|
|
54
|
+
children.push(JSXText(text))
|
|
55
|
+
} else {
|
|
56
|
+
children.push(node)
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
children.push(...sequence)
|
|
60
|
+
}
|
|
61
|
+
sequence.length = 0
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
astNode.children.forEach(child => {
|
|
65
|
+
switch (child.type) {
|
|
66
|
+
case 'JSXText':
|
|
67
|
+
if (child.value.trim()?.length) {
|
|
68
|
+
sequence.push(child)
|
|
69
|
+
}
|
|
70
|
+
break
|
|
71
|
+
case 'JSXExpressionContainer':
|
|
72
|
+
sequence.push(child)
|
|
73
|
+
break
|
|
74
|
+
case 'JSXElement':
|
|
75
|
+
flush()
|
|
76
|
+
children.push(child)
|
|
77
|
+
break
|
|
78
|
+
default:
|
|
79
|
+
throw new Error('Unexpected node type: ' + child.type)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
flush()
|
|
83
|
+
return children
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
const parseJSX = (path, componentName) => {
|
|
89
|
+
const parser = new JSXParser(path, componentName)
|
|
90
|
+
parser.parse()
|
|
91
|
+
return {
|
|
92
|
+
componentName: componentName,
|
|
93
|
+
html: parser.html,
|
|
94
|
+
dynamicNodes: parser.dynamicNodes
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {parseJSX}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature temporarily disabled
|
|
3
|
+
*
|
|
4
|
+
* This relates to using HTML template files instead of HTML string.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require("fs")
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const {parseHTML, stripHtml} = require('../utils/dom')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A pool of component templates by path. These are files, typically called components.html
|
|
13
|
+
* which contain the HTML for components.
|
|
14
|
+
*/
|
|
15
|
+
class ComponentTemplatePool {
|
|
16
|
+
constructor() {
|
|
17
|
+
this._filePools = {}
|
|
18
|
+
}
|
|
19
|
+
getHtml(filepath, className) {
|
|
20
|
+
const dir = path.dirname(filepath)
|
|
21
|
+
const templateFile = path.join(dir, 'components.html')
|
|
22
|
+
if (fs.existsSync(templateFile)) {
|
|
23
|
+
let pooldComponents = this._filePools[templateFile]
|
|
24
|
+
if (pooldComponents === undefined) {
|
|
25
|
+
pooldComponents = this.addToPool(templateFile)
|
|
26
|
+
}
|
|
27
|
+
return pooldComponents[className]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
addToPool(templateFile) {
|
|
31
|
+
const contents = fs.readFileSync(templateFile, {encoding:'utf8', flag:'r'})
|
|
32
|
+
const dom = parseHTML(stripHtml(contents)) // Must strip!
|
|
33
|
+
const filePool = {}
|
|
34
|
+
const childNodes = Array.from(dom.childNodes)
|
|
35
|
+
childNodes.forEach(n => {
|
|
36
|
+
filePool[n.tagName] = n.childNodes[0].outerHTML
|
|
37
|
+
})
|
|
38
|
+
this._filePools[templateFile] = filePool
|
|
39
|
+
return filePool
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const componentTemplates = new ComponentTemplatePool()
|
|
44
|
+
module.exports = {componentTemplates}
|