/*
* Deepkit Framework
* Copyright (C) 2021 Deepkit UG, Marc J. Schmidt
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the MIT License.
*
* You should have received a copy of the MIT License along with this program.
*/
import { addHook } from 'pirates';
import type {
ArrayExpression,
ArrayPattern,
AssignmentPattern,
BinaryExpression,
CallExpression,
Expression,
Identifier,
Literal,
MemberExpression,
Node,
ObjectExpression,
ObjectPattern,
Property,
RestElement,
SpreadElement,
UnaryExpression
} from 'estree';
// @ts-ignore
import abstractSyntaxTree from 'abstract-syntax-tree';
import { inDebugMode } from '@deepkit/core';
import { escape, escapeHtml } from './utils.js';
import { voidElements } from './template.js';
const { parse, generate, replace } = abstractSyntaxTree;
export function transform(code: string, filename: string) {
if (inDebugMode()) return code;
//for CommonJs its jsx_runtime_1.jsx, for ESM its _jsx. ESM is handled in loader.ts#transformSource
if (code.indexOf('.jsx(') === -1 && code.indexOf('.jsxs(') === -1) return code;
// console.log('optimize code for', filename, optimizeJSX(code));
return optimizeJSX(code);
}
addHook(transform, { exts: ['.js', '.tsx'] });
class NotSerializable {
}
export function parseCode(code: string) {
return parse(code);
}
export function generateCode(ast: Node) {
return generate(ast);
}
function serializeValue(node: Literal | UnaryExpression): any {
if (node.type === 'Literal') {
return node.value;
}
if (node.type === 'UnaryExpression' && node.argument?.type === 'Literal') {
return node.argument.value;
}
return NotSerializable;
}
/**
* Our strategy is to convert ObjectExpressions to a BinaryExpression that has basically like
*
* p1 + '=' + p1Value + ' ' + p2 + '=' + p2Value + ...
*
* concatExpressions() can then further optimise it down to one big literal (or several literals depending on where non-literals are used).
*/
function optimizeAttributes(jsxRuntime: string, node: ObjectExpression): any {
const expressions: ConcatableType[] = [];
for (const p of node.properties) {
if (p.type === 'SpreadElement') return node;
if (isPattern(p.value)) return node;
const value = p.value.type === 'UnaryExpression' ? p.value.argument : p.value;
if (expressions.length) {
expressions.push(createEscapedLiteral(' '));
}
if (p.key.type === 'Identifier') {
expressions.push(createEscapedLiteral(p.key.name));
} else {
expressions.push(p.key);
}
expressions.push(createEscapedLiteral('="'));
if (value.type !== 'Literal' && !isPattern(value) && !isCreateElementCall(value) && !isEscapeCall(value) && !isHtmlCall(value)) {
expressions.push(toSafeString(jsxRuntime, toEscapeAttributeCall(jsxRuntime, value)));
} else {
if (value.type === 'Literal') {
expressions.push(createEscapedLiteral(value.value));
} else {
expressions.push(value);
}
}
expressions.push(createEscapedLiteral('"'));
}
if (expressions.length >= 2) return concatExpressions(jsxRuntime, expressions);
return createEscapedLiteral('');
}
function isPattern(e: Node): e is ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | MemberExpression {
return e.type === 'ArrayPattern' || e.type === 'ObjectPattern' || e.type === 'AssignmentPattern' || e.type === 'RestElement' || e.type === 'MemberExpression';
}
//jsx_runtime_1.jsx()
function isCjsJSXCall(node: Node): boolean {
if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression') {
const expression = node.callee;
return expression.property && expression.property.type === 'Identifier' && (expression.property.name === 'jsx' || expression.property?.name === 'jsxs');
}
return false;
}
function unwrapCallExpression(node: Node): MemberExpression | Identifier | undefined {
if (node.type === 'CallExpression' && node.callee.type === 'SequenceExpression'
&& node.callee.expressions[0] && node.callee.expressions[0].type === 'Literal' && node.callee.expressions[0].value === 0
&& node.callee.expressions[1]) {
if (node.callee.expressions[1].type === 'MemberExpression') return node.callee.expressions[1];
if (node.callee.expressions[1].type === 'Identifier') return node.callee.expressions[1];
}
if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression') {
return node.callee;
}
return;
}
//(0, jsx_runtime_1.jsx)
function isAvoidedThisCjsJSXCall(node: Node): boolean {
const expression = unwrapCallExpression(node);
if (expression && expression.type === 'MemberExpression') {
return !!expression.property && expression.property.type === 'Identifier' && (expression.property.name === 'jsx' || expression.property?.name === 'jsxs');
}
return false;
}
function isESMJSXCall(node: Node): boolean {
return node.type === 'CallExpression' && node.callee.type === 'Identifier' && (node.callee.name === '_jsx' || node.callee.name === '_jsxs');
}
/**
* We convert
*
* _jsx.createElement("div", { id: "123" }, void 0)
* -> "
"
*
* _jsx.createElement("div", { children: "Test" }, void 0)
* -> "Test
"
*
* _jsx.createElement("div", Object.assign({ id: "123" }, { children: "Test" }), void 0)
* -> "Test
"
*
* _jsx.createElement("div", Object.assign({ id: "123" }, { children: _jsx.createElement('b", { children: "strong' }, void 0) }), void 0);
* -> "" + "strong" + "
"
*/
function optimizeNode(node: Expression, root: boolean = true): Expression {
if (node.type !== 'CallExpression') return node;
if (node.callee.type !== 'MemberExpression') return node;
if (node.callee.object.type !== 'Identifier') return node;
const isCreateElementExpression = node.callee.property.type === 'Identifier' && node.callee.property?.name === 'createElement';
if (!isCreateElementExpression) return node;
const jsxRuntime = node.callee.object.name;
//if first argument is a Fragment, we optimise for that
if (node.arguments[0].type === 'MemberExpression' && node.arguments[0].object.type === 'Identifier'
&& node.arguments[0].property.type === 'Identifier' && node.arguments[0].property.name === 'Fragment') {
//we normalize Fragments (or all createElement(var)) to have as second parameter the attributes.
//since Fragments don't have attributes, this is always an empty object, so we skip that
const concat: Expression[] = [];
let optimisedString: boolean = true;
let strings: string[] = [];
for (let i = 2; i < node.arguments.length; i++) {
const a = node.arguments[i];
if (a && a.type !== 'SpreadElement') {
const o = optimizeNode(a, false);
concat.push(o);
const staticString = extractStaticString(o);
if (staticString !== noStaticValue) {
strings.push(staticString);
} else {
optimisedString = false;
}
}
}
//all arguments are string literals, which can merge to one big string
const result = optimisedString ? createEscapedLiteral(strings.join('')) : concatExpressions(jsxRuntime, concat);
const safeString = getSafeString(result);
if (safeString !== undefined) {
if (root) return createRenderObjectWithEscapedChildren(safeString);
return safeString;
}
if (root) return createRenderObjectWithEscapedChildren(result);
return result;
}
//go deeper if possible
for (let i = 2; i < node.arguments.length; i++) {
const a = node.arguments[i];
if (a && a.type === 'CallExpression') {
node.arguments[i] = optimizeNode(a, false);
}
}
let tag: any = '';
//we only optimize attributes when we have createElement(string)
if (node.arguments[0].type === 'Literal') {
tag = node.arguments[0].value;
const attributes = node.arguments[1];
if (isObjectAssignCall(attributes)) {
//when we have here Object.assign still, then it includes a spread operator, which we can't optimise.
return node;
} else {
if (node.arguments[1] && node.arguments[1].type === 'ObjectExpression') {
const ori = node.arguments[1];
node.arguments[1] = optimizeAttributes(jsxRuntime, ori);
if (ori === node.arguments[1]) {
//we did not change the attributes to a better option, so we stop optimizing further.
return node;
}
}
}
}
//check if we can concatenate (+) arguments
let canBeConcatenated = true;
for (let i = 1; i < node.arguments.length; i++) {
const a = node.arguments[i];
if (a && a.type !== 'SpreadElement' && !isConcatenatable(a)) {
canBeConcatenated = false;
break;
}
}
if (canBeConcatenated) {
const args = node.arguments as (Literal | CallExpression)[];
const attributes = args[1];
const concat: Expression[] = [];
const closing = voidElements[tag] ? '/>' : '>';
if (tag) {
if (attributes.type === 'Literal' && !attributes.value) {
//no attributes
concat.push(createEscapedLiteral('<' + tag + closing));
} else {
concat.push(createEscapedLiteral('<' + tag + ' '));
concat.push(attributes);
concat.push(createEscapedLiteral(closing));
}
}
for (let i = 2; i < node.arguments.length; i++) {
let e = node.arguments[i];
if (e.type !== 'SpreadElement') {
concat.push(e);
}
}
if (tag && closing === '>') {
concat.push(createEscapedLiteral('' + tag + '>'));
}
if (concat.length === 0) {
return node;
}
if (node.arguments[0].type !== 'Literal') {
//for custom elements, we only merge its arguments
if (node.arguments[2]) {
node.arguments[2] = optimiseArguments(jsxRuntime, concat, false);
node.arguments.splice(3);
}
//todo: convert to {render:, attributes:, children: }
return node;
}
// //concatExpressions tries to convert everything to one big literal if possible
// //alternatively returns an renderObject expression with array children.
return optimiseArguments(jsxRuntime, concat, root);
}
return node;
}
function optimiseArguments(jsxRuntime: string, expressions: ConcatableType[], root: boolean): Expression {
if (expressions.length === 0) throw new Error('No expression for optimise arguments');
if (expressions.length === 1) {
const e = expressions[0];
const htmlArg = getHtmlCallArg(e);
const value = htmlArg ? extractStaticString(htmlArg) : extractStaticString(e);
if (value !== noStaticValue) {
expressions[0] = toSafeString(jsxRuntime, createEscapedLiteral(value));
}
}
const result = expressions.length === 1 ? toOptimisedArrayExpression(expressions) : concatExpressions(jsxRuntime, expressions);
const safeString = getSafeString(result);
if (safeString !== undefined) {
if (root) return createRenderObjectWithEscapedChildren(safeString);
return safeString;
}
if (root) return createRenderObjectWithEscapedChildren(result);
return result;
}
function concatExpressions(jsxRuntime: string, expressions: ConcatableType[]): ConcatableType {
if (expressions.length < 2) {
console.dir(expressions, { depth: null });
throw new Error('concatExpressions requires at least 2 expressions');
}
const normalizedExpressions: ConcatableType[] = [];
//todo: can this BinaryExpression check be entirely be removed?
for (let e of expressions) {
if (e.type === 'BinaryExpression' && !isOptimisedBinaryExpression(e)) {
//check if its ours BinaryExpression from concatExpressions. If so do not merge.
//we merge later at the end of the pipeline
//unwrap existing binaryExpression to avoid brackets: a + (b + (c + d)) + e
for (const a of extractBinaryExpressions(e)) {
normalizedExpressions.push(a);
}
} else if (isOptimisedArrayExpression(e)) {
for (const a of e.elements) {
if (a.type === 'SpreadElement') continue;
normalizedExpressions.push(a);
}
} else {
normalizedExpressions.push(e);
}
}
function mergeLiterals(expression: Expression[]): ConcatableType[] {
const result: ConcatableType[] = [];
let lastLiteral: Literal | undefined;
//try to optimise static values together
for (let e of expression) {
if (e.type === 'Literal' && !(e as any).escape) {
//we need to escape it
e.value = escapeHtml(e.value);
(e as any).escape = true;
}
const htmlArg = getHtmlCallArg(e);
const value = htmlArg ? extractStaticString(htmlArg) : extractStaticString(e);
if (value === noStaticValue) {
lastLiteral = undefined;
result.push(e);
} else {
if (lastLiteral) {
lastLiteral.value += value;
} else {
lastLiteral = createEscapedLiteral(value);
result.push(toSafeString(jsxRuntime, lastLiteral));
}
}
}
return result;
}
let optimizedExpressions = mergeLiterals(normalizedExpressions);
if (optimizedExpressions.length === 1) {
return optimizedExpressions[0];
}
const allExpressionsConcatable = optimizedExpressions.every(e => {
return isOptimisedBinaryExpression(e) || isHtmlCall(e) || isSafeCall(e) || isEscapedLiteral(e) || getSafeString(e) !== undefined || isUserEscapeCall(e) || isOptimisedHtmlString(e);
});
const lastStep: ConcatableType[] = [];
for (let e of optimizedExpressions) {
if (e.type === 'BinaryExpression') {
//unwrap existing binaryExpression to avoid brackets: a + (b + (c + d)) + e
for (const a of extractBinaryExpressions(e)) {
lastStep.push(a);
}
} else {
lastStep.push(e);
}
}
optimizedExpressions = mergeLiterals(lastStep);
// console.log('allExpressionsConcatable', allExpressionsConcatable);
// console.dir(optimizedExpressions, { depth: null });
if (!allExpressionsConcatable) {
//we can't optimise this as one big binary expression
//we have to return it as array
return toOptimisedArrayExpression(optimizedExpressions);
}
let lastBinaryExpression: OptimisedBinaryExpression | undefined;
for (let i = 1; i < optimizedExpressions.length; i++) {
lastBinaryExpression = {
type: 'BinaryExpression',
_optimised: true,
left: lastBinaryExpression || normalizeLastStep(optimizedExpressions[0]),
right: normalizeLastStep(optimizedExpressions[i]),
operator: '+',
};
}
if (!lastBinaryExpression) throw new Error('Could not build binary expression');
return lastBinaryExpression;
}
function normalizeLastStep(e: Expression): Expression {
const safeCallArg = getSafeCallArg(e);
if (safeCallArg !== undefined) return safeCallArg;
const safeString = getSafeString(e);
if (safeString !== undefined) return safeString;
const htmlCall = getHtmlCallArg(e);
if (htmlCall !== undefined) return htmlCall;
if (isUserEscapeCall(e) || isHtmlCall(e)) {
//convert from `escape(e)` to `escape(e).htmlString`
//and from `html(e)` to `html(e).htmlString`
return {
type: 'MemberExpression',
object: e,
computed: false,
property: { type: 'Identifier', name: 'htmlString' }
} as MemberExpression;
}
return e;
}
function toOptimisedArrayExpression(expressions: ConcatableType[]): ArrayExpression {
const arrayExpression: OptimisedArrayExpression = {
type: 'ArrayExpression',
_optimised: true,
elements: [],
};
for (let e of expressions) {
arrayExpression.elements.push(e);
}
return arrayExpression;
}
function createRenderObjectWithEscapedChildren(children?: Expression, render: Expression = { type: 'Identifier', name: 'undefined' }, attributes: Expression = {
type: 'Identifier',
name: 'undefined'
}): Expression {
const o: ObjectExpression = {
type: 'ObjectExpression',
properties: [
{
type: 'Property',
key: { type: 'Identifier', name: 'render' },
value: render,
kind: 'init',
computed: false,
method: false,
shorthand: false
},
{
type: 'Property',
key: { type: 'Identifier', name: 'attributes' },
value: attributes,
kind: 'init',
computed: false,
method: false,
shorthand: false
}
]
};
if (children) {
o.properties.push({
type: 'Property',
key: { type: 'Identifier', name: 'childrenEscaped' },
value: children,
kind: 'init',
computed: false,
method: false,
shorthand: false
});
}
return o;
}
function isRenderObject(e: Node) {
return e.type === 'ObjectExpression' && e.properties[0] && e.properties[0].type === 'Property'
&& e.properties[0].key.type === 'Identifier' && e.properties[0].key.name === 'render'
;
}
function isObjectAssignCall(e: Node) {
return e.type === 'CallExpression' && e.callee.type === 'MemberExpression'
&& e.callee.object.type === 'Identifier' && e.callee.object.name === 'Object'
&& e.callee.property.type === 'Identifier' && e.callee.property.name === 'assign'
;
}
function createEscapedLiteral(v: string | number | boolean | null | RegExp | undefined): Literal {
return { type: 'Literal', value: v, escape: true } as any;
}
function isEscapedLiteral(e: Node): boolean {
return e.type === 'Literal' && (e as any).escape === true;
}
function isCreateElementCall(e: Node) {
return e.type === 'CallExpression' && e.callee.type === 'MemberExpression'
&& e.callee.object.type === 'Identifier'
&& e.callee.property.type === 'Identifier' && e.callee.property.name === 'createElement';
}
function isConcatenatable(e: Expression): boolean {
return extractStaticString(e) !== undefined || e.type === 'Identifier';
}
function toHtmlCall(jsxRuntime: string, expression: Expression): CallExpression {
return {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: { type: 'Identifier', name: jsxRuntime },
computed: false,
optional: false,
property: { type: 'Identifier', name: 'html' }
},
optional: false,
arguments: [expression]
};
}
// function toEscapeCall(jsxRuntime: string, expression: Expression): CallExpression {
// return {
// type: 'CallExpression',
// callee: {
// type: 'MemberExpression',
// object: { type: 'Identifier', name: jsxRuntime },
// computed: false,
// optional: false,
// property: { type: 'Identifier', name: 'escape' }
// },
// optional: false,
// arguments: [expression]
// };
// }
function toEscapeAttributeCall(jsxRuntime: string, expression: Expression): CallExpression {
return {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: { type: 'Identifier', name: jsxRuntime },
computed: false,
optional: false,
property: { type: 'Identifier', name: 'escapeAttribute' }
},
optional: false,
arguments: [expression]
};
}
export const noStaticValue: unique symbol = Symbol('');
export function extractStaticString(e: Expression | SpreadElement): string | typeof noStaticValue {
if (e.type === 'Literal') return '' + e.value;
if (e.type === 'TemplateLiteral' && e.expressions.length === 0) {
return e.quasis.map(v => v.value.cooked).join('');
}
const safeString = getSafeString(e);
if (safeString !== undefined) return extractStaticString(safeString);
return noStaticValue;
}
function unwrapSpread(e: Expression | SpreadElement): Expression {
return e.type === 'SpreadElement' ? e.argument : e;
}
function isHtmlCall(object: Expression | SpreadElement): boolean {
return getHtmlCallArg(object) !== undefined;
}
function getHtmlCallArg(e: Expression | SpreadElement): Expression | undefined {
const expression = unwrapCallExpression(e);
if (!expression) return;
if (e.type === 'CallExpression' && expression.type === 'MemberExpression'
&& expression.object.type === 'Identifier' && expression.object.name === '_jsx'
&& expression.property.type === 'Identifier' && expression.property.name === 'html'
&& e.arguments[0]) return unwrapSpread(e.arguments[0]);
if (e.type === 'CallExpression' && expression.type === 'MemberExpression'
&& expression.object.type === 'Identifier'
&& expression.property.type === 'Identifier' && expression.property.name === 'html'
&& e.arguments[0]) return unwrapSpread(e.arguments[0]);
if (e.type === 'CallExpression' && expression.type === 'Identifier' && expression.name === 'html' && e.arguments[0]) {
return unwrapSpread(e.arguments[0]);
}
return;
}
function isSafeCall(object: Expression | SpreadElement): boolean {
return getSafeCallArg(object) !== undefined;
}
function getSafeCallArg(e: Expression | SpreadElement): Expression | undefined {
const expression = unwrapCallExpression(e);
if (!expression) return;
if (e.type === 'CallExpression' && expression.type === 'MemberExpression'
&& expression.object.type === 'Identifier' && expression.object.name === '_jsx'
&& expression.property.type === 'Identifier' && expression.property.name === 'safe'
&& e.arguments[0]) return unwrapSpread(e.arguments[0]);
if (e.type === 'CallExpression' && expression.type === 'MemberExpression'
&& expression.object.type === 'Identifier'
&& expression.property.type === 'Identifier' && expression.property.name === 'safe'
&& e.arguments[0]) return unwrapSpread(e.arguments[0]);
if (e.type === 'CallExpression' && expression.type === 'Identifier' && expression.name === 'safe' && e.arguments[0]) {
return unwrapSpread(e.arguments[0]);
}
return;
}
function isChildren(e: Expression | SpreadElement): boolean {
if (e.type === 'MemberExpression' && (e.object.type === 'Identifier' || e.object.type === 'ThisExpression')
&& e.property.type === 'Identifier' && e.property.name === 'children') return true;
if (e.type === 'Identifier' && e.name === 'children') {
return true;
}
return false;
}
function getSafeString(e: Expression | SpreadElement): Expression | undefined {
if (e.type === 'ObjectExpression' && e.properties.length === 1 && e.properties[0].type === 'Property' && e.properties[0].key.type === 'MemberExpression'
&& e.properties[0].key.property.type === 'Identifier' && e.properties[0].key.property.name === 'safeString'
&& !isPattern(e.properties[0].value)
) {
return e.properties[0].value;
}
return;
}
function toSafeString(jsxRuntime: string, e: Expression): ObjectExpression {
return {
type: 'ObjectExpression',
properties: [
{
type: 'Property',
key: {
type: 'MemberExpression',
object: { type: 'Identifier', name: jsxRuntime },
computed: false,
property: { type: 'Identifier', name: 'safeString' }
} as MemberExpression,
value: e,
kind: 'init',
computed: true,
method: false,
shorthand: false
}
]
};
}
function isEscapeCall(e: Expression | SpreadElement): boolean {
const expression = unwrapCallExpression(e);
if (!expression) return false;
return e.type === 'CallExpression' && expression.type === 'MemberExpression'
&& expression.object.type === 'Identifier'
&& expression.property.type === 'Identifier'
&& (expression.property.name === 'escape' || expression.property.name === 'escapeAttribute')
;
}
export function isOptimisedHtmlString(e: Expression | SpreadElement): boolean {
//utils_1.escape(page).htmlString
//or (0, utils_1.escape)(page).htmlString
if (e.type !== 'MemberExpression') return false;
const expression = unwrapCallExpression(e.object);
if (!expression) return false;
return expression.type === 'MemberExpression'
&& expression.property.type === 'Identifier' && expression.property.name === 'escape'
&& e.property.type === 'Identifier' && e.property.name === 'htmlString';
}
function isUserEscapeCall(e: Expression | SpreadElement): boolean {
const expression = unwrapCallExpression(e);
if (!expression) return false;
if (e.type === 'CallExpression' && expression.type === 'MemberExpression'
&& expression.object.type === 'Identifier'
&& expression.property.type === 'Identifier'
&& (expression.property.name === 'escape')) {
return true;
}
return e.type === 'CallExpression' && expression.type === 'Identifier' && expression.name === 'escape' && e.arguments[0] !== undefined;
}
function extractChildrenFromObjectExpressionProperties(props: Array): Expression | undefined {
for (let i = 0; i < props.length; i++) {
const prop = props[i];
if (prop.type === 'Property' && prop.key.type === 'Identifier' && prop.key.name === 'children') {
props.splice(i, 1);
return prop.value as Expression;
}
}
return;
}
type ConcatableType = Expression | Identifier;
type OptimisedBinaryExpression = BinaryExpression & { _optimised?: boolean };
type OptimisedArrayExpression = ArrayExpression & { _optimised?: boolean };
function isOptimisedArrayExpression(e: Expression): e is OptimisedArrayExpression {
return e.type === 'ArrayExpression' && (e as any)._optimised === true;
}
function isOptimisedBinaryExpression(e: Expression): e is OptimisedBinaryExpression {
return e.type === 'BinaryExpression' && (e as any)._optimised === true;
}
function extractBinaryExpressions(e: BinaryExpression, expressions?: Expression[]): Expression[] {
expressions ||= [];
if (e.left.type === 'BinaryExpression') {
extractBinaryExpressions(e.left, expressions);
} else {
expressions.push(e.left);
}
if (e.right.type === 'BinaryExpression') {
extractBinaryExpressions(e.right, expressions);
} else {
expressions.push(e.right);
}
return expressions;
}
/**
* We convert .jsx/.jsxs back to createElement syntax to have one optimization syntax.
* createElement is used by TSX as well as fallback when `` is used.
*
*
* _jsx("div", { id: "123" }, void 0)
* -> _jsx.createElement("div", {id: "123"}}
*
* _jsx("div", { children: "Test" }, void 0)
* -> _jsx.createElement("div", {}, "Test")
*
* _jsx("div", Object.assign({ id: "123" }, { children: "Test" }), void 0)
* -> _jsx.createElement("div", {id: "123"}, "Test"}
*
* _jsx("div", Object.assign({ id: "123" }, { children: _jsx('b", { children: "strong' }, void 0) }), void 0);
* -> _jsx.createElement("div", {id: "123"}, _jsx.createElement("b", {}, "strong"))
*/
function convertNodeToCreateElement(node: Expression): Expression {
if (node.type !== 'CallExpression') return node;
let isCJS = isCjsJSXCall(node);
const isAvoidedCJS = isAvoidedThisCjsJSXCall(node);
const isESM = isESMJSXCall(node);
if (!isCJS && !isESM && !isAvoidedCJS) return node;
if (isESM) {
//rewrite to _jsx.createElement
node.callee = {
type: 'MemberExpression',
object: { type: 'Identifier', name: '_jsx' },
computed: false,
property: { type: 'Identifier', name: 'createElement' }
} as MemberExpression;
} else if (isAvoidedCJS) {
//convert (0, x)() to x()
if (node.type === 'CallExpression' && node.callee.type === 'SequenceExpression') {
const expression = node.callee.expressions[1];
node.callee = expression;
isCJS = true;
}
}
if (isCJS) {
if (node.callee.type === 'MemberExpression' && node.callee.property.type === 'Identifier') {
node.callee.property.name = 'createElement';
}
}
node.arguments.splice(2); //remove void 0
if (!node.arguments[1]) return node;
if (node.arguments[1].type === 'CallExpression' && node.arguments[1].callee.type === 'MemberExpression' && node.arguments[1].callee.object.type === 'Identifier' && node.arguments[1].callee.object.name === 'Object') {
//Object.assign(), means we have 2 entries, one with attributes, and second with `children`
// Object.assign({id: 123}, {children: "Test"}) or
// Object.assign({}, props, { id: "123" }, { children: "Test" })
const objectAssignsArgs = node.arguments[1].arguments;
const lastArgument = objectAssignsArgs[objectAssignsArgs.length - 1];
if (lastArgument.type !== 'ObjectExpression') throw new Error(`Expect ObjectExpression, got ${JSON.stringify(lastArgument)}`);
const children = extractChildrenFromObjectExpressionProperties(lastArgument.properties);
if (children) {
if (children.type === 'ArrayExpression') {
node.arguments.push(...children.elements.map(v => convertNodeToCreateElement(v as Expression)));
} else {
node.arguments.push(convertNodeToCreateElement(children));
}
}
if (objectAssignsArgs.length > 2) {
//remove last
objectAssignsArgs.splice(objectAssignsArgs.length - 1, 1);
} else {
if (objectAssignsArgs[0].type === 'ObjectExpression') {
node.arguments[1] = { type: 'ObjectExpression', properties: objectAssignsArgs[0].properties };
}
}
} else if (node.arguments[1].type === 'ObjectExpression') {
//simple {}
const children = extractChildrenFromObjectExpressionProperties(node.arguments[1].properties);
if (children) {
if (children.type === 'ArrayExpression') {
node.arguments.push(...children.elements.map(v => convertNodeToCreateElement(v as Expression)));
} else {
node.arguments.push(convertNodeToCreateElement(children));
}
}
}
return node;
}
export function optimizeJSX(code: string): string {
const tree = parse(code);
replace(tree, (node: any) => {
if (isESMJSXCall(node) || isCjsJSXCall(node) || isAvoidedThisCjsJSXCall(node)) {
return optimizeNode(convertNodeToCreateElement(node), true);
}
return node;
});
return generate(tree);
}
export function convertJsxCodeToCreateElement(code: string): string {
const tree = parse(code);
replace(tree, (node: any) => {
if (isESMJSXCall(node) || isCjsJSXCall(node) || isAvoidedThisCjsJSXCall(node)) {
return convertNodeToCreateElement(node);
}
return node;
});
return generate(tree);
}