→ 1 import * as t from "@babel/types";
2 import {
3 Aliases,
4 getPropAlias,
5 Properties,
6 ChildProperties,
7 SVGNamespace,
8 DelegatedEvents,
9 SVGElements
10 } from "dom-expressions/src/constants";
11 import VoidElements from "../VoidElements";
12 import {
13 getTagName,
14 isDynamic,
15 isComponent,
16 registerImportMethod,
17 filterChildren,
18 toEventName,
19 toPropertyName,
20 checkLength,
21 getStaticExpression,
22 reservedNameSpaces,
23 wrappedByText,
24 getRendererConfig,
25 getConfig,
26 escapeBackticks,
27 escapeHTML,
28 convertJSXIdentifier,
29 canNativeSpread,
30 transformCondition,
31 trimWhitespace
32 } from "../shared/utils";
33 import { transformNode } from "../shared/transform";
34 import { InlineElements, BlockElements } from "./constants";
35
36 const alwaysClose = [
37 "title",
38 "style",
39 "a",
40 "strong",
41 "small",
42 "b",
43 "u",
44 "i",
45 "em",
46 "s",
47 "code",
48 "object",
49 "table",
50 "button",
51 "textarea",
52 "select",
53 "iframe",
54 "script",
55 "template",
56 "fieldset"
57 ];
58
59 export function transformElement(path, info) {
60 let tagName = getTagName(path.node),
61 config = getConfig(path),
62 wrapSVG = info.topLevel && tagName != "svg" && SVGElements.has(tagName),
63 voidTag = VoidElements.indexOf(tagName) > -1,
64 isCustomElement = tagName.indexOf("-") > -1,
65 results = {
66 template: `<${tagName}`,
67 declarations: [],
68 exprs: [],
69 dynamics: [],
70 postExprs: [],
71 isSVG: wrapSVG,
72 hasCustomElement: isCustomElement,
73 tagName,
74 renderer: "dom"
75 };
76 if (config.hydratable && (tagName === "html" || tagName === "head" || tagName === "body")) {
77 results.skipTemplate = true;
78 if (tagName === "head" && info.topLevel) {
79 const createComponent = registerImportMethod(
80 path,
81 "createComponent",
82 getRendererConfig(path, "dom").moduleName
83 );
84 const NoHydration = registerImportMethod(
85 path,
86 "NoHydration",
87 getRendererConfig(path, "dom").moduleName
88 );
89 results.exprs.push(
90 t.expressionStatement(
91 t.callExpression(createComponent, [NoHydration, t.objectExpression([])])
92 )
93 );
94 return results;
95 }
96 }
97 if (wrapSVG) results.template = "<svg>" + results.template;
98 if (!info.skipId) results.id = path.scope.generateUidIdentifier("el$");
99 transformAttributes(path, results);
100 if (config.contextToCustomElements && (tagName === "slot" || isCustomElement)) {
101 contextToCustomElement(path, results);
102 }
103 results.template += ">";
104 if (!voidTag) {
105 // always close tags can still be skipped if they have no closing parents and are the last element
106 const toBeClosed =
107 !info.lastElement ||
108 (info.toBeClosed && (!config.omitNestedClosingTags || info.toBeClosed.has(tagName)));
109 if (toBeClosed) {
110 results.toBeClosed = new Set(info.toBeClosed || alwaysClose);
111 results.toBeClosed.add(tagName);
112 if (InlineElements.includes(tagName)) BlockElements.forEach(i => results.toBeClosed.add(i));
113 } else results.toBeClosed = info.toBeClosed;
114 transformChildren(path, results, config);
115 if (toBeClosed) results.template += `</${tagName}>`;
116 }
117 if (info.topLevel && config.hydratable && results.hasHydratableEvent) {
118 let runHydrationEvents = registerImportMethod(
119 path,
120 "runHydrationEvents",
121 getRendererConfig(path, "dom").moduleName
122 );
123 results.postExprs.push(t.expressionStatement(t.callExpression(runHydrationEvents, [])));
124 }
125 if (wrapSVG) results.template += "</svg>";
126 return results;
127 }
128
129 export function setAttr(path, elem, name, value, { isSVG, dynamic, prevId, isCE, tagName }) {
130 // pull out namespace
131 const config = getConfig(path);
132 let parts, namespace;
133 if ((parts = name.split(":")) && parts[1] && reservedNameSpaces.has(parts[0])) {
134 name = parts[1];
135 namespace = parts[0];
136 }
137
138 // TODO: consider moving to a helper
139 if (namespace === "style") {
140 if (t.isStringLiteral(value)) {
141 return t.callExpression(
142 t.memberExpression(
143 t.memberExpression(elem, t.identifier("style")),
144 t.identifier("setProperty")
145 ),
146 [t.stringLiteral(name), value]
147 );
148 }
149 if (t.isNullLiteral(value) || t.isIdentifier(value, { name: "undefined" })) {
150 return t.callExpression(
151 t.memberExpression(
152 t.memberExpression(elem, t.identifier("style")),
153 t.identifier("removeProperty")
154 ),
155 [t.stringLiteral(name)]
156 );
157 }
158 return t.conditionalExpression(
159 t.binaryExpression("!=", value, t.nullLiteral()),
160 t.callExpression(
161 t.memberExpression(
162 t.memberExpression(elem, t.identifier("style")),
163 t.identifier("setProperty")
164 ),
165 [t.stringLiteral(name), prevId ? prevId : value]
166 ),
167 t.callExpression(
168 t.memberExpression(
169 t.memberExpression(elem, t.identifier("style")),
170 t.identifier("removeProperty")
171 ),
172 [t.stringLiteral(name)]
173 )
174 );
175 }
176
177 if (namespace === "class") {
178 return t.callExpression(
179 t.memberExpression(
180 t.memberExpression(elem, t.identifier("classList")),
181 t.identifier("toggle")
182 ),
183 [
184 t.stringLiteral(name),
185 dynamic ? value : t.unaryExpression("!", t.unaryExpression("!", value))
186 ]
187 );
188 }
189
190 if (name === "style") {
191 return t.callExpression(
192 registerImportMethod(path, "style", getRendererConfig(path, "dom").moduleName),
193 prevId ? [elem, value, prevId] : [elem, value]
194 );
195 }
196
197 if (!isSVG && name === "class") {
198 return t.callExpression(
199 registerImportMethod(path, "className", getRendererConfig(path, "dom").moduleName),
200 [elem, value]
201 );
202 }
203
204 if (name === "classList") {
205 return t.callExpression(
206 registerImportMethod(path, "classList", getRendererConfig(path, "dom").moduleName),
207 prevId ? [elem, value, prevId] : [elem, value]
208 );
209 }
210
211 if (dynamic && name === "textContent") {
212 if (config.hydratable) {
213 return t.callExpression(registerImportMethod(path, "setProperty"), [elem, t.stringLiteral("data"), value]);
214 }
215 return t.assignmentExpression("=", t.memberExpression(elem, t.identifier("data")), value);
216 }
217
218 const isChildProp = ChildProperties.has(name);
219 const isProp = Properties.has(name);
220 const alias = getPropAlias(name, tagName.toUpperCase());
221 if (namespace !== "attr" && (isChildProp || (!isSVG && isProp) || isCE || namespace === "prop")) {
222 if (isCE && !isChildProp && !isProp && namespace !== "prop") name = toPropertyName(name);
223 if (config.hydratable && namespace !== "prop") {
224 return t.callExpression(registerImportMethod(path, "setProperty"), [elem, t.stringLiteral(name), value]);
225 }
226 return t.assignmentExpression(
227 "=",
228 t.memberExpression(elem, t.identifier(alias || name)),
229 value
230 );
231 }
232
233 let isNameSpaced = name.indexOf(":") > -1;
234 name = Aliases[name] || name;
235 !isSVG && (name = name.toLowerCase());
236 const ns = isNameSpaced && SVGNamespace[name.split(":")[0]];
237 if (ns) {
238 return t.callExpression(
239 registerImportMethod(path, "setAttributeNS", getRendererConfig(path, "dom").moduleName),
240 [elem, t.stringLiteral(ns), t.stringLiteral(name), value]
241 );
242 } else {
243 return t.callExpression(
244 registerImportMethod(path, "setAttribute", getRendererConfig(path, "dom").moduleName),
245 [elem, t.stringLiteral(name), value]
246 );
247 }
248 }
249
250 function detectResolvableEventHandler(attribute, handler) {
251 while (t.isIdentifier(handler)) {
252 const lookup = attribute.scope.getBinding(handler.name);
253 if (lookup) {
254 if (t.isVariableDeclarator(lookup.path.node)) {
255 handler = lookup.path.node.init;
256 } else if (t.isFunctionDeclaration(lookup.path.node)) {
257 return true;
258 } else return false;
259 } else return false;
260 }
261 return t.isFunction(handler);
262 }
263
264 function transformAttributes(path, results) {
265 let elem = results.id,
266 hasHydratableEvent = false,
267 children,
268 spreadExpr,
269 attributes = path.get("openingElement").get("attributes");
270 const tagName = getTagName(path.node),
271 isSVG = SVGElements.has(tagName),
272 isCE = tagName.includes("-"),
273 hasChildren = path.node.children.length > 0,
274 config = getConfig(path);
275
276 // preprocess spreads
277 if (attributes.some(attribute => t.isJSXSpreadAttribute(attribute.node))) {
278 [attributes, spreadExpr] = processSpreads(path, attributes, {
279 elem,
280 isSVG,
281 hasChildren,
282 wrapConditionals: config.wrapConditionals
283 });
284 path.get("openingElement").set(
285 "attributes",
286 attributes.map(a => a.node)
287 );
288 //NOTE: can't be checked at compile time so add to compiled output
289 hasHydratableEvent = true;
290 }
291
292 // preprocess styles
293 const styleAttribute = path
294 .get("openingElement")
295 .get("attributes")
296 .find(
297 a =>
298 a.node.name &&
299 a.node.name.name === "style" &&
300 t.isJSXExpressionContainer(a.node.value) &&
301 t.isObjectExpression(a.node.value.expression) &&
302 !a.node.value.expression.properties.some(p => t.isSpreadElement(p))
303 );
304 if (styleAttribute) {
305 let i = 0,
306 leading = styleAttribute.node.value.expression.leadingComments;
307 styleAttribute.node.value.expression.properties.slice().forEach((p, index) => {
308 if (!p.computed) {
309 if (leading) p.value.leadingComments = leading;
310 path
311 .get("openingElement")
312 .node.attributes.splice(
313 styleAttribute.key + ++i,
314 0,
315 t.JSXAttribute(
316 t.JSXNamespacedName(
317 t.JSXIdentifier("style"),
318 t.JSXIdentifier(t.isIdentifier(p.key) ? p.key.name : p.key.value)
319 ),
320 t.JSXExpressionContainer(p.value)
321 )
322 );
323 styleAttribute.node.value.expression.properties.splice(index - i - 1, 1);
324 }
325 });
326 if (!styleAttribute.node.value.expression.properties.length)
327 path.get("openingElement").node.attributes.splice(styleAttribute.key, 1);
328 }
329
330 // preprocess classList
331 attributes = path.get("openingElement").get("attributes");
332 const classListAttribute = attributes.find(
333 a =>
334 a.node.name &&
335 a.node.name.name === "classList" &&
336 t.isJSXExpressionContainer(a.node.value) &&
337 t.isObjectExpression(a.node.value.expression) &&
338 !a.node.value.expression.properties.some(
339 p =>
340 t.isSpreadElement(p) ||
341 p.computed ||
342 (t.isStringLiteral(p.key) && (p.key.value.includes(" ") || p.key.value.includes(":")))
343 )
344 );
345 if (classListAttribute) {
346 let i = 0,
347 leading = classListAttribute.node.value.expression.leadingComments,
348 classListProperties = classListAttribute.get("value").get("expression").get("properties");
349 classListProperties.slice().forEach((propPath, index) => {
350 const p = propPath.node;
351 const { confident, value: computed } = propPath.get("value").evaluate();
352 if (leading) p.value.leadingComments = leading;
353 if (!confident) {
354 path
355 .get("openingElement")
356 .node.attributes.splice(
357 classListAttribute.key + ++i,
358 0,
359 t.JSXAttribute(
360 t.JSXNamespacedName(
361 t.JSXIdentifier("class"),
362 t.JSXIdentifier(t.isIdentifier(p.key) ? p.key.name : p.key.value)
363 ),
364 t.JSXExpressionContainer(p.value)
365 )
366 );
367 } else if (computed) {
368 path
369 .get("openingElement")
370 .node.attributes.splice(
371 classListAttribute.key + ++i,
372 0,
373 t.JSXAttribute(
374 t.JSXIdentifier("class"),
375 t.stringLiteral(t.isIdentifier(p.key) ? p.key.name : p.key.value)
376 )
377 );
378 }
379 classListProperties.splice(index - i - 1, 1);
380 });
381 if (!classListProperties.length)
382 path.get("openingElement").node.attributes.splice(classListAttribute.key, 1);
383 }
384
385 // combine class properties
386 attributes = path.get("openingElement").get("attributes");
387 const classAttributes = attributes.filter(
388 a => a.node.name && (a.node.name.name === "class" || a.node.name.name === "className")
389 );
390 if (classAttributes.length > 1) {
391 const first = classAttributes[0].node,
392 values = [],
393 quasis = [t.TemplateElement({ raw: "" })];
394 for (let i = 0; i < classAttributes.length; i++) {
395 const attr = classAttributes[i].node,
396 isLast = i === classAttributes.length - 1;
397 if (!t.isJSXExpressionContainer(attr.value)) {
398 const prev = quasis.pop();
399 quasis.push(
400 t.TemplateElement({
401 raw: (prev ? prev.value.raw : "") + `${attr.value.value}` + (isLast ? "" : " ")
402 })
403 );
404 } else {
405 values.push(t.logicalExpression("||", attr.value.expression, t.stringLiteral("")));
406 quasis.push(t.TemplateElement({ raw: isLast ? "" : " " }));
407 }
408 i && attributes.splice(attributes.indexOf(classAttributes[i]), 1);
409 }
410 if (values.length) first.value = t.JSXExpressionContainer(t.TemplateLiteral(quasis, values));
411 else first.value = t.stringLiteral(quasis[0].value.raw);
412 }
413 path.get("openingElement").set(
414 "attributes",
415 attributes.map(a => a.node)
416 );
417
418 let needsSpacing = true;
419
420 path
421 .get("openingElement")
422 .get("attributes")
423 .forEach(attribute => {
424 const node = attribute.node;
425 let value = node.value,
426 key = t.isJSXNamespacedName(node.name)
427 ? `${node.name.namespace.name}:${node.name.name.name}`
428 : node.name.name,
429 reservedNameSpace =
430 t.isJSXNamespacedName(node.name) && reservedNameSpaces.has(node.name.namespace.name);
431 if (t.isJSXExpressionContainer(value) && !key.startsWith("use:")) {
432 const evaluated = attribute.get("value").get("expression").evaluate().value;
433 let type;
434 if (
435 evaluated !== undefined &&
436 ((type = typeof evaluated) === "string" || type === "number")
437 ) {
438 value = t.stringLiteral(String(evaluated));
439 }
440 }
441 if (
442 t.isJSXNamespacedName(node.name) &&
443 reservedNameSpace &&
444 !t.isJSXExpressionContainer(value)
445 ) {
446 node.value = value = t.JSXExpressionContainer(value || t.JSXEmptyExpression());
447 }
448 if (
449 t.isJSXExpressionContainer(value) &&
450 (reservedNameSpace ||
451 !(t.isStringLiteral(value.expression) || t.isNumericLiteral(value.expression)))
452 ) {
453 if (key === "ref") {
454 // Normalize expressions for non-null and type-as
455 while (
456 t.isTSNonNullExpression(value.expression) ||
457 t.isTSAsExpression(value.expression)
458 ) {
459 value.expression = value.expression.expression;
460 }
461 let binding,
462 isFunction =
463 t.isIdentifier(value.expression) &&
464 (binding = path.scope.getBinding(value.expression.name)) &&
465 binding.kind === "const";
466 if (!isFunction && t.isLVal(value.expression)) {
467 const refIdentifier = path.scope.generateUidIdentifier("_ref$");
468 results.exprs.unshift(
469 t.variableDeclaration("const", [
470 t.variableDeclarator(refIdentifier, value.expression)
471 ]),
472 t.expressionStatement(
473 t.conditionalExpression(
474 t.binaryExpression(
475 "===",
476 t.unaryExpression("typeof", refIdentifier),
477 t.stringLiteral("function")
478 ),
479 t.callExpression(
480 registerImportMethod(path, "use", getRendererConfig(path, "dom").moduleName),
481 [refIdentifier, elem]
482 ),
483 t.assignmentExpression("=", value.expression, elem)
484 )
485 )
486 );
487 } else if (isFunction || t.isFunction(value.expression)) {
488 results.exprs.unshift(
489 t.expressionStatement(
490 t.callExpression(
491 registerImportMethod(path, "use", getRendererConfig(path, "dom").moduleName),
492 [value.expression, elem]
493 )
494 )
495 );
496 } else if (t.isCallExpression(value.expression)) {
497 const refIdentifier = path.scope.generateUidIdentifier("_ref$");
498 results.exprs.unshift(
499 t.variableDeclaration("const", [
500 t.variableDeclarator(refIdentifier, value.expression)
<response clipped>