Skip to content

Commit 7993463

Browse files
committed
Add support for passing components
1 parent 4c3f940 commit 7993463

File tree

9 files changed

+170
-7
lines changed

9 files changed

+170
-7
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
coverage/
55
node_modules/
66
yarn.lock
7+
!lib/components.d.ts

index.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/**
2+
* @typedef {import('./lib/components.js').Components} Components
23
* @typedef {import('./lib/index.js').Fragment} Fragment
34
* @typedef {import('./lib/index.js').Jsx} Jsx
45
* @typedef {import('./lib/index.js').JsxDev} JsxDev

lib/components.d.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Basic functional component: given props, returns an element.
3+
*
4+
* @typeParam ComponentProps
5+
* Props type.
6+
* @param props
7+
* Props.
8+
* @returns
9+
* Result.
10+
*/
11+
export type FunctionComponent<ComponentProps> = (
12+
props: ComponentProps
13+
) => JSX.Element | string | null | undefined
14+
15+
/**
16+
* Class component: given props, returns an instance.
17+
*
18+
* @typeParam ComponentProps
19+
* Props type.
20+
* @param props
21+
* Props.
22+
* @returns
23+
* Instance.
24+
*/
25+
export type ClassComponent<ComponentProps> = new (
26+
props: ComponentProps
27+
) => JSX.ElementClass
28+
29+
/**
30+
* Function or class component.
31+
*
32+
* You can access props at `JSX.IntrinsicElements`.
33+
* For example, to find props for `a`, use `JSX.IntrinsicElements['a']`.
34+
*
35+
* @typeParam ComponentProps
36+
* Props type.
37+
*/
38+
export type Component<ComponentProps> =
39+
| FunctionComponent<ComponentProps>
40+
| ClassComponent<ComponentProps>
41+
42+
/**
43+
* Possible components to use.
44+
*
45+
* Each key is a tag name typed in `JSX.IntrinsicElements`.
46+
* Each value is a component accepting the corresponding props or a different
47+
* tag name.
48+
*
49+
* You can access props at `JSX.IntrinsicElements`.
50+
* For example, to find props for `a`, use `JSX.IntrinsicElements['a']`.
51+
52+
*/
53+
export type Components = {
54+
[TagName in keyof JSX.IntrinsicElements]:
55+
| Component<JSX.IntrinsicElements[TagName]>
56+
| keyof JSX.IntrinsicElements
57+
}

lib/components.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// TypeScript only.
2+
export {}

lib/index.js

+20-3
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
* @typedef {import('hast').Content} Content
44
* @typedef {import('hast').Element} Element
55
* @typedef {import('hast').Root} Root
6+
* @typedef {import('./components.js').Components} Components
67
*/
78

89
/**
910
* @typedef {Content | Root} Node
1011
* @typedef {Extract<Node, import('unist').Parent>} Parent
11-
*
12+
*/
13+
14+
/**
1215
* @typedef {unknown} Fragment
1316
* Represent the children, typically a symbol.
1417
*
@@ -61,7 +64,7 @@
6164
* @typedef {[string, Value]} Field
6265
* Property field.
6366
*
64-
* @typedef {JSX.Element | string} Child
67+
* @typedef {JSX.Element | string | null | undefined} Child
6568
* Child.
6669
*
6770
* @typedef {{children: Array<Child>, [prop: string]: Value | Array<Child>}} Props
@@ -84,6 +87,8 @@
8487
* Info passed around.
8588
* @property {string | undefined} filePath
8689
* File path.
90+
* @property {Partial<Components>} components
91+
* Components to swap.
8792
* @property {Schema} schema
8893
* Current schema.
8994
* @property {unknown} Fragment
@@ -93,6 +98,11 @@
9398
*
9499
* @typedef RegularFields
95100
* Configuration.
101+
* @property {Partial<Components> | null | undefined} [components]
102+
* Components to use.
103+
*
104+
* Each key is the name of an HTML (or SVG) element to override.
105+
* The value is the component to render instead.
96106
* @property {string | null | undefined} [filePath]
97107
* File path to the original source file.
98108
*
@@ -215,6 +225,7 @@ export function toJsxRuntime(tree, options) {
215225
const state = {
216226
Fragment: options.Fragment,
217227
schema: options.space === 'svg' ? svg : html,
228+
components: options.components || {},
218229
filePath,
219230
create
220231
}
@@ -262,9 +273,15 @@ function one(state, node, key) {
262273
}
263274

264275
const children = createChildren(state, node)
265-
const type = node.type === 'root' ? state.Fragment : node.tagName
266276
const props = createProperties(state, node)
267277

278+
let type = node.type === 'root' ? state.Fragment : node.tagName
279+
280+
if (typeof type === 'string' && own.call(state.components, type)) {
281+
const key = /** @type {keyof JSX.IntrinsicElements} */ (type)
282+
type = state.components[key]
283+
}
284+
268285
props.children = children
269286

270287
// Restore parent schema.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"prettier": true,
8181
"#": "`n` is wrong",
8282
"rules": {
83+
"@typescript-eslint/ban-types": "off",
8384
"n/file-extension-in-import": "off"
8485
}
8586
},

readme.md

+40-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ with an automatic JSX runtime.
2020
* [API](#api)
2121
* [`toJsxRuntime(tree, options)`](#tojsxruntimetree-options)
2222
* [`Options`](#options)
23+
* [`Components`](#components-1)
2324
* [`Fragment`](#fragment-1)
2425
* [`Jsx`](#jsx-1)
2526
* [`JsxDev`](#jsxdev-1)
@@ -143,6 +144,13 @@ Static JSX ([`Jsx`][jsx], required in production).
143144

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

147+
###### `components`
148+
149+
Components to use ([`Partial<Components>`][components], optional).
150+
151+
Each key is the name of an HTML (or SVG) element to override.
152+
The value is the component to render instead.
153+
146154
###### `development`
147155

148156
Whether to use `jsxDEV` when on or `jsx` and `jsxs` when off (`boolean`,
@@ -170,6 +178,33 @@ it.
170178
> Passing SVG might break but fragments of modern SVG should be fine.
171179
> Use `xast` if you need to support SVG as XML.
172180
181+
### `Components`
182+
183+
Possible components to use (TypeScript type).
184+
185+
Each key is a tag name typed in `JSX.IntrinsicElements`.
186+
Each value is a component accepting the corresponding props or a different tag
187+
name.
188+
189+
You can access props at `JSX.IntrinsicElements`.
190+
For example, to find props for `a`, use `JSX.IntrinsicElements['a']`.
191+
192+
###### Type
193+
194+
```ts
195+
type Components = {
196+
[TagName in keyof JSX.IntrinsicElements]:
197+
| Component<JSX.IntrinsicElements[TagName]>
198+
| keyof JSX.IntrinsicElements
199+
}
200+
201+
type Component<ComponentProps> =
202+
// Function component:
203+
| ((props: ComponentProps) => JSX.Element | string | null | undefined)
204+
// Class component:
205+
| (new (props: ComponentProps) => JSX.ElementClass)
206+
```
207+
173208
### `Fragment`
174209
175210
Represent the children, typically a symbol (TypeScript type).
@@ -348,9 +383,9 @@ followed by browsers such as Chrome, Firefox, and Safari.
348383
## Types
349384

350385
This package is fully typed with [TypeScript][].
351-
It exports the additional types [`Fragment`][fragment], [`Jsx`][jsx],
352-
[`JsxDev`][jsxdev], [`Options`][options], [`Props`][props], [`Source`][source],
353-
and [`Space`][Space].
386+
It exports the additional types [`Components`][components],
387+
[`Fragment`][fragment], [`Jsx`][jsx], [`JsxDev`][jsxdev], [`Options`][options],
388+
[`Props`][props], [`Source`][source], and [`Space`][Space].
354389

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

465500
[space]: #space-1
501+
502+
[components]: #components-1

test/index.js

+47
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import assert from 'node:assert/strict'
99
import test from 'node:test'
10+
import React from 'react'
1011
import * as prod from 'react/jsx-runtime'
1112
import * as dev from 'react/jsx-dev-runtime'
1213
import {renderToStaticMarkup} from 'react-dom/server'
@@ -255,6 +256,7 @@ test('properties', () => {
255256
'should support properties in the SVG space'
256257
)
257258
})
259+
258260
test('children', () => {
259261
assert.equal(
260262
renderToStaticMarkup(toJsxRuntime(h('a'), production)),
@@ -354,3 +356,48 @@ test('source', () => {
354356
return source
355357
}
356358
})
359+
360+
test('components', () => {
361+
assert.equal(
362+
renderToStaticMarkup(
363+
toJsxRuntime(h('b#x'), {
364+
...production,
365+
components: {
366+
b(props) {
367+
// Note: types for this are working.
368+
assert(props.id === 'x')
369+
return 'a'
370+
}
371+
}
372+
})
373+
),
374+
'a',
375+
'should support function components'
376+
)
377+
378+
assert.equal(
379+
renderToStaticMarkup(
380+
toJsxRuntime(h('b#x'), {
381+
...production,
382+
components: {
383+
b: class extends React.Component {
384+
/**
385+
* @param {JSX.IntrinsicElements['b']} props
386+
*/
387+
constructor(props) {
388+
super(props)
389+
// Note: types for this are working.
390+
assert(props.id === 'x')
391+
}
392+
393+
render() {
394+
return 'a'
395+
}
396+
}
397+
}
398+
})
399+
),
400+
'a',
401+
'should support class components'
402+
)
403+
})

tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"include": ["**/**.js"],
2+
"include": ["**/**.js", "lib/components.d.ts"],
33
"exclude": ["coverage/", "node_modules/"],
44
"compilerOptions": {
55
"checkJs": true,

0 commit comments

Comments
 (0)