Skip to content

Commit f1b4d19

Browse files
committed
✨Add support for function elements
1 parent 0410550 commit f1b4d19

File tree

8 files changed

+115
-54
lines changed

8 files changed

+115
-54
lines changed

lib/core.js

+28-3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
* List of children.
3333
* @typedef {Node | HPrimitiveChild | HArrayChild} HChild
3434
* Acceptable child value.
35+
* @typedef {Record<string, unknown>} HComponentProperties
36+
* Acceptable properties for a component function.
37+
* @typedef {(props: HComponentProperties) => HResult} HComponent
38+
* Function returning a hyperscript result (useful for JSX)
3539
*/
3640

3741
import {find, normalize} from 'property-information'
@@ -58,18 +62,38 @@ export function core(schema, defaultTagName, caseSensitive) {
5862
* (selector: null | undefined, ...children: Array<HChild>): Root
5963
* (selector: string, properties: HProperties, ...children: Array<HChild>): Element
6064
* (selector: string, ...children: Array<HChild>): Element
65+
* (component: HComponent, props: HComponentProperties, ...children: Array<HChild>): HResult
6166
* }}
6267
*/
6368
(
6469
/**
6570
* Hyperscript compatible DSL for creating virtual hast trees.
6671
*
67-
* @param {string | null | undefined} [selector]
68-
* @param {HProperties | HChild | null | undefined} [properties]
72+
* @param {string | null | undefined | HComponent} [selector]
73+
* @param {HProperties | HChild | null | undefined | HComponentProperties} [properties]
6974
* @param {Array<HChild>} children
7075
* @returns {HResult}
7176
*/
7277
function (selector, properties, ...children) {
78+
if (typeof selector === 'function') {
79+
const props = properties ?? {}
80+
if (typeof props !== 'object') {
81+
// We should rarely hit this case because this codepath is only
82+
// called by JSX transforms, but we have to make the check to
83+
// satisfy TypeScript.
84+
throw new TypeError(
85+
`second argument to h(${
86+
selector.name
87+
}, props) must be an object, but was ${typeof props}`
88+
)
89+
}
90+
91+
return selector({
92+
...props,
93+
children
94+
})
95+
}
96+
7397
let index = -1
7498
/** @type {HResult} */
7599
let node
@@ -98,6 +122,7 @@ export function core(schema, defaultTagName, caseSensitive) {
98122
}
99123
}
100124
} else {
125+
// @ts-expect-error cannot be `HComponentProperties` because we're not on that codepath.
101126
children.unshift(properties)
102127
}
103128
}
@@ -120,7 +145,7 @@ export function core(schema, defaultTagName, caseSensitive) {
120145
}
121146

122147
/**
123-
* @param {HProperties | HChild} value
148+
* @param {HProperties | HChild | HComponentProperties } value
124149
* @param {string} name
125150
* @returns {value is HProperties}
126151
*/

lib/jsx-automatic.d.ts

+19-36
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+
* 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/jsx-automatic.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
/// <reference types="./jsx-automatic.d.ts"/>
12
// Empty (only used for TypeScript).
23
export {}

lib/runtime.js

+12-3
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: Record<string, unknown>) => HResult} HComponent
1213
*/
1314

1415
/**
@@ -26,13 +27,21 @@ export function runtime(f) {
2627
*/
2728
(
2829
/**
29-
* @param {string | null} type
30+
* @param {string | null | HComponent} component
3031
* @param {HProperties & {children?: HChild}} props
3132
* @returns {HResult}
3233
*/
33-
function (type, props) {
34+
function (component, props, key) {
35+
// If (typeof component === 'function') {
36+
// return component({...props, key})
37+
// }
38+
3439
const {children, ...properties} = props
35-
return type === null ? f(type, children) : f(type, properties, children)
40+
41+
return component === null
42+
? f(component, children)
43+
: // @ts-expect-error we can handle it.
44+
f(component, {...properties, key}, children)
3645
}
3746
)
3847

readme.md

+16-10
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://babeljs.io/docs/babel-plugin-transform-react-jsx#customizing-the-automatic-runtime-import

test-d/automatic-h.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@ expectError(<a invalid={[true]} />)
5353
expectType<Result>(<a children={<b />} />)
5454

5555
declare function Bar(props?: Record<string, unknown>): Element
56-
expectError(<Bar />)
56+
expectType<Result>(<Bar />)

test-d/automatic-s.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,4 @@ expectError(<a invalid={[true]} />)
4343
expectType<Result>(<a children={<b />} />)
4444

4545
declare function Bar(props?: Record<string, unknown>): Element
46-
expectError(<Bar />)
46+
expectType<Result>(<Bar />)

test/jsx.jsx

+37
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,41 @@ test('name', () => {
125125
]),
126126
'should support a fragment in an element (#2)'
127127
)
128+
129+
/**
130+
* @typedef {import('../lib/core.js').HChild} HChild
131+
*/
132+
133+
/**
134+
* @param {{key: HChild; value: HChild}} options
135+
* @returns {JSX.Element}
136+
*/
137+
// eslint-disable-next-line no-unused-vars
138+
const DlEntry = ({key, value}) => (
139+
<>
140+
<dt>{key}</dt>
141+
<dd>{value}</dd>
142+
</>
143+
)
144+
145+
/**
146+
* @param {{ children?: HChild }} options
147+
* @returns {JSX.Element}
148+
*/
149+
// eslint-disable-next-line no-unused-vars
150+
const Dl = ({children}) => <dl>{children}</dl>
151+
152+
assert.deepEqual(
153+
<Dl>
154+
<DlEntry key="Firefox" value="A red panda." />
155+
<DlEntry key="Chrome" value="A chemical element." />
156+
</Dl>,
157+
h('dl', [
158+
h('dt', 'Firefox'),
159+
h('dd', 'A red panda.'),
160+
h('dt', 'Chrome'),
161+
h('dd', 'A chemical element.')
162+
]),
163+
'should support functional elements'
164+
)
128165
})

0 commit comments

Comments
 (0)