-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
270 lines (240 loc) · 10.3 KB
/
index.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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
/*
https://github.com/facebook/jsx/blob/master/AST.md
https://github.com/estree/estree/blob/master/es5.md
https://github.com/estree/estree/blob/master/es2015.md
https://github.com/benjamn/ast-types
https://github.com/jsx-eslint/jsx-ast-utils
https://astexplorer.net/#/gist/336cefe7aa160d894ecb38868898f389/a9d94ea8bcbfd0ec10b5b513140ba64928b6a64a
https://astexplorer.net/#/gist/6beeac58462e0cf754eaf0599965c6da/435c77abb125380bbb531a28d2642e3bd96db4a3
*/
// const writeFileSync = require('fs').writeFileSync;
const debug = require('debug')('pug-to-jsx');
const chalk = require('chalk');
const generate = require('@babel/generator').default;
const { parse: parseEs, parseExpression } = require('@babel/parser');
const b = require('@babel/types');
const lex = require('pug-lexer');
const load = require('pug-load');
const parsePug = require('pug-parser');
const literalToAst = require('babel-literal-to-ast');
const pugAttrNameToJsx = (name) => {
switch (name) {
case 'class':
return 'className';
default:
return name;
}
};
const pugAttrValToJsx = (name, val) => {
if (typeof val === 'boolean') {
return null;
}
// styles as a css string
if (name === 'style' && !val.includes('{')) {
console.log('val :>> ', val);
return b.jsxExpressionContainer(
literalToAst(
val
.replace(/["\s+]/g, '')
.split(';')
.reduce((styles, styleRaw) => {
const style = styleRaw.split(':');
styles[style[0]] = style[1];
return styles;
}, {}),
),
);
}
// FIXME: try to distinguish simple strings from actual expressions
// this should result in cleaner string attrs in jsx
// return b.stringLiteral(val.replace(/'/g, ''));
return b.jsxExpressionContainer(parseExpression(val));
};
const processArrayForConditional = (nodesArr) => {
if (nodesArr.length === 0) return b.nullLiteral();
// assuming that we'll get only markup here
return b.jsxFragment(
b.jsxOpeningFragment(),
b.jsxClosingFragment(),
nodesArr.map((child) => {
if (child.expression) {
return b.jsxExpressionContainer(child.expression);
}
return child;
}),
);
};
function getEsNode(pugNode, esChildren) {
let esNode;
debug(`\ngot node type ${pugNode.type}: %O\n`, pugNode);
if (pugNode.type === 'Text') {
// intentionally streamlining this case for now since formatting is done by the `generator` anyway
if (pugNode.val.replace(/\s|\n/g, '').length === 0) esNode = b.emptyStatement();
else esNode = b.jsxText(pugNode.val); // string
} else if (pugNode.type === 'Code') {
esNode = parseEs(pugNode.val).program.body;
} else if (pugNode.type === 'Block') {
esNode = esChildren;
} else if (pugNode.type === 'Comment') {
esNode = b.emptyStatement(); // FIXME: add actual comments
} else if (pugNode.type === 'Doctype') {
esNode = b.emptyStatement();
} else if (pugNode.type === 'Case') {
esNode = b.switchStatement(parseExpression(pugNode.expr), esChildren);
} else if (pugNode.type === 'When') {
esNode = b.switchCase(
pugNode.expr === 'default' ? null : parseExpression(pugNode.expr),
esChildren
? // break statement is implicit by default, FIXME: handle explicit break statements
[...[].concat(esChildren), b.breakStatement()]
: [],
);
} else if (pugNode.type === 'Conditional') {
let consequent = walk(pugNode.consequent);
if (Array.isArray(consequent)) consequent = processArrayForConditional(consequent);
debug('consequent %O', consequent);
let alternate = (pugNode.alternate && walk(pugNode.alternate)) || b.nullLiteral();
if (Array.isArray(alternate)) alternate = processArrayForConditional(alternate);
debug('alternate %O', alternate);
esNode = b.conditionalExpression(parseExpression(pugNode.test), consequent, alternate);
} else if (pugNode.type === 'Each') {
let children = Array.isArray(esChildren) ? esChildren.flat() : [esChildren];
esNode = b.expressionStatement(
b.callExpression(b.memberExpression(b.identifier(pugNode.obj), b.identifier('map')), [
b.arrowFunctionExpression(
[b.identifier(pugNode.val), pugNode.key ? b.identifier(pugNode.key) : undefined].filter(Boolean),
b.blockStatement([
b.returnStatement(
b.jsxFragment(
b.jsxOpeningFragment(),
b.jsxClosingFragment(),
children.map((child) => {
if (child.expression) {
return child.expression;
}
return child;
}),
),
),
]),
),
]),
);
} else if (pugNode.type === 'Mixin' && pugNode.call === false) {
// component declaration
esNode = b.variableDeclaration('const', [
b.variableDeclarator(
b.identifier(pugNode.name),
b.arrowFunctionExpression(
pugNode.args.split(', ').map((arg) => b.identifier(arg)), // TODO: generate props destructuring for easier refactoring?
b.blockStatement(esChildren),
),
),
]);
} else if (pugNode.type === 'Mixin' && pugNode.call === true) {
// component call
} else if (pugNode.type === 'Tag') {
let children = Array.isArray(esChildren) ? esChildren.flat() : [esChildren];
esNode = b.expressionStatement(
b.jsxElement(
b.jsxOpeningElement(b.jsxIdentifier(pugNode.name), [
...pugNode.attrs.map((attr) =>
b.jsxAttribute(
b.jsxIdentifier(pugAttrNameToJsx(attr.name)),
pugAttrValToJsx(attr.name, attr.val),
),
),
...pugNode.attributeBlocks.map((attrBlock) => b.jsxSpreadAttribute(parseExpression(attrBlock.val))),
]),
b.jsxClosingElement(b.jsxIdentifier(pugNode.name)),
children.map((child) => {
if (!child) {
return b.jsxText(''); // FIXME: same as below
} else if (child.expression) {
// passthrough
return b.jsxExpressionContainer(child.expression);
} else if (b.isEmptyStatement(child)) {
return b.jsxText(' '); // FIXME: hack to override the Text node handler behavior -- ES `program`'s `body` doesn't like jsxText as its children, so I return EmptyExpression there, but have to make this hack here
} else if (
!(
b.isJSXText(child) ||
b.isJSXSpreadChild(child) ||
b.isJSXElement(child) ||
b.isJSXFragment(child)
)
) {
return b.jsxExpressionContainer(child);
}
return child;
}) || [],
pugNode.selfClosing,
),
);
} else if (pugNode.type === 'InterpolatedTag') {
esNode = b.jsxExpressionContainer(parseExpression(pugNode.expr));
} else if (pugNode.type === 'Include') {
if (pugNode.column !== 1) {
console.warn(
chalk.yellow(
`Skipping ${pugNode.file.path}.\nPlease convert this file a mixin: move its import to the top of the file and use as a mixin instead of include, e.g. +myMixin(arg1, arg2, ...)`,
),
);
return;
}
const noExtPath = pugNode.file.path.replace(/.pug$/, '');
const name = noExtPath.split('/').pop().replace('-', ''); // TODO: camel case names
const specifier = b.identifier(name);
esNode = b.importDeclaration([b.importSpecifier(specifier, specifier)], b.stringLiteral(noExtPath));
} else if (pugNode.type === 'RawInclude') {
console.warn(chalk.yellow(`Skipping ${pugNode.file.path}. Please handle these files manually.`));
return;
}
if (typeof esNode === 'undefined') throw new Error(`Unsupported pug node type: ${pugNode.type}`);
return esNode;
}
function walk(pugNode) {
if (Array.isArray(pugNode)) {
return pugNode.map((n) => walk(n));
}
const pugChildren = pugNode.nodes || pugNode.block;
let esChildren;
if (pugChildren) {
esChildren = walk(pugChildren);
}
return getEsNode(pugNode, esChildren);
}
/**
* @param {String[]} paths
*/
module.exports.convert = function convert(paths) {
const res = {};
paths.forEach((path) => {
const pugAst = load.file(path, {
lex,
parse: parsePug,
resolve: function (filename, source, options) {
console.log('"' + filename + '" file requested from "' + source + '".');
return load.resolve(filename, source, options);
},
});
debug('Pug AST: %O', pugAst);
const walkRes = walk(pugAst);
debug('Pug AST walk: %O', walkRes);
const esAst = b.program(
[].concat(...walkRes).map((res) => {
// probably a hack for cases when we get a "bare" or an empty expression
// that is not a statement as required by b.program()'s body param,
// see conditionals/unless test
if (!res) return b.emptyStatement();
if (!b.isStatement(res)) return b.expressionStatement(res);
return res;
}),
);
debug('ES AST: %O', esAst);
const { code } = generate(esAst);
debug('resulting code: %o', code);
// writeFileSync(path.replace(/\.pug$/, '.jsx'), code);
res[path] = code;
});
return res;
};