diff --git a/.gitignore b/.gitignore index c977c85..e1b186f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ coverage/ node_modules/ yarn.lock +!lib/components.d.ts diff --git a/index.js b/index.js index 0ef655e..cec9f6d 100644 --- a/index.js +++ b/index.js @@ -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 diff --git a/lib/components.d.ts b/lib/components.d.ts new file mode 100644 index 0000000..63070b7 --- /dev/null +++ b/lib/components.d.ts @@ -0,0 +1,57 @@ +/** + * Basic functional component: given props, returns an element. + * + * @typeParam ComponentProps + * Props type. + * @param props + * Props. + * @returns + * Result. + */ +export type FunctionComponent = ( + 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 = 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 = + | FunctionComponent + | ClassComponent + +/** + * 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 + | keyof JSX.IntrinsicElements +} diff --git a/lib/components.js b/lib/components.js new file mode 100644 index 0000000..3efcbf7 --- /dev/null +++ b/lib/components.js @@ -0,0 +1,2 @@ +// TypeScript only. +export {} diff --git a/lib/index.js b/lib/index.js index a79f2e9..b66c2cf 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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} Parent - * + */ + +/** * @typedef {unknown} Fragment * Represent the children, typically a symbol. * @@ -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, [prop: string]: Value | Array}} Props @@ -84,6 +87,8 @@ * Info passed around. * @property {string | undefined} filePath * File path. + * @property {Partial} components + * Components to swap. * @property {Schema} schema * Current schema. * @property {unknown} Fragment @@ -93,6 +98,11 @@ * * @typedef RegularFields * Configuration. + * @property {Partial | 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. * @@ -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 } @@ -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. diff --git a/package.json b/package.json index 115ca9c..aec48e7 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "prettier": true, "#": "`n` is wrong", "rules": { + "@typescript-eslint/ban-types": "off", "n/file-extension-in-import": "off" } }, diff --git a/readme.md b/readme.md index 801cb9e..d57d9c9 100644 --- a/readme.md +++ b/readme.md @@ -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) @@ -143,6 +144,13 @@ Static JSX ([`Jsx`][jsx], required in production). Development JSX ([`JsxDev`][jsxdev], required in development). +###### `components` + +Components to use ([`Partial`][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`, @@ -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 + | keyof JSX.IntrinsicElements +} + +type Component = + // 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). @@ -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. @@ -463,3 +498,5 @@ abide by its terms. [source]: #source [space]: #space-1 + +[components]: #components-1 diff --git a/test/index.js b/test/index.js index 24baee6..efc97fd 100644 --- a/test/index.js +++ b/test/index.js @@ -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' @@ -255,6 +256,7 @@ test('properties', () => { 'should support properties in the SVG space' ) }) + test('children', () => { assert.equal( renderToStaticMarkup(toJsxRuntime(h('a'), production)), @@ -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' + ) +}) diff --git a/tsconfig.json b/tsconfig.json index 1bc9e99..bef0069 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["**/**.js"], + "include": ["**/**.js", "lib/components.d.ts"], "exclude": ["coverage/", "node_modules/"], "compilerOptions": { "checkJs": true,