Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions lib/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
* List of children.
* @typedef {Node | HPrimitiveChild | HArrayChild} HChild
* Acceptable child value.
* @typedef {Record<string, unknown>} HComponentProperties
* Acceptable properties for a component function.
* @typedef {(props: HComponentProperties) => HResult} HComponent
* Function returning a hyperscript result (useful for JSX)
*/

import {find, normalize} from 'property-information'
Expand All @@ -58,18 +62,38 @@ export function core(schema, defaultTagName, caseSensitive) {
* (selector: null | undefined, ...children: Array<HChild>): Root
* (selector: string, properties: HProperties, ...children: Array<HChild>): Element
* (selector: string, ...children: Array<HChild>): Element
* (component: HComponent, props: HComponentProperties, ...children: Array<HChild>): HResult
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we support h(Component)? h(Component, null)? h(Component, Child)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And h(Component, {children: ['a']}, 'b')?

* }}
*/
(
/**
* Hyperscript compatible DSL for creating virtual hast trees.
*
* @param {string | null | undefined} [selector]
* @param {HProperties | HChild | null | undefined} [properties]
* @param {string | null | undefined | HComponent} [selector]
* @param {HProperties | HChild | null | undefined | HComponentProperties} [properties]
* @param {Array<HChild>} children
* @returns {HResult}
*/
function (selector, properties, ...children) {
if (typeof selector === 'function') {
const props = properties ?? {}
if (typeof props !== 'object') {
// We should rarely hit this case because this codepath is only
// called by JSX transforms, but we have to make the check to
// satisfy TypeScript.
throw new TypeError(
`second argument to h(${
selector.name
}, props) must be an object, but was ${typeof props}`
)
}

return selector({
...props,
children
})
}

let index = -1
/** @type {HResult} */
let node
Expand Down Expand Up @@ -98,6 +122,7 @@ export function core(schema, defaultTagName, caseSensitive) {
}
}
} else {
// @ts-expect-error cannot be `HComponentProperties` because we're not on that codepath.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn’t it be easier to reuse HProperties for props, instead of a different but similar HComponentProperties?
That would solve this. And probably make it easier to create a component MyLink which wraps a and internally calls h('a', {...props, something: true}), which would currently fail I think?

children.unshift(properties)
}
}
Expand All @@ -120,7 +145,7 @@ export function core(schema, defaultTagName, caseSensitive) {
}

/**
* @param {HProperties | HChild} value
* @param {HProperties | HChild | HComponentProperties } value
* @param {string} name
* @returns {value is HProperties}
*/
Expand Down
55 changes: 19 additions & 36 deletions lib/jsx-automatic.d.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,26 @@
import type {HProperties, HChild, HResult} from './core.js'

export namespace JSX {
/**
* This defines the return value of JSX syntax.
*/
type Element = HResult

/**
* This disallows the use of functional components.
*/
type IntrinsicAttributes = never

/**
* This defines the prop types for known elements.
*
* For `hastscript` this defines any string may be used in combination with `hast` `Properties`.
*
* This **must** be an interface.
*/
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style, @typescript-eslint/consistent-type-definitions
interface IntrinsicElements {
[name: string]:
| HProperties
| {
/**
* The prop that matches `ElementChildrenAttribute` key defines the type of JSX children, defines the children type.
*/
children?: HChild
}
}
declare global {
namespace JSX {
/**
* Return value of JSX syntax.
*/
type Element = HResult

/**
* The key of this interface defines as what prop children are passed.
*/
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface ElementChildrenAttribute {
/**
* Only the key matters, not the value.
* This defines the prop types for known elements.
*
* For `hastscript` this defines any string may be used in combination with `hast` `Properties`.
*
* This **must** be an interface.
Comment on lines +11 to +15
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* This defines the prop types for known elements.
*
* For `hastscript` this defines any string may be used in combination with `hast` `Properties`.
*
* This **must** be an interface.
* Prop types for known elements.
*
* We allow any tag name, mapped to `HProperties`, and allow a `children` field.
*
* Note: this **must** be an interface.
*/
children?: never
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style, @typescript-eslint/consistent-type-definitions
interface IntrinsicElements {
[name: string]:
| HProperties
| {
children?: HChild
}
}
}
}
1 change: 1 addition & 0 deletions lib/jsx-automatic.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/// <reference types="./jsx-automatic.d.ts"/>
// Empty (only used for TypeScript).
export {}
15 changes: 12 additions & 3 deletions lib/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* @typedef {import('./core.js').core} Core
*
* @typedef {Record<string, HPropertyValue | HStyle | HChild>} JSXProps
* @typedef {(props: Record<string, unknown>) => HResult} HComponent
*/

/**
Expand All @@ -26,13 +27,21 @@ export function runtime(f) {
*/
(
/**
* @param {string | null} type
* @param {string | null | HComponent} component
* @param {HProperties & {children?: HChild}} props
* @returns {HResult}
*/
function (type, props) {
function (component, props, key) {
// If (typeof component === 'function') {
// return component({...props, key})
// }

const {children, ...properties} = props
return type === null ? f(type, children) : f(type, properties, children)

return component === null
? f(component, children)
: // @ts-expect-error we can handle it.
f(component, {...properties, key}, children)
}
)

Expand Down
26 changes: 16 additions & 10 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,20 +257,13 @@ The syntax tree is [hast][].

## JSX

This package can be used with JSX.
You should use the automatic JSX runtime set to `hastscript` (also available as
the more explicit name `hastscript/html`) or `hastscript/svg`.
This package can be used with JSX by setting your
[`jsxImportSource`][jsx-import-source] to `hastscript`

> 👉 **Note**: while `h` supports dots (`.`) for classes or number signs (`#`)
> for IDs in `selector`, those are not supported in JSX.

> 🪦 **Legacy**: you can also use the classic JSX runtime, but this is not
> recommended.
> To do so, import `h` (or `s`) yourself and define it as the pragma (plus
> set the fragment to `null`).

The Use example above can then be written like so, using inline pragmas, so
that SVG can be used too:
For example, to write the hastscript example provided earlier u

`example-html.jsx`:

Expand All @@ -287,6 +280,8 @@ console.log(
)
```

SVG can be used too by setting `jxsImportSource` to `hastscript/svg`:

`example-svg.jsx`:

```jsx
Expand All @@ -299,6 +294,15 @@ console.log(
)
```

> 🪦 **Legacy**: you can also use the classic JSX runtime by importing either
> `h` (or `s`) manually from `hastscript/jsx-factory` and setting it as the
> `jsxFactory` in your transpiler, but it is not recommended. For example:

```jsx
/** @jsxFactory h */
import { h } from "hastscript/jsx-factory";
```

## Types

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

[result]: #result

[jsx-import-source]: https://babeljs.io/docs/babel-plugin-transform-react-jsx#customizing-the-automatic-runtime-import
2 changes: 1 addition & 1 deletion test-d/automatic-h.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ expectError(<a invalid={[true]} />)
expectType<Result>(<a children={<b />} />)

declare function Bar(props?: Record<string, unknown>): Element
expectError(<Bar />)
expectType<Result>(<Bar />)
2 changes: 1 addition & 1 deletion test-d/automatic-s.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ expectError(<a invalid={[true]} />)
expectType<Result>(<a children={<b />} />)

declare function Bar(props?: Record<string, unknown>): Element
expectError(<Bar />)
expectType<Result>(<Bar />)
37 changes: 37 additions & 0 deletions test/jsx.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
/** @jsxImportSource hastscript */

/**
* @typedef {import('../lib/core.js').HChild} HChild
*/

import assert from 'node:assert/strict'
import test from 'node:test'
import {u} from 'unist-builder'
Expand Down Expand Up @@ -125,4 +129,37 @@ test('name', () => {
]),
'should support a fragment in an element (#2)'
)

/**
* @param {{key: HChild; value: HChild}} options
* @returns {JSX.Element}
*/
// eslint-disable-next-line no-unused-vars
const DlEntry = ({key, value}) => (
<>
<dt>{key}</dt>
<dd>{value}</dd>
</>
)

/**
* @param {{children?: HChild}} options
* @returns {JSX.Element}
*/
// eslint-disable-next-line no-unused-vars
const Dl = ({children}) => <dl>{children}</dl>

assert.deepEqual(
<Dl>
<DlEntry key="Firefox" value="A red panda." />
<DlEntry key="Chrome" value="A chemical element." />
</Dl>,
h('dl', [
h('dt', 'Firefox'),
h('dd', 'A red panda.'),
h('dt', 'Chrome'),
h('dd', 'A chemical element.')
]),
'should support functional elements'
)
})