In a quest to explore improvements over common JSX transformers I have managed to find great performance able to compete with template literal based libraries.
In this document I would like to describe, via JS itself, and as PoC, how JSX could be both rebranded as ESX and improved.
Differently from E4X, but also differently from JSX, this proposal leaves developers provide their own "render" implementation, freeing ESX usage from any previous attempt to confine JSX or E4X into the DOM world, where it's been proven, in the JSX case, it's not really where it belongs, as it can be used as neutral DSL.
This document describes all the moving parts of ESX in a way that:
- there is no DOM at all involved, only new primitives introduced by JSX
- all relevant details around each part of the transformation are described through simple objects, here represented as classes instances, but these easily work just as object literals (easy polyfills via transformers)
- no extra scope pollution is needed, hence no
jsxPragmaorjsxFragmentaround is required at all (noReact.createElementorReact.Fragmentneeded, norudomsay.interpolation) - all classes can be used just as types to infer, as oppsite of being really classes ... no clashing in the logic can happen neither
- hints to "parse-once" through templates and/or Components are all over the place, making usignal like alternative implementations possible, but also any SSR related project can benefit from these
// base class: the only class needed in userland to:
// * check if a child value is an instanceof ESXToken
// * retrieve all XXX_TYPE values to properly parse
class ESXToken {
// property/ies + interpolations + templates
static STATIC_TYPE = 1 << 0; // 1
static MIXED_TYPE = 1 << 1; // 2
static RUNTIME_TYPE = 1 << 2; // 4
static TEMPLATE_TYPE = 1 << 3; // 8
// angle-brackets kind
static ELEMENT_TYPE = 1 << 6; // 64
static FRAGMENT_TYPE = 1 << 7; // 128
static COMPONENT_TYPE = 1 << 8; // 256
/**
* @param {number} type any ESXToken.XXX_TYPE
* @param {any} value the value carried by this token
*/
constructor(type, value) {
this.type = type;
this.value = value;
}
}
// any property
class ESXProperty extends ESXToken {
/**
* @param {Object} property
* @param {number} property.type ESXToken.STATIC_TYPE | ESXToken.MIXED_TYPE | ESXToken.RUNTIME_TYPE
* @param {string} property.name the property name
* @param {any} property.value the property value
*/
constructor(type, name, value) {
super(type, value);
this.name = name;
}
}
// Outer JSX template elements
class ESXTemplate extends ESXToken {
/**
* @param {Object} template
* @param {object} template.id unique template reference
* @param {ESXEntry} template.value the entry value this template carries
*/
constructor(id, value) {
super(ESXToken.TEMPLATE_TYPE, value);
this.id = id;
}
}
// <any /> <>angle</> <Brackets />
class ESXChevrons extends ESXToken {
/**
*
* @param {number} type ESXToken.ELEMENT_TYPE | ESXToken.FRAGMENT_TYPE | ESXToken.COMPONENT_TYPE
* @param {string | null | Function} value
* @param {ESXToken | null} properties
* @param {ESXToken[]} children
*/
constructor(type, value, properties, children) {
super(type, value);
this.properties = properties;
this.children = children;
}
}
// any <tag-name />
class ESXElement extends ESXChevrons {
/**
*
* @param {string} value the tag name
* @param {ESXToken | null} properties the properties, if any
* @param {ESXToken[]} children zero, one, or more element child
*/
constructor(value, properties, ...children) {
super(ESXToken.ELEMENT_TYPE, value, properties, children);
}
}
// any <>fragment</>
class ESXFragment extends ESXChevrons {
/**
*
* @param {null} value fragments have no representation
* @param {ESXToken | null} properties the properties, if any
* @param {ESXToken[]} children zero, one, or more element child
*/
constructor(...children) {
super(ESXToken.FRAGMENT_TYPE, null, null, children);
}
}
// any <Component />
class ESXComponent extends ESXChevrons {
/**
* @param {Function} value the component callback
* @param {ESXToken | null} properties the properties, if any
* @param {ESXToken[]} children zero, one, or more element child
*/
constructor(value, properties, ...children) {
super(ESXToken.COMPONENT_TYPE, value, properties, children);
}
}// ESX
const div = <div />;// internal representation
const templateReference1 = {};
const div = new ESXTemplate(
templateReference1,
new ESXElement('div', null)
);// concrete JS representation
const templateReference1 = {};
const div = {
id: templateReference1,
type: 8,
value: {
type: 64,
value: 'div',
properties: null,
children: []
}
};// ESX
const div = <div a="a" b={"b"}><p>c</p></div>;// internal representation
const templateReference2 = {};
const div = new ESXTemplate(
templateReference2,
new ESXElement(
'div',
// properties token
new ESXToken(
// STATIC_TYPE:
// all properties are static
// MIXED_TYPE:
// properties can also be static
// RUNTIME_TYPE:
// all properties are runtime, such as
// <div even="though" {...spread} />
// case that can overwrite also static props
ESXToken.MIXED_TYPE,
// properties value
[
new ESXProperty(ESXToken.STATIC_TYPE, 'a', 'a'),
new ESXProperty(ESXToken.RUNTIME_TYPE, 'b', 'b')
]
),
// a static child
new ESXToken(
ESXToken.STATIC_TYPE,
new ESXElement(
'p',
null,
new ESXToken(ESXToken.STATIC_TYPE, 'c')
)
)
)
);// concrete JS representation
const templateReference2 = {};
const div = {
id: templateReference2,
type: 8,
value: {
type: 64,
value: 'div',
properties: {
type: 2,
value: [
{type: 1, name: 'a', value: 'a'},
{type: 4, name: 'b', value: 'b'}
]
},
children: [
{
type: 1,
value: {
type: 64,
value: 'p',
properties: null,
children: [
{type: 1, value: 'c'}
]
}
}
]
}
};// ESX
function MyComponent(...args) {
return (
<>
{'A'},
{'B'}
</>
);
}
const props = {a: '', b: 'b'};
const component = <MyComponent a="a" {...props} />;// internal representation
const templateReference3 = {};
const templateReference4 = {};
function MyComponent(...args) {
return new ESXTemplate(
templateReference3,
new ESXFragment(
// child as interpolation
new ESXToken(ESXToken.RUNTIME_TYPE, 'A'),
// the static comma interpolations separator
new ESXToken(ESXToken.STATIC_TYPE, ', '),
// last child as interpolation
new ESXToken(ESXToken.RUNTIME_TYPE, 'B'),
)
);
}
const props = {a: '', b: 'b'};
const component = new ESXTemplate(
templateReference4,
new ESXComponent(
MyComponent,
new ESXToken(
// there is a {...spread} involved which could
// overwrite even known static props
ESXToken.RUNTIME_TYPE,
// properties value
[
new ESXProperty(ESXToken.STATIC_TYPE, 'a', 'a'),
new ESXProperty(ESXToken.RUNTIME_TYPE, 'props', props)
]
)
)
);// concrete JS representation
const templateReference3 = {};
const templateReference4 = {};
function MyComponent(...args) {
return {
id: templateReference3,
type: 8,
value: {
type: 128,
value: null,
properties: null,
children: [
{type: 4, value: 'A'},
{type: 1, value: ', '},
{type: 4, value: 'B'}
]
}
};
}
const props = {a: '', b: 'b'};
const component = {
id: templateReference4,
type: 8,
value: {
type: 256,
value: MyComponent,
properties: {
type: 4,
value: [
{type: 1, name: 'a', value: 'a'},
{type: 4, name: 'props', value: props}
]
},
children: []
}
};If you have ideas around possible improvements, if you want to bring this to the TC39 attention as champion, if have any question related to this proposal, or if you'd like to know more about how udomsay became the fastest and smallest runtime using 90% of this proposal through a dedicated transformer, I will be more than happy if you could reach out, either here, in twitter, or in mastodon.
Thank you very much for your patience reading through this gist 🙏
Simplified implementation
Deprecated
The previous content doesn't matter anymore, because the current updated OP gist represent an already simplified implementation of ESX.
The class is defined in its polyfill repository.