@knighted/jsx 1.0.0-alpha.2 → 1.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist/lite/index.js +4 -0
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -96,6 +96,16 @@ When you are not using a bundler, load the module directly from a CDN that under
|
|
|
96
96
|
|
|
97
97
|
If you are building locally with Vite/Rollup/Webpack make sure the WASM binding is installable (see the `npm_config_ignore_platform` tip above) so the bundler can resolve `@oxc-parser/binding-wasm32-wasi`.
|
|
98
98
|
|
|
99
|
+
### Lite bundle entry
|
|
100
|
+
|
|
101
|
+
If you already run this package through your own bundler you can trim a few extra kilobytes by importing the minified entry:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { jsx } from '@knighted/jsx/lite'
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The `lite` export ships the exact same API as the default entry but is pre-minified via `tsup`, so bundlers have less work to do and browsers download ~10% less code. No functionality is removed—you can freely swap between the standard and lite imports.
|
|
108
|
+
|
|
99
109
|
## Testing
|
|
100
110
|
|
|
101
111
|
Run the Vitest suite (powered by jsdom) to exercise the DOM runtime and component support:
|
|
@@ -108,7 +118,7 @@ Tests live in `test/jsx.test.ts` and cover DOM props/events, custom components,
|
|
|
108
118
|
|
|
109
119
|
## Browser demo / Vite build
|
|
110
120
|
|
|
111
|
-
This repo ships with a ready-to-run Vite demo under `examples/browser` that bundles the library (and the WASM binding vendored in `vendor/binding-wasm32-wasi`). Use it for a full end-to-end verification in a real browser:
|
|
121
|
+
This repo ships with a ready-to-run Vite demo under `examples/browser` that bundles the library (and the WASM binding vendored in `vendor/binding-wasm32-wasi`). Use it for a full end-to-end verification in a real browser (the demo now imports `@knighted/jsx/lite` so you can confirm the lighter entry behaves identically):
|
|
112
122
|
|
|
113
123
|
```sh
|
|
114
124
|
# Start a dev server at http://localhost:5173
|
|
@@ -119,7 +129,7 @@ npm run build:demo
|
|
|
119
129
|
npm run preview
|
|
120
130
|
```
|
|
121
131
|
|
|
122
|
-
The Vite config aliases `@oxc-parser/binding-wasm32-wasi` to the vendored copy so you don’t have to perform any extra install tricks locally, while production consumers can still rely on the published package.
|
|
132
|
+
The Vite config aliases `@oxc-parser/binding-wasm32-wasi` to the vendored copy so you don’t have to perform any extra install tricks locally, while production consumers can still rely on the published package. For a zero-build verification of the lite bundle, open `examples/esm-demo-lite.html` directly in your browser (it sources `https://esm.sh/@knighted/jsx/lite`).
|
|
123
133
|
|
|
124
134
|
## Limitations
|
|
125
135
|
|
|
@@ -136,7 +146,7 @@ Tradeoffs to keep in mind:
|
|
|
136
146
|
- **Parser vs tokenizer** – `htm` performs lightweight string tokenization, while `@knighted/jsx` pays a higher one-time parse cost but gains the full JSX grammar (fragments, spread children, nested namespaces) without heuristics. For large or deeply nested templates the WASM-backed parser is typically faster and more accurate than string slicing.
|
|
137
147
|
- **DOM-first rendering** – this runtime builds DOM nodes directly, so the cost after parsing is mostly attribute assignment and child insertion. `htm` usually feeds a virtual DOM/hyperscript factory (e.g., Preact’s `h`), which may add an extra abstraction layer before hitting the DOM.
|
|
138
148
|
- **Bundle size** – including the parser and WASM binding is heavier than `htm`’s ~1 kB tokenizer. If you just need hyperscript sugar, `htm` stays leaner; if you value real JSX semantics without a build step, the extra kilobytes buy you correctness and speed on complex trees.
|
|
139
|
-
- **Actual size** – the
|
|
149
|
+
- **Actual size** – the default `dist/jsx.js` bundle is ~13.9 kB raw / ~3.6 kB min+gzip, while the new `@knighted/jsx/lite` entry is ~5.7 kB raw / ~2.5 kB min+gzip. `htm` weighs in at roughly 0.7 kB min+gzip, so the lite entry narrows the gap to ~1.8 kB for production payloads.
|
|
140
150
|
|
|
141
151
|
In short, `@knighted/jsx` trades a slightly larger runtime for the ability to parse genuine JSX with native performance, whereas `htm` favors minimal footprint and hyperscript integration. Pick the tool that aligns with your rendering stack and performance envelope.
|
|
142
152
|
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import {parseSync}from'oxc-parser';var N=/<\s*$/,C=/<\/\s*$/,k="__KX_EXPR__",A=0,R={lang:"jsx",sourceType:"module",range:true,preserveParens:true},j=()=>{if(typeof document>"u"||typeof document.createElement!="function")throw new Error("The jsx template tag requires a DOM-like environment (document missing).")},$=e=>{let n=`[oxc-parser] ${e.message}`;if(e.labels?.length){let t=e.labels[0];t.message&&(n+=`
|
|
2
|
+
${t.message}`);}return e.codeframe&&(n+=`
|
|
3
|
+
${e.codeframe}`),n},_=e=>{for(let n of e.body)if(n.type==="ExpressionStatement"){let t=n.expression;if(t.type==="JSXElement"||t.type==="JSXFragment")return t}throw new Error("The jsx template must contain a single JSX element or fragment.")},y=e=>{switch(e.type){case "JSXIdentifier":return e.name;case "JSXNamespacedName":return `${e.namespace.name}:${e.name.name}`;case "JSXMemberExpression":return `${y(e.object)}.${e.property.name}`;default:return ""}},P=e=>typeof Node>"u"?false:e instanceof Node||e instanceof DocumentFragment,O=e=>!e||typeof e=="string"?false:typeof e[Symbol.iterator]=="function",S=e=>!e||typeof e!="object"&&typeof e!="function"?false:typeof e.then=="function",F=e=>{let t=e.replace(/\r/g,"").replace(/\n\s+/g," ").trim();return t.length>0?t:""},I=(e,n,t)=>{if(!(t===false||t===null||t===void 0)){if(n==="dangerouslySetInnerHTML"&&typeof t=="object"&&t&&"__html"in t){e.innerHTML=String(t.__html??"");return}if(n==="ref"){if(typeof t=="function"){t(e);return}if(t&&typeof t=="object"){t.current=e;return}}if(n==="style"&&typeof t=="object"&&t!==null){let r=t,s=e.style;if(!s)return;let o=s;Object.entries(r).forEach(([a,i])=>{if(i!=null){if(a.startsWith("--")){s.setProperty(a,String(i));return}o[a]=i;}});return}if(typeof t=="function"&&n.startsWith("on")){let r=n.slice(2).toLowerCase();e.addEventListener(r,t);return}if(n==="class"||n==="className"){let r=Array.isArray(t)?t.filter(Boolean).join(" "):String(t);e.setAttribute("class",r);return}if(n==="htmlFor"){e.setAttribute("for",String(t));return}if(n in e&&!n.includes("-")){e[n]=t;return}e.setAttribute(n,t===true?"":String(t));}},u=(e,n)=>{if(n!=null&&typeof n!="boolean"){if(S(n))throw new Error("Async values are not supported inside jsx template results.");if(Array.isArray(n)){n.forEach(t=>u(e,t));return}if(O(n)){for(let t of n)u(e,t);return}if(P(n)){e.appendChild(n);return}e.appendChild(document.createTextNode(String(n)));}},b=(e,n)=>{let t={};return e.forEach(r=>{if(r.type==="JSXSpreadAttribute"){let o=d(r.argument,n);o&&typeof o=="object"&&!Array.isArray(o)&&Object.assign(t,o);return}let s=y(r.name);if(!r.value){t[s]=true;return}if(r.value.type==="Literal"){t[s]=r.value.value;return}if(r.value.type==="JSXExpressionContainer"){if(r.value.expression.type==="JSXEmptyExpression")return;t[s]=d(r.value.expression,n);}}),t},T=(e,n,t)=>{let r=b(n,t);Object.entries(r).forEach(([s,o])=>{if(s!=="key"){if(s==="children"){u(e,o);return}I(e,s,o);}});},x=(e,n,t)=>{let r=[];return e.forEach(s=>{switch(s.type){case "JSXText":{let o=F(s.value);o&&r.push(o);break}case "JSXExpressionContainer":{if(s.expression.type==="JSXEmptyExpression")break;r.push(d(s.expression,n));break}case "JSXSpreadChild":{let o=d(s.expression,n);o!=null&&r.push(o);break}case "JSXElement":case "JSXFragment":{r.push(J(s,n,t));break}}}),r},M=(e,n,t,r)=>{let s=b(e.openingElement.attributes,n),o=x(e.children,n,r);o.length===1?s.children=o[0]:o.length>1&&(s.children=o);let a=t(s);if(S(a))throw new Error("Async jsx components are not supported.");return a},D=(e,n,t)=>{let r=e.openingElement,s=y(r.name),o=n.components.get(s);if(o)return M(e,n,o,t);if(/[A-Z]/.test(s[0]??""))throw new Error(`Unknown component "${s}". Did you interpolate it with the template literal?`);let a=s==="svg"?"svg":t,i=s==="foreignObject"?null:a,c=a==="svg"?document.createElementNS("http://www.w3.org/2000/svg",s):document.createElement(s);return T(c,r.attributes,n),x(e.children,n,i).forEach(m=>u(c,m)),c},J=(e,n,t)=>{if(e.type==="JSXFragment"){let r=document.createDocumentFragment();return x(e.children,n,t).forEach(o=>u(r,o)),r}return D(e,n,t)},g=(e,n)=>{if(!e||typeof e!="object")return;let t=e;typeof t.type=="string"&&(n(t),Object.values(t).forEach(r=>{if(r){if(Array.isArray(r)){r.forEach(s=>g(s,n));return}typeof r=="object"&&g(r,n);}}));},L=(e,n)=>{let t=new Set;return g(e,r=>{r.type==="Identifier"&&n.placeholders.has(r.name)&&t.add(r.name);}),Array.from(t)},d=(e,n)=>{if(e.type==="JSXElement"||e.type==="JSXFragment")return J(e,n,null);if(!("range"in e)||!e.range)throw new Error("Unable to evaluate expression: missing source range information.");let[t,r]=e.range,s=n.source.slice(t,r),o=L(e,n);try{let a=new Function(...o,`"use strict"; return (${s});`),i=o.map(c=>n.placeholders.get(c));return a(...i)}catch(a){throw new Error(`Failed to evaluate expression ${s}: ${a.message}`)}},B=e=>{let n=e.replace(/[^a-zA-Z0-9_$]/g,"");return n?/[A-Za-z_$]/.test(n[0])?n:`Component${n}`:"Component"},z=(e,n,t)=>{let r=t.get(e);if(r)return r;let s=e.displayName||e.name||`Component${n.length}`,o=B(s),a=o,i=1;for(;n.some(p=>p.name===a);)a=`${o}${i++}`;let c={name:a,value:e};return n.push(c),t.set(e,c),c},V=(e,n)=>{let t=e.raw??e,r=new Map,s=[],o=new Map,a=t[0]??"",i=A++,c=0;for(let p=0;p<n.length;p++){let m=t[p]??"",f=t[p+1]??"",l=n[p],h=N.test(m)||C.test(m);if(h&&typeof l=="function"){let w=z(l,s,o);a+=w.name+f;continue}if(h&&typeof l=="string"){a+=l+f;continue}let E=`${k}${i}_${c++}__`;r.set(E,l),a+=E+f;}return {source:a,placeholders:r,bindings:s}},W=(e,...n)=>{j();let t=V(e,n),r=parseSync("inline.jsx",t.source,R);if(r.errors.length>0)throw new Error($(r.errors[0]));let s=_(r.program),o={source:t.source,placeholders:t.placeholders,components:new Map(t.bindings.map(a=>[a.name,a.value]))};return J(s,o,null)};
|
|
4
|
+
export{W as jsx};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knighted/jsx",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-rc.0",
|
|
4
4
|
"description": "A simple JSX transpiler that runs in node.js or the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jsx browser transform",
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
"import": "./dist/index.js",
|
|
21
21
|
"default": "./dist/index.js"
|
|
22
22
|
},
|
|
23
|
+
"./lite": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/lite/index.js",
|
|
26
|
+
"default": "./dist/lite/index.js"
|
|
27
|
+
},
|
|
23
28
|
"./package.json": "./package.json"
|
|
24
29
|
},
|
|
25
30
|
"engines": {
|
|
@@ -27,7 +32,7 @@
|
|
|
27
32
|
},
|
|
28
33
|
"engineStrict": true,
|
|
29
34
|
"scripts": {
|
|
30
|
-
"build": "duel",
|
|
35
|
+
"build": "duel && npm run build:lite",
|
|
31
36
|
"precheck-types": "npm run build",
|
|
32
37
|
"check-types": "npm run check-types:lib && npm run check-types:demo",
|
|
33
38
|
"check-types:lib": "tsc --noEmit --project tsconfig.json",
|
|
@@ -39,6 +44,7 @@
|
|
|
39
44
|
"dev": "vite dev --config vite.config.ts",
|
|
40
45
|
"build:demo": "vite build --config vite.config.ts",
|
|
41
46
|
"preview": "vite preview --config vite.config.ts",
|
|
47
|
+
"build:lite": "tsup --config tsup.config.ts",
|
|
42
48
|
"prepack": "npm run build"
|
|
43
49
|
},
|
|
44
50
|
"devDependencies": {
|
|
@@ -49,6 +55,7 @@
|
|
|
49
55
|
"eslint": "^9.39.1",
|
|
50
56
|
"jsdom": "^27.2.0",
|
|
51
57
|
"prettier": "^3.7.3",
|
|
58
|
+
"tsup": "^8.3.0",
|
|
52
59
|
"typescript": "^5.9.3",
|
|
53
60
|
"typescript-eslint": "^8.48.0",
|
|
54
61
|
"vite": "^7.2.4",
|