Skip to content

Commit

Permalink
Add support for passing components
Browse files Browse the repository at this point in the history
  • Loading branch information
wooorm committed Jan 18, 2023
1 parent 4c3f940 commit 7993463
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 7 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
coverage/
node_modules/
yarn.lock
!lib/components.d.ts
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/**
* @typedef {import('./lib/components.js').Components} Components
* @typedef {import('./lib/index.js').Fragment} Fragment
* @typedef {import('./lib/index.js').Jsx} Jsx
* @typedef {import('./lib/index.js').JsxDev} JsxDev
Expand Down
57 changes: 57 additions & 0 deletions lib/components.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Basic functional component: given props, returns an element.
*
* @typeParam ComponentProps
* Props type.
* @param props
* Props.
* @returns
* Result.
*/
export type FunctionComponent<ComponentProps> = (
props: ComponentProps
) => JSX.Element | string | null | undefined

/**
* Class component: given props, returns an instance.
*
* @typeParam ComponentProps
* Props type.
* @param props
* Props.
* @returns
* Instance.
*/
export type ClassComponent<ComponentProps> = new (
props: ComponentProps
) => JSX.ElementClass

/**
* Function or class component.
*
* You can access props at `JSX.IntrinsicElements`.
* For example, to find props for `a`, use `JSX.IntrinsicElements['a']`.
*
* @typeParam ComponentProps
* Props type.
*/
export type Component<ComponentProps> =
| FunctionComponent<ComponentProps>
| ClassComponent<ComponentProps>

/**
* Possible components to use.
*
* Each key is a tag name typed in `JSX.IntrinsicElements`.
* Each value is a component accepting the corresponding props or a different
* tag name.
*
* You can access props at `JSX.IntrinsicElements`.
* For example, to find props for `a`, use `JSX.IntrinsicElements['a']`.
*/
export type Components = {
[TagName in keyof JSX.IntrinsicElements]:
| Component<JSX.IntrinsicElements[TagName]>
| keyof JSX.IntrinsicElements
}
2 changes: 2 additions & 0 deletions lib/components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// TypeScript only.
export {}
23 changes: 20 additions & 3 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
* @typedef {import('hast').Content} Content
* @typedef {import('hast').Element} Element
* @typedef {import('hast').Root} Root
* @typedef {import('./components.js').Components} Components
*/

/**
* @typedef {Content | Root} Node
* @typedef {Extract<Node, import('unist').Parent>} Parent
*
*/

/**
* @typedef {unknown} Fragment
* Represent the children, typically a symbol.
*
Expand Down Expand Up @@ -61,7 +64,7 @@
* @typedef {[string, Value]} Field
* Property field.
*
* @typedef {JSX.Element | string} Child
* @typedef {JSX.Element | string | null | undefined} Child
* Child.
*
* @typedef {{children: Array<Child>, [prop: string]: Value | Array<Child>}} Props
Expand All @@ -84,6 +87,8 @@
* Info passed around.
* @property {string | undefined} filePath
* File path.
* @property {Partial<Components>} components
* Components to swap.
* @property {Schema} schema
* Current schema.
* @property {unknown} Fragment
Expand All @@ -93,6 +98,11 @@
*
* @typedef RegularFields
* Configuration.
* @property {Partial<Components> | null | undefined} [components]
* Components to use.
*
* Each key is the name of an HTML (or SVG) element to override.
* The value is the component to render instead.
* @property {string | null | undefined} [filePath]
* File path to the original source file.
*
Expand Down Expand Up @@ -215,6 +225,7 @@ export function toJsxRuntime(tree, options) {
const state = {
Fragment: options.Fragment,
schema: options.space === 'svg' ? svg : html,
components: options.components || {},
filePath,
create
}
Expand Down Expand Up @@ -262,9 +273,15 @@ function one(state, node, key) {
}

const children = createChildren(state, node)
const type = node.type === 'root' ? state.Fragment : node.tagName
const props = createProperties(state, node)

let type = node.type === 'root' ? state.Fragment : node.tagName

if (typeof type === 'string' && own.call(state.components, type)) {
const key = /** @type {keyof JSX.IntrinsicElements} */ (type)
type = state.components[key]
}

props.children = children

// Restore parent schema.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"prettier": true,
"#": "`n` is wrong",
"rules": {
"@typescript-eslint/ban-types": "off",
"n/file-extension-in-import": "off"
}
},
Expand Down
43 changes: 40 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ with an automatic JSX runtime.
* [API](#api)
* [`toJsxRuntime(tree, options)`](#tojsxruntimetree-options)
* [`Options`](#options)
* [`Components`](#components-1)
* [`Fragment`](#fragment-1)
* [`Jsx`](#jsx-1)
* [`JsxDev`](#jsxdev-1)
Expand Down Expand Up @@ -143,6 +144,13 @@ Static JSX ([`Jsx`][jsx], required in production).

Development JSX ([`JsxDev`][jsxdev], required in development).

###### `components`

Components to use ([`Partial<Components>`][components], optional).

Each key is the name of an HTML (or SVG) element to override.
The value is the component to render instead.

###### `development`

Whether to use `jsxDEV` when on or `jsx` and `jsxs` when off (`boolean`,
Expand Down Expand Up @@ -170,6 +178,33 @@ it.
> Passing SVG might break but fragments of modern SVG should be fine.
> Use `xast` if you need to support SVG as XML.
### `Components`

Possible components to use (TypeScript type).

Each key is a tag name typed in `JSX.IntrinsicElements`.
Each value is a component accepting the corresponding props or a different tag
name.

You can access props at `JSX.IntrinsicElements`.
For example, to find props for `a`, use `JSX.IntrinsicElements['a']`.

###### Type

```ts
type Components = {
[TagName in keyof JSX.IntrinsicElements]:
| Component<JSX.IntrinsicElements[TagName]>
| keyof JSX.IntrinsicElements
}

type Component<ComponentProps> =
// Function component:
| ((props: ComponentProps) => JSX.Element | string | null | undefined)
// Class component:
| (new (props: ComponentProps) => JSX.ElementClass)
```
### `Fragment`
Represent the children, typically a symbol (TypeScript type).
Expand Down Expand Up @@ -348,9 +383,9 @@ followed by browsers such as Chrome, Firefox, and Safari.
## Types

This package is fully typed with [TypeScript][].
It exports the additional types [`Fragment`][fragment], [`Jsx`][jsx],
[`JsxDev`][jsxdev], [`Options`][options], [`Props`][props], [`Source`][source],
and [`Space`][Space].
It exports the additional types [`Components`][components],
[`Fragment`][fragment], [`Jsx`][jsx], [`JsxDev`][jsxdev], [`Options`][options],
[`Props`][props], [`Source`][source], and [`Space`][Space].

The function `toJsxRuntime` returns a `JSX.Element`, which means that the JSX
namespace has to by typed.
Expand Down Expand Up @@ -463,3 +498,5 @@ abide by its terms.
[source]: #source

[space]: #space-1

[components]: #components-1
47 changes: 47 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import assert from 'node:assert/strict'
import test from 'node:test'
import React from 'react'
import * as prod from 'react/jsx-runtime'
import * as dev from 'react/jsx-dev-runtime'
import {renderToStaticMarkup} from 'react-dom/server'
Expand Down Expand Up @@ -255,6 +256,7 @@ test('properties', () => {
'should support properties in the SVG space'
)
})

test('children', () => {
assert.equal(
renderToStaticMarkup(toJsxRuntime(h('a'), production)),
Expand Down Expand Up @@ -354,3 +356,48 @@ test('source', () => {
return source
}
})

test('components', () => {
assert.equal(
renderToStaticMarkup(
toJsxRuntime(h('b#x'), {
...production,
components: {
b(props) {
// Note: types for this are working.
assert(props.id === 'x')
return 'a'
}
}
})
),
'a',
'should support function components'
)

assert.equal(
renderToStaticMarkup(
toJsxRuntime(h('b#x'), {
...production,
components: {
b: class extends React.Component {
/**
* @param {JSX.IntrinsicElements['b']} props
*/
constructor(props) {
super(props)
// Note: types for this are working.
assert(props.id === 'x')
}

render() {
return 'a'
}
}
}
})
),
'a',
'should support class components'
)
})
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"include": ["**/**.js"],
"include": ["**/**.js", "lib/components.d.ts"],
"exclude": ["coverage/", "node_modules/"],
"compilerOptions": {
"checkJs": true,
Expand Down

0 comments on commit 7993463

Please sign in to comment.