Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jsx/dom): improve compatibility with React #2553

Merged
merged 24 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8887fcd
feat(jsx/dom): Match the type with the object returned from React's c…
usualoma Apr 15, 2024
29a8fb3
feat(jsx): add Children utility functions
usualoma Apr 15, 2024
869cbc8
feat(jsx/dom): export Fragment from hono/jsx/dom
usualoma Apr 15, 2024
61c0a5b
feat(jsx): export DOMAttributes type
usualoma Apr 15, 2024
e618c22
feat(jsx): export ReactNode and ReactElement types
usualoma Apr 15, 2024
d1ba481
feat(jsx): export types for compat with react
usualoma Apr 16, 2024
e862c7a
feat(jsx): Add hooks and utils for React compatibility
usualoma Apr 16, 2024
1023dc1
refactor(jsx): use Object.is for state comparison like React
usualoma Apr 16, 2024
7f4833a
feat(jsx/dom): Implement `flushSync` and `createPortal`
usualoma Apr 24, 2024
e8b9e0f
feat(jsx/dom/jsx-dev-runtime): preserve only one children in jsxDEV
usualoma Apr 24, 2024
5e19c54
refactor(jsx/dom): change data structure of NodeString
usualoma Apr 24, 2024
58684a9
feat(jsx/dom): accept nullish value for event handler and ref
usualoma Apr 24, 2024
0fa1406
feat(jsx/dom): enable to set CSS variables
usualoma Apr 24, 2024
343b263
feat(jsx/dom): support defaultProps in functional components
usualoma Apr 24, 2024
df10997
refactor(jsx/dom): improve removing node process
usualoma Apr 24, 2024
0c8ea12
fix(jsx/dom): propagate the context to the children
usualoma Apr 24, 2024
dab2792
perf(jsx/dom): skip snapshotting for internal tags
usualoma Apr 24, 2024
85e6f46
feat(jsx): preserve only one children also in hono/jsx/jsx-dev-runtim…
usualoma Apr 24, 2024
6fb5a9a
feat(jsx): support CSS variable in server-side rendering
usualoma Apr 24, 2024
be6d2b9
feat(jsx): accept null as props in createElement
usualoma Apr 24, 2024
b9b080d
refactor(jsx): refactor internal children handling
usualoma Apr 25, 2024
c100042
chore: denoify
usualoma Apr 24, 2024
4215e02
test(jsx): add tests
usualoma Apr 24, 2024
12e4b8e
refactor(jsx): use `toArray` unitility function in render.ts
usualoma Apr 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 44 additions & 29 deletions deno_dist/jsx/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import type { StringBuffer, HtmlEscaped, HtmlEscapedString } from '../utils/html
import type { Context } from './context.ts'
import { globalContexts } from './context.ts'
import type { IntrinsicElements as IntrinsicElementsDefined } from './intrinsic-elements.ts'
import { normalizeIntrinsicElementProps } from './utils.ts'
import { normalizeIntrinsicElementProps, styleObjectForEach } from './utils.ts'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Props = Record<string, any>
export type FC<T = Props> = (props: T) => HtmlEscapedString | Promise<HtmlEscapedString>
export type FC<P = Props> = {
(props: P): HtmlEscapedString | Promise<HtmlEscapedString>
defaultProps?: Partial<P> | undefined
displayName?: string | undefined
}
export type DOMAttributes = Hono.HTMLAttributes

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
Expand Down Expand Up @@ -114,6 +119,16 @@ export class JSXNode implements HtmlEscaped {
this.children = children
}

get type(): string | Function {
return this.tag as string
}

// Added for compatibility with libraries that rely on React's internal structure
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get ref(): any {
return this.props.ref || null
}

toString(): string | Promise<string> {
const buffer: StringBuffer = ['']
this.localContexts?.forEach(([context, value]) => {
Expand Down Expand Up @@ -141,16 +156,18 @@ export class JSXNode implements HtmlEscaped {
for (let i = 0, len = propsKeys.length; i < len; i++) {
const key = propsKeys[i]
const v = props[key]
// object to style strings
if (key === 'style' && typeof v === 'object') {
const styles = Object.keys(v)
.map((k) => {
const property = k.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)
return `${property}:${v[k]}`
})
.join(';')
if (key === 'children') {
// skip children
} else if (key === 'style' && typeof v === 'object') {
// object to style strings
let styleStr = ''
styleObjectForEach(v, (property, value) => {
if (value != null) {
styleStr += `${styleStr ? ';' : ''}${property}:${value}`
}
})
buffer[0] += ' style="'
escapeToBuffer(styles, buffer)
escapeToBuffer(styleStr, buffer)
buffer[0] += '"'
} else if (typeof v === 'string') {
buffer[0] += ` ${key}="`
Expand Down Expand Up @@ -241,14 +258,17 @@ export class JSXFragmentNode extends JSXNode {

export const jsx = (
tag: string | Function,
props: Props,
...children: (string | HtmlEscapedString)[]
props: Props | null,
...children: (string | number | HtmlEscapedString)[]
): JSXNode => {
let key
if (props) {
key = props?.key
delete props['key']
props ??= {}
if (children.length) {
props.children = children.length === 1 ? children[0] : children
}

const key = props.key
delete props['key']

const node = jsxFn(tag, props, children)
node.key = key
return node
Expand All @@ -257,7 +277,7 @@ export const jsx = (
export const jsxFn = (
tag: string | Function,
props: Props,
children: (string | HtmlEscapedString)[]
children: (string | number | HtmlEscapedString)[]
): JSXNode => {
if (typeof tag === 'function') {
return new JSXFunctionNode(tag, props, children)
Expand Down Expand Up @@ -317,30 +337,25 @@ export const Fragment = ({
}): HtmlEscapedString => {
return new JSXFragmentNode(
'',
{},
{
children,
},
Array.isArray(children) ? children : children ? [children] : []
) as never
}

export const isValidElement = (element: unknown): element is JSXNode => {
return !!(
element &&
typeof element === 'object' &&
'tag' in element &&
'props' in element &&
'children' in element
)
return !!(element && typeof element === 'object' && 'tag' in element && 'props' in element)
}

export const cloneElement = <T extends JSXNode | JSX.Element>(
element: T,
props: Partial<Props>,
...children: Child[]
): T => {
return jsxFn(
return jsx(
(element as JSXNode).tag,
{ ...(element as JSXNode).props, ...props },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
children.length ? children : ((element as JSXNode).children as any) || []
...(children as (string | number | HtmlEscapedString)[])
) as T
}
20 changes: 20 additions & 0 deletions deno_dist/jsx/children.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Child } from './base.ts'

export const toArray = (children: Child): Child[] =>
Array.isArray(children) ? children : [children]
export const Children = {
map: (children: Child[], fn: (child: Child, index: number) => Child): Child[] =>
toArray(children).map(fn),
forEach: (children: Child[], fn: (child: Child, index: number) => void): void => {
toArray(children).forEach(fn)
},
count: (children: Child[]): number => toArray(children).length,
only: (_children: Child[]): Child => {
const children = toArray(_children)
if (children.length !== 1) {
throw new Error('Children.only() expects only one child')
}
return children[0]
},
toArray,
}
1 change: 1 addition & 0 deletions deno_dist/jsx/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const DOM_RENDERER = Symbol('RENDERER')
export const DOM_ERROR_HANDLER = Symbol('ERROR_HANDLER')
export const DOM_STASH = Symbol('STASH')
export const DOM_INTERNAL_TAG = Symbol('INTERNAL')
38 changes: 25 additions & 13 deletions deno_dist/jsx/dom/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,44 @@ import { DOM_ERROR_HANDLER } from '../constants.ts'
import type { Context } from '../context.ts'
import { globalContexts } from '../context.ts'
import { Fragment } from './jsx-runtime.ts'
import { setInternalTagFlag } from './utils.ts'

export const createContextProviderFunction =
<T>(values: T[]) =>
({ value, children }: { value: T; children: Child[] }) => {
const res = Fragment({
export const createContextProviderFunction = <T>(values: T[]) =>
setInternalTagFlag(({ value, children }: { value: T; children: Child[] }) => {
if (!children) {
return undefined
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const props: { children: any } = {
children: [
{
tag: () => {
tag: setInternalTagFlag(() => {
values.push(value)
},
},
...(children as Child[]),
{
tag: () => {
values.pop()
},
}),
props: {},
},
],
}
if (Array.isArray(children)) {
props.children.push(...children.flat())
} else {
props.children.push(children)
}
props.children.push({
tag: setInternalTagFlag(() => {
values.pop()
}),
props: {},
})
const res = Fragment(props)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(res as any)[DOM_ERROR_HANDLER] = (err: unknown) => {
values.pop()
throw err
}
return res
}
})

export const createContext = <T>(defaultValue: T): Context<T> => {
const values = [defaultValue]
Expand Down
12 changes: 8 additions & 4 deletions deno_dist/jsx/dom/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,14 @@ export const createCssJsxDomObjects = ({ id }: { id: Readonly<string> }) => {
const Style: FC<PropsWithChildren<void>> = ({ children }) =>
({
tag: 'style',
children: (Array.isArray(children) ? children : [children]).map(
(c) => (c as unknown as CssClassName)[STYLE_STRING]
),
props: { id },
props: {
id,
children:
children &&
(Array.isArray(children) ? children : [children]).map(
(c) => (c as unknown as CssClassName)[STYLE_STRING]
),
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)

Expand Down
41 changes: 36 additions & 5 deletions deno_dist/jsx/dom/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Props, Child, JSXNode } from '../base.ts'
import type { Props, Child, DOMAttributes, JSXNode } from '../base.ts'
import { memo, isValidElement } from '../base.ts'
import { Children } from '../children.ts'
import { useContext } from '../context.ts'
import {
useState,
Expand All @@ -17,19 +18,28 @@ import {
useReducer,
useId,
useDebugValue,
createRef,
forwardRef,
useImperativeHandle,
useSyncExternalStore,
} from '../hooks/index.ts'
import { Suspense, ErrorBoundary } from './components.ts'
import { createContext } from './context.ts'
import { jsx } from './jsx-runtime.ts'
import { jsx, Fragment } from './jsx-runtime.ts'
import { flushSync, createPortal } from './render.ts'

export { render } from './render.ts'

const createElement = (
tag: string | ((props: Props) => JSXNode),
props: Props,
props: Props | null,
...children: Child[]
): JSXNode => {
const jsxProps: Props = { ...props, children }
const jsxProps: Props = props ? { ...props } : {}
if (children.length) {
jsxProps.children = children.length === 1 ? children[0] : children
}

let key = undefined
if ('key' in jsxProps) {
key = jsxProps.key
Expand All @@ -49,7 +59,7 @@ const cloneElement = <T extends JSXNode | JSX.Element>(
{
...(element as JSXNode).props,
...props,
children: children.length ? children : (element as JSXNode).children,
children: children.length ? children : (element as JSXNode).props.children,
},
(element as JSXNode).key
) as T
Expand All @@ -72,6 +82,10 @@ export {
useReducer,
useId,
useDebugValue,
createRef,
forwardRef,
useImperativeHandle,
useSyncExternalStore,
Suspense,
ErrorBoundary,
createContext,
Expand All @@ -80,6 +94,11 @@ export {
isValidElement,
createElement,
cloneElement,
Children,
Fragment,
DOMAttributes,
flushSync,
createPortal,
}

export default {
Expand All @@ -98,6 +117,10 @@ export default {
useReducer,
useId,
useDebugValue,
createRef,
forwardRef,
useImperativeHandle,
useSyncExternalStore,
Suspense,
ErrorBoundary,
createContext,
Expand All @@ -106,6 +129,14 @@ export default {
isValidElement,
createElement,
cloneElement,
Children,
Fragment,
flushSync,
createPortal,
}

export type { Context } from '../context.ts'

// TODO: change to `export type *` after denoify bug is fixed
// https://github.com/garronej/denoify/issues/124
export * from '../types.ts'
38 changes: 23 additions & 15 deletions deno_dist/jsx/dom/jsx-dev-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import type { Props } from '../base.ts'
import type { Props, JSXNode } from '../base.ts'
import { normalizeIntrinsicElementProps } from '../utils.ts'

export const jsxDEV = (tag: string | Function, props: Props, key?: string) => {
const JSXNodeCompatPrototype = {
type: {
get(this: { tag: string | Function }): string | Function {
return this.tag
},
},
ref: {
get(this: { props?: { ref: unknown } }): unknown {
return this.props?.ref
},
},
}

export const jsxDEV = (tag: string | Function, props: Props, key?: string): JSXNode => {
if (typeof tag === 'string') {
normalizeIntrinsicElementProps(props)
}
let children
if (props && 'children' in props) {
children = props.children
delete props['children']
} else {
children = []
}
return {
tag,
props,
key,
children: Array.isArray(children) ? children : [children],
}
return Object.defineProperties(
{
tag,
props,
key,
},
JSXNodeCompatPrototype
) as JSXNode
}

export const Fragment = (props: Record<string, unknown>) => jsxDEV('', props, undefined)
Loading