Skip to content

Commit cddf999

Browse files
committed
Experimental JSX transformation
1 parent d589eb3 commit cddf999

File tree

7 files changed

+157
-0
lines changed

7 files changed

+157
-0
lines changed

build/files.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ export default (target, plugins) => [
1515
file: `./dist/${target}/cdn.js`,
1616
}
1717
},
18+
{
19+
plugins,
20+
input: './src/jsx/index.js',
21+
output: {
22+
esModule: true,
23+
file: `./dist/${target}/jsx.js`,
24+
}
25+
},
1826
{
1927
plugins,
2028
input: './src/json/index.js',

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@
5353
"types": "./types/dom/index.d.ts",
5454
"description": "explicit uhtml browsers' production export"
5555
},
56+
"./jsx": {
57+
"import": "./dist/prod/jsx.js",
58+
"types": "./types/jsx/index.d.ts",
59+
"description": "experimental - transform templates into JSX compatible syntax"
60+
},
5661
"./dom": {
5762
"import": "./src/dom/index.js",
5863
"types": "./types/dom/index.d.ts",

src/jsx/index.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//@ts-nocheck
2+
3+
import {
4+
ELEMENT,
5+
Node,
6+
Comment,
7+
DocumentType,
8+
Text,
9+
Fragment,
10+
Element,
11+
Component,
12+
fromJSON,
13+
props,
14+
} from '../dom/ish.js';
15+
16+
import {
17+
COMPONENT,
18+
DOTS,
19+
KEY,
20+
HOLE,
21+
update,
22+
} from './update.js';
23+
24+
import parser from '../parser/index.js';
25+
import resolve from '../json/resolve.js';
26+
import { assign } from '../utils.js';
27+
28+
const textParser = parser({
29+
Comment,
30+
DocumentType,
31+
Text,
32+
Fragment,
33+
Element,
34+
Component,
35+
update,
36+
});
37+
38+
const { stringify, parse } = JSON;
39+
const isNode = node => node instanceof Node;
40+
const get = node => node.props === props ? (node.props = props) : node.props;
41+
42+
export default (jsx, jsxs = jsx) => {
43+
const twm = new WeakMap;
44+
const cache = (template, values) => {
45+
const parsed = textParser(template, values, true);
46+
parsed[0] = parse(stringify(parsed[0]));
47+
twm.set(template, parsed);
48+
return parsed;
49+
};
50+
51+
const getProps = (node) => {
52+
const { children } = node;
53+
if (children.length) get(node).children = children.map(getValue);
54+
return get(node);
55+
};
56+
57+
const getValue = node => {
58+
if (isNode(node)) {
59+
return node.type === ELEMENT ?
60+
getInvoke(node)(node.name, getProps(node)) :
61+
node.toString()
62+
;
63+
}
64+
return node;
65+
};
66+
67+
const getInvoke = ({ children }) => (jsx === jsxs || children.every(isNode)) ? jsx : jsxs;
68+
69+
return (template, ...values) => {
70+
const [json, updates] = twm.get(template) || cache(template, values);
71+
// TODO: this could be mapped once so that every consecutive call
72+
// will simply loop over values and updates[length](values[length])
73+
// before returning the list or arguments to pass to jsx or jsxs
74+
// this way it'd be way faster on repeated invokes, if needed/desired
75+
const root = fromJSON(json);
76+
let length = values.length, args, prev, node;
77+
while (length--) {
78+
const [path, type] = updates[length];
79+
const value = values[length];
80+
if (prev !== path) {
81+
node = resolve(root, path);
82+
prev = path;
83+
args = [node.name, getProps(node)];
84+
}
85+
if (type === COMPONENT) {
86+
args[0] = value;
87+
const { children } = node.parent;
88+
children[children.indexOf(node)] = getInvoke(node)(...args);
89+
}
90+
else if (type === KEY) args.push(value);
91+
else if (type === DOTS) assign(get(node), value);
92+
else if (type === HOLE) {
93+
const { children } = node.parent;
94+
children[children.indexOf(node)] = value;
95+
}
96+
else get(node)[type] = value;
97+
}
98+
return getValue(root.children[0]);
99+
};
100+
};

src/jsx/update.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {
2+
ATTRIBUTE as TEMPLATE_ATTRIBUTE,
3+
COMMENT as TEMPLATE_COMMENT,
4+
COMPONENT as TEMPLATE_COMPONENT,
5+
} from '../dom/ish.js';
6+
7+
export const COMPONENT = 1;
8+
export const DOTS = 3;
9+
export const KEY = 4;
10+
export const HOLE = 5;
11+
12+
export const update = (_, type, path, name) => {
13+
switch (type) {
14+
case TEMPLATE_COMMENT: return [path, HOLE];
15+
case TEMPLATE_COMPONENT: return [path, COMPONENT];
16+
case TEMPLATE_ATTRIBUTE: {
17+
switch (name) {
18+
case 'key': return [path, KEY];
19+
case '...': return [path, DOTS];
20+
default: return [path, name];
21+
}
22+
}
23+
}
24+
};

test/jsx.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<script type="module">
3+
import { jsx, jsxs } from 'https://esm.run/react/jsx-runtime';
4+
import { createRoot } from 'https://esm.run/react-dom/client';
5+
6+
import uhtml from '../src/jsx/index.js';
7+
// const x = uhtml((...args) => args);
8+
const x = uhtml(jsx, jsxs);
9+
10+
createRoot(document.body).render(
11+
x`<div a="1" b=${2}>Hello</div>`
12+
);
13+
</script>

types/jsx/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
declare function _default(jsx: any, jsxs?: any): (template: any, ...values: any[]) => any;
2+
export default _default;

types/jsx/update.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const COMPONENT: 1;
2+
export const DOTS: 3;
3+
export const KEY: 4;
4+
export const HOLE: 5;
5+
export function update(_: any, type: any, path: any, name: any): any[];

0 commit comments

Comments
 (0)