diff --git a/packages/rax/jsx-dev-runtime.js b/packages/rax/jsx-dev-runtime.js new file mode 100644 index 0000000000..ddee318bbc --- /dev/null +++ b/packages/rax/jsx-dev-runtime.js @@ -0,0 +1 @@ +export { Fragment, jsxDEV } from './index'; diff --git a/packages/rax/jsx-runtime.js b/packages/rax/jsx-runtime.js new file mode 100644 index 0000000000..d0f12c53b9 --- /dev/null +++ b/packages/rax/jsx-runtime.js @@ -0,0 +1 @@ +export { Fragment, jsx, jsxs, jsxDEV } from './index'; diff --git a/packages/rax/package.json b/packages/rax/package.json index 7d41a505c6..a872d536d1 100644 --- a/packages/rax/package.json +++ b/packages/rax/package.json @@ -4,6 +4,11 @@ "description": "A universal React-compatible render engine.", "license": "BSD-3-Clause", "main": "index.js", + "exports": { + ".": "./index.js", + "./jsx-runtime": "./jsx-runtime.js", + "./jsx-dev-runtime": "./jsx-dev-runtime.js" + }, "repository": { "type": "git", "url": "git+https://github.com/alibaba/rax.git" diff --git a/packages/rax/src/__tests__/jsx-runtime.test.js b/packages/rax/src/__tests__/jsx-runtime.test.js new file mode 100644 index 0000000000..6ffd38340a --- /dev/null +++ b/packages/rax/src/__tests__/jsx-runtime.test.js @@ -0,0 +1,92 @@ +import Component from '../vdom/component'; +import createElement from '../createElement'; +import createRef from '../createRef'; +import { jsx, jsxs, jsxDEV } from '../jsx-runtime'; +import Fragment from '../fragment'; + +describe('Support JSX-Runtime', () => { + it('should export all modules needed by importSource', () => { + expect(typeof jsx).toBe('function'); + expect(typeof jsxs).toBe('function'); + expect(typeof jsxDEV).toBe('function'); + expect(typeof Fragment).toBe('function'); + }); + + it('should add key', () => { + const vnode = jsx('div', null, 'foo'); + expect(vnode.key).toBe('foo'); + }); + + it('should keep ref in props', () => { + let ref = () => null; + let vnode = jsx('div', { ref }); + expect(vnode.ref).toBe(ref); + + ref = 'fooRef'; + vnode = jsx('div', { ref }); + expect(vnode.ref).toBe('fooRef'); + }); + + it('should apply defaultProps', () => { + class Foo extends Component { + render() { + return
; + } + } + + Foo.defaultProps = { + foo: 'bar', + fake: 'bar1', + }; + + let vnode = jsx(Foo, {}, null); + expect(vnode.props.foo).toBe('bar'); + + vnode = jsx(Foo, {fake: 'bar2'}, null); + expect(vnode.props.fake).toBe('bar2'); + }); + + it('should keep props over defaultProps', () => { + class Foo extends Component { + render() { + return
; + } + } + + Foo.defaultProps = { + foo: 'bar' + }; + + const vnode = jsx(Foo, { foo: 'baz' }, null); + expect(vnode.props).toEqual({ + foo: 'baz' + }); + }); + + it('should set __source and __self', () => { + const vnode = jsxDEV('div', { class: 'foo' }, 'key', 'source', 'self'); + expect(vnode.__source).toBe('source'); + expect(vnode.__self).toBe('self'); + }); + + it('should return a vnode like createElement', () => { + const elementVNode = createElement('div', { + class: 'foo', + key: 'key' + }); + const jsxVNode = jsx('div', { class: 'foo' }, 'key'); + expect(jsxVNode).toEqual(elementVNode); + }); + + it('should remove ref from props', () => { + const ref = createRef(); + const vnode = jsx('div', { ref }, null); + expect(vnode.props).toEqual({}); + expect(vnode.ref).toBe(ref); + }); + + it('should receive children props', () => { + const fooProps = {children: []}; + expect(Fragment(fooProps)).toEqual([]); + }); +}); diff --git a/packages/rax/src/index.js b/packages/rax/src/index.js index e6ba641f08..728eb41b22 100644 --- a/packages/rax/src/index.js +++ b/packages/rax/src/index.js @@ -7,6 +7,7 @@ export memo from './memo'; export Fragment from './fragment'; export render from './render'; export Component, { PureComponent } from './vdom/component'; +export { jsx, jsxs, jsxDEV } from './jsx-runtime'; export version from './version'; import Host from './vdom/host'; diff --git a/packages/rax/src/jsx-runtime.js b/packages/rax/src/jsx-runtime.js new file mode 100644 index 0000000000..431075cb72 --- /dev/null +++ b/packages/rax/src/jsx-runtime.js @@ -0,0 +1,19 @@ +import { jsxWithValidation, jsxWithValidationDynamic, jsxWithValidationStatic } from './vdom/jsxValidator'; +import { jsxRuntime as jsxProd } from './vdom/jsx'; + +/** + * This module is adapted to react's jsx-runtime module. + * @seeļ¼š[introducing-the-new-jsx-transform](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.htm) + */ +const __DEV__ = process.env.NODE_ENV !== 'production'; + +const jsx = __DEV__ ? jsxWithValidationDynamic : jsxProd; +const jsxs = __DEV__ ? jsxWithValidationStatic : jsxProd; +const jsxDEV = __DEV__ ? jsxWithValidation : undefined; + +export { + jsx, + jsxs, + jsxDEV +}; + diff --git a/packages/rax/src/vdom/element.js b/packages/rax/src/vdom/element.js index 056de2f930..8355f327b3 100644 --- a/packages/rax/src/vdom/element.js +++ b/packages/rax/src/vdom/element.js @@ -1,6 +1,15 @@ import checkPropTypes from 'prop-types/checkPropTypes'; -export default function Element(type, key, ref, props, owner) { +function setBuiltInPropertyDescriptor(element, propertyName, value) { + Object.defineProperty(element, propertyName, { + configurable: false, + enumerable: false, + writable: true, + value: value + }); +} + +export default function Element(type, key, ref, props, owner, __source, __self) { let element = { // Built-in properties that belong on the element type, @@ -26,17 +35,14 @@ export default function Element(type, key, ref, props, owner) { } // We make validation flag non-enumerable, so the test framework could ignore it - Object.defineProperty(element, '__validated', { - configurable: false, - enumerable: false, - writable: true, - value: false - }); + setBuiltInPropertyDescriptor(element, '__validated', false); + setBuiltInPropertyDescriptor(element, '__source', __source); + setBuiltInPropertyDescriptor(element, '__self', __self); + } - // Props is immutable - if (Object.freeze) { - Object.freeze(props); - } + // Props is immutable + if (Object.freeze) { + Object.freeze(props); } return element; diff --git a/packages/rax/src/vdom/jsx.js b/packages/rax/src/vdom/jsx.js new file mode 100644 index 0000000000..2afcc8d64f --- /dev/null +++ b/packages/rax/src/vdom/jsx.js @@ -0,0 +1,36 @@ +import Host from './host'; +import Element from './element'; + +export function jsxRuntime(type, props, key, __source, __self) { + let normalizedProps = {}; + let propName; + + // The default value of key and ref of rax element is null + let ref = props && props.ref || null; + key = key || null; + + for (propName in props) { + if (propName !== 'ref') { + normalizedProps[propName] = props[propName]; + } + } + + let defaults; + if (typeof type === 'function' && (defaults = type.defaultProps)) { + for (propName in defaults) + if (normalizedProps[propName] === undefined) { + normalizedProps[propName] = defaults[propName]; + } + } + + return new Element( + type, + key, + ref, + normalizedProps, + Host.owner, + __source, + __self + ); +} + diff --git a/packages/rax/src/vdom/jsxValidator.js b/packages/rax/src/vdom/jsxValidator.js new file mode 100644 index 0000000000..113e8c3ddb --- /dev/null +++ b/packages/rax/src/vdom/jsxValidator.js @@ -0,0 +1,56 @@ +import { throwError, throwMinifiedWarn, warning } from '../error'; +import { isArray, NOOP } from '../types'; +import validateChildKeys from '../validateChildKeys'; +import { jsxRuntime } from './jsx'; + +const __DEV__ = process.env.NODE_ENV !== 'production'; + +function _jsxWithValidation(type, props, key, isStaticChildren, __source, __self) { + const elem = jsxRuntime(type, props, key, __source, __self); + + if (type == null) { + if (__DEV__) { + throwError(`Invalid element type, expected a string or a class/function component but got "${type}".`); + } else { + // A empty component replaced avoid break render in production + type = NOOP; + throwMinifiedWarn(0); + } + } + + const children = props && props.children; + if (children !== undefined) { + if (isStaticChildren) { + if (isArray(children)) { + for (let i = 0; i < children.length; i++) { + validateChildKeys(children[i], type); + } + + if (Object.freeze) { + Object.freeze(children); + } + } else { + warning( + 'Rax.jsx: Static children should always be an array. ' + + 'You are likely explicitly calling Rax.jsxs or Rax.jsxDEV. ' + + 'Use the Babel transform instead.', + ); + } + } else { + validateChildKeys(children, type); + } + } + + return elem; +} + + +export function jsxWithValidationDynamic(type, props, key) { + return _jsxWithValidation(type, props, key, false); +} +export function jsxWithValidationStatic(type, props, key) { + return _jsxWithValidation(type, props, key, true); +} +export function jsxWithValidation(type, props, key, __source, __self) { + return _jsxWithValidation(type, props, key, true, __source, __self); +}