Proposal: ESX as core JS feature

another variant could be having all types as one shape:

({
  __proto__: ESXToken.prototype,
  id: null | {},
  type: STATIC_TYPE | RUNTIME_TYPE,
  name: '#fragment' | '#component' | tagName,
  value: null | Function,         // the reference class or callback for #component
  properties: null | {...props},  // optional properties forwarded just like in JSX for #component
  attributes: null | ESXToken[],  // none, one, or more {type, name, value} attributes not for #component
  children: null | ESXToken[]     // a list of children like in JSX. These can be anything
})

The peculiarity of this approach is that the token itself states if it's static or runtime, the kind can be retrieved by the name where:

  • a #fragment won't have neither properties nor attributes or value, just children and, optionally, an id, but only if it's runtime as static fragments within nodes don't get one
  • a #component won't have attributes (or should it?) but it could have an id for consistency sake, in case it's an outer component instead of an inner one. This simplifies the render logic as it can just check if previous id is same, or no id at all was present for a specific container.
  • any other element might have an id if this is an outer template, they won't have a value or properties, just details around attributes.
  • attributes, if present, can be just a list of {type, name, value} without needing to be specific token instances (desirable though)
  • children, if present, are still a list of tokens, otherwise it's impossible to distinguish <>A</> from <>{condition ? 'A' : 'B'}</>. Children can use exact same shape of attribute as {type, name, value} where the name could be #child or #content, making parsing children pretty easy out there.

This version nukes the ability to have <#thingy /> in the future but I don't think anyone would mind.

With this version there could be 2 globally defined classes, and maybe we can allow making these constructable.

class ESXToken {
  static STATIC   = 0;
  static RUNTIME  = 1;
  constructor(type:number, name:string, value:null|function) {
    this.type = type;
    this.name = name;
    this.value = value;
  }
}

class ESXNode extends ESXToken {
  constructor(
    type:number, name:string, value:null|function,
    id:null|object,
    children:null|ESXToken[],
    properties:null|object = null,
    attributes:null|ESXToken[] = null
  ) {
    super(type, name, value);
    this.id = id;
    this.children = children;
    this.properties = properties;
    this.attributes = attributes;
  }
}

The helpers for the transformers can then be something similar to this:

const component = (type, value, id, properties, ...children) =>
  new ESXNode(type, '#component', value, id, children, properties);
const element = (type, name, id, attributes, ...children) =>
  new ESXNode(type, name, null, id, children, null, attributes);
const fragment = (type, id, ...children) =>
  new ESXNode(type, '#fragment', null, id, children);

const attribute = (type, name, value) => new ESXToken(type, name, value);
const child = (type, value) => new ESXToken(type, '#content', value);

so all the details around everything is still available, the separation of concern among children or attributes is still in place, and it should be relatively straight forward for library authors to orchestrate any render being SSR, browser, cloud worker, and so on.

I am not sure I can simplify this further, otherwise precious details will be lost in the process.