Skip to content

Commit c20acd7

Browse files
committed
✨Add support for functional elements
1 parent 0410550 commit c20acd7

File tree

9 files changed

+124
-55
lines changed

9 files changed

+124
-55
lines changed

jsx-factory.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* @typedef {import('./lib/core.js').Element} Element
3+
* @typedef {import('./lib/core.js').Root} Root
4+
* @typedef {import('./lib/core.js').HResult} HResult
5+
* @typedef {import('./lib/core.js').HArrayChild} HArrayChild
6+
* @typedef {import('./lib/core.js').HProperties} HProperties
7+
* @typedef {import('./lib/core.js').HPropertyValue} HPropertyValue
8+
* @typedef {import('./lib/core.js').HStyle} HStyle
9+
* @typedef {import('./lib/core.js').core} Core
10+
*
11+
* @typedef {Record<string, unknown>} JSXProps
12+
* @typedef {(props: JSXProps) => HResult } HFn
13+
*/
14+
15+
import {h as hast} from './index.js'
16+
17+
/**
18+
* @param {string | null | HFn} typeOrFn
19+
* @param { JSXProps | undefined } props
20+
* @param { HArrayChild } children
21+
* @returns HResult
22+
*/
23+
export function h(typeOrFn, props, ...children) {
24+
if (typeof typeOrFn === 'function') {
25+
return typeOrFn({...props, children})
26+
}
27+
28+
// @ts-expect-error just doo eeet.
29+
return hast(typeOrFn ?? undefined, props, children)
30+
}

lib/jsx-automatic.d.ts

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,26 @@
11
import type {HProperties, HChild, HResult} from './core.js'
22

3-
export namespace JSX {
4-
/**
5-
* This defines the return value of JSX syntax.
6-
*/
7-
type Element = HResult
8-
9-
/**
10-
* This disallows the use of functional components.
11-
*/
12-
type IntrinsicAttributes = never
13-
14-
/**
15-
* This defines the prop types for known elements.
16-
*
17-
* For `hastscript` this defines any string may be used in combination with `hast` `Properties`.
18-
*
19-
* This **must** be an interface.
20-
*/
21-
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style, @typescript-eslint/consistent-type-definitions
22-
interface IntrinsicElements {
23-
[name: string]:
24-
| HProperties
25-
| {
26-
/**
27-
* The prop that matches `ElementChildrenAttribute` key defines the type of JSX children, defines the children type.
28-
*/
29-
children?: HChild
30-
}
31-
}
3+
declare global {
4+
namespace JSX {
5+
/**
6+
* This defines the return value of JSX syntax.
7+
*/
8+
type Element = HResult
329

33-
/**
34-
* The key of this interface defines as what prop children are passed.
35-
*/
36-
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
37-
interface ElementChildrenAttribute {
3810
/**
39-
* Only the key matters, not the value.
11+
* This defines the prop types for known elements.
12+
*
13+
* For `hastscript` this defines any string may be used in combination with `hast` `Properties`.
14+
*
15+
* This **must** be an interface.
4016
*/
41-
children?: never
17+
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style, @typescript-eslint/consistent-type-definitions
18+
interface IntrinsicElements {
19+
[name: string]:
20+
| HProperties
21+
| {
22+
children?: HChild
23+
}
24+
}
4225
}
4326
}

lib/runtime.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* @typedef {import('./core.js').core} Core
1010
*
1111
* @typedef {Record<string, HPropertyValue | HStyle | HChild>} JSXProps
12+
* @typedef {(props: JSXProps) => HResult } HFn
1213
*/
1314

1415
/**
@@ -26,13 +27,19 @@ export function runtime(f) {
2627
*/
2728
(
2829
/**
29-
* @param {string | null} type
30+
* @param {string | null | HFn} typeOrFn
3031
* @param {HProperties & {children?: HChild}} props
3132
* @returns {HResult}
3233
*/
33-
function (type, props) {
34+
function (typeOrFn, props) {
35+
if (typeof typeOrFn === 'function') {
36+
return typeOrFn(props)
37+
}
38+
3439
const {children, ...properties} = props
35-
return type === null ? f(type, children) : f(type, properties, children)
40+
return typeOrFn === null
41+
? f(typeOrFn, children)
42+
: f(typeOrFn, properties, children)
3643
}
3744
)
3845

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"./index.js": "./index.js",
3737
"./html.js": "./html.js",
3838
"./svg.js": "./svg.js",
39+
"./jsx-factory": "./jsx-factory.js",
3940
"./jsx-runtime": "./jsx-runtime.js",
4041
"./jsx-dev-runtime": "./jsx-runtime.js",
4142
"./html/jsx-runtime": "./html/jsx-runtime.js",

readme.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -257,20 +257,13 @@ The syntax tree is [hast][].
257257
258258
## JSX
259259
260-
This package can be used with JSX.
261-
You should use the automatic JSX runtime set to `hastscript` (also available as
262-
the more explicit name `hastscript/html`) or `hastscript/svg`.
260+
This package can be used with JSX by setting your
261+
[`jsxImportSource`][jsx-import-source] to `hastscript`
263262
264263
> 👉 **Note**: while `h` supports dots (`.`) for classes or number signs (`#`)
265264
> for IDs in `selector`, those are not supported in JSX.
266265
267-
> 🪦 **Legacy**: you can also use the classic JSX runtime, but this is not
268-
> recommended.
269-
> To do so, import `h` (or `s`) yourself and define it as the pragma (plus
270-
> set the fragment to `null`).
271-
272-
The Use example above can then be written like so, using inline pragmas, so
273-
that SVG can be used too:
266+
For example, to write the hastscript example provided earlier u
274267
275268
`example-html.jsx`:
276269
@@ -287,6 +280,8 @@ console.log(
287280
)
288281
```
289282
283+
SVG can be used too by setting `jxsImportSource` to `hastscript/svg`:
284+
290285
`example-svg.jsx`:
291286
292287
```jsx
@@ -299,6 +294,15 @@ console.log(
299294
)
300295
```
301296
297+
> 🪦 **Legacy**: you can also use the classic JSX runtime by importing either
298+
> `h` (or `s`) manually from `hastscript/jsx-factory` and setting it as the
299+
> `jsxFactory` in your transpiler, but it is not recommended. For example:
300+
301+
```jsx
302+
/** @jsxFactory h */
303+
import { h } from "hastscript/jsx-factory";
304+
```
305+
302306
## Types
303307

304308
This package is fully typed with [TypeScript][].
@@ -466,3 +470,5 @@ abide by its terms.
466470
[properties]: #properties-1
467471

468472
[result]: #result
473+
474+
[jsx-import-source]: https://react.dev/jsx-transform

script/generate-jsx.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ await fs.writeFile(
1616
plugins: [acornJsx()],
1717
module: true
1818
}),
19-
{pragma: 'h', pragmaFrag: 'null'}
19+
{pragma: 'createElement', pragmaFrag: 'null'}
2020
)
2121
).value
2222
)

test-d/automatic-h.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,11 @@ expectError(<a invalid={[true]} />)
5252

5353
expectType<Result>(<a children={<b />} />)
5454

55-
declare function Bar(props?: Record<string, unknown>): Element
56-
expectError(<Bar />)
55+
// You can't use nonprimitive attribute values for primitive elements
56+
expectError(<a children={() => null} />)
57+
58+
export function F1({children}: {children: () => string}) {
59+
return <p>{children()}</p>
60+
}
61+
62+
expectType<Result>(<F1 children={() => 'Hello World'} />)

test-d/automatic-s.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,3 @@ expectError(<a invalid={[true]} />)
4141
// The automatic runtime the children prop to define JSX children, whereas it’s used as an attribute in the classic runtime.
4242

4343
expectType<Result>(<a children={<b />} />)
44-
45-
declare function Bar(props?: Record<string, unknown>): Element
46-
expectError(<Bar />)

test/jsx.jsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import assert from 'node:assert/strict'
44
import test from 'node:test'
55
import {u} from 'unist-builder'
66
import {h} from '../index.js'
7+
// eslint-disable-next-line no-unused-vars
8+
import {h as createElement} from '../jsx-factory.js'
79

810
test('name', () => {
911
assert.deepEqual(<a />, h('a'), 'should support a self-closing element')
@@ -125,4 +127,41 @@ test('name', () => {
125127
]),
126128
'should support a fragment in an element (#2)'
127129
)
130+
131+
/**
132+
* @typedef {import('../lib/core.js').HChild} HChild
133+
134+
* @param {{title: string; definition: string, children?: HChild}} options
135+
* @returns {JSX.Element}
136+
*/
137+
// eslint-disable-next-line no-unused-vars
138+
const Dl = ({title, definition, children}) => (
139+
<dl>
140+
<dt>{title}</dt>
141+
<dd>{definition}</dd>
142+
{children}
143+
</dl>
144+
)
145+
146+
assert.deepEqual(
147+
<Dl title={'Firefox'} definition={'A red panda.'} />,
148+
h('dl', [h('dt', 'Firefox'), h('dd', 'A red panda.')]),
149+
'should support functional elements'
150+
)
151+
152+
assert.deepEqual(
153+
<Dl title={'Firefox'} definition={'A red panda.'}>
154+
<>
155+
<dt>Chrome</dt>
156+
<dd>A chemical element.</dd>
157+
</>
158+
</Dl>,
159+
h('dl', [
160+
h('dt', 'Firefox'),
161+
h('dd', 'A red panda.'),
162+
h('dt', 'Chrome'),
163+
h('dd', 'A chemical element.')
164+
]),
165+
'should support functional elements that render their children'
166+
)
128167
})

0 commit comments

Comments
 (0)