mirror of https://github.com/alibaba/ice.git
Refactor/rax compat (#6493)
* refactor: rax compat 1. update @swc/helpers version to latest. 2. relplace create react class with simple impl. * test: add specs for createReactClass * chore: rename to createReactClass
This commit is contained in:
parent
42718fd65d
commit
b70bba1efd
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
'rax-compat': patch
|
||||
---
|
||||
|
||||
1. update @swc/helpers version to latest.
|
||||
2. relplace create react class with simple impl.
|
||||
|
|
@ -48,16 +48,16 @@
|
|||
"compat"
|
||||
],
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.4.3",
|
||||
"style-unit": "^3.0.5",
|
||||
"create-react-class": "^15.7.0",
|
||||
"@ice/appear": "^0.2.0"
|
||||
"@ice/appear": "^0.2.0",
|
||||
"@swc/helpers": "^0.5.1",
|
||||
"style-unit": "^3.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ice/pkg": "^1.5.0",
|
||||
"@types/rax": "^1.0.8",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,129 @@
|
|||
import createReactClass from 'create-react-class';
|
||||
|
||||
// See https://github.com/alibaba/rax/blob/master/packages/rax-create-class/src/index.js
|
||||
// Imported by 'rax-compat/createReactClass'
|
||||
export default createReactClass;
|
||||
import type { ComponentSpec, ClassicComponentClass } from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
const AUTOBIND_BLACKLIST = {
|
||||
render: 1,
|
||||
shouldComponentUpdate: 1,
|
||||
componentWillReceiveProps: 1,
|
||||
componentWillUpdate: 1,
|
||||
componentDidUpdate: 1,
|
||||
componentWillMount: 1,
|
||||
componentDidMount: 1,
|
||||
componentWillUnmount: 1,
|
||||
componentDidUnmount: 1,
|
||||
};
|
||||
|
||||
function collateMixins(mixins: any) {
|
||||
let keyed: Record<string, any> = {};
|
||||
|
||||
for (let i = 0; i < mixins.length; i++) {
|
||||
let mixin = mixins[i];
|
||||
if (mixin.mixins) {
|
||||
applyMixins(mixin, collateMixins(mixin.mixins));
|
||||
}
|
||||
|
||||
for (let key in mixin) {
|
||||
if (mixin.hasOwnProperty(key) && key !== 'mixins') {
|
||||
(keyed[key] || (keyed[key] = [])).push(mixin[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keyed;
|
||||
}
|
||||
|
||||
function flattenHooks(key: string, hooks: Array<any>) {
|
||||
let hookType = typeof hooks[0];
|
||||
// Consider "null" value.
|
||||
if (hooks[0] && hookType === 'object') {
|
||||
// Merge objects in hooks
|
||||
hooks.unshift({});
|
||||
return Object.assign.apply(null, hooks);
|
||||
} else if (hookType === 'function' && (key === 'getInitialState' || key === 'getDefaultProps' || key === 'getChildContext')) {
|
||||
return function () {
|
||||
let ret;
|
||||
for (let i = 0; i < hooks.length; i++) {
|
||||
// @ts-ignore
|
||||
let r = hooks[i].apply(this, arguments);
|
||||
if (r) {
|
||||
if (!ret) ret = {};
|
||||
Object.assign(ret, r);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
} else {
|
||||
return hooks[0];
|
||||
}
|
||||
}
|
||||
function applyMixins(proto: any, mixins: Record<string, any>) {
|
||||
for (let key in mixins) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (mixins.hasOwnProperty(key)) {
|
||||
proto[key] = flattenHooks(key, mixins[key].concat(proto[key] || []));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createReactClass<P, S = {}>(spec: ComponentSpec<P, S>): ClassicComponentClass<P> {
|
||||
class ReactClass extends Component<P, S> {
|
||||
constructor(props: P, context: any) {
|
||||
super(props, context);
|
||||
|
||||
for (let methodName in this) {
|
||||
let method = this[methodName];
|
||||
// @ts-ignore
|
||||
if (typeof method === 'function' && !AUTOBIND_BLACKLIST[methodName]) {
|
||||
this[methodName] = method.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
if (spec.getInitialState) {
|
||||
this.state = spec.getInitialState.call(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (spec.mixins) {
|
||||
applyMixins(spec, collateMixins(spec.mixins));
|
||||
}
|
||||
|
||||
// Not to pass contextTypes to prototype.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { propTypes, contextTypes, ...others } = spec;
|
||||
Object.assign(ReactClass.prototype, others);
|
||||
|
||||
if (spec.statics) {
|
||||
Object.assign(ReactClass, spec.statics);
|
||||
}
|
||||
|
||||
if (spec.propTypes) {
|
||||
// @ts-ignore
|
||||
ReactClass.propTypes = spec.propTypes;
|
||||
}
|
||||
|
||||
if (spec.getDefaultProps) {
|
||||
// @ts-ignore
|
||||
ReactClass.defaultProps = spec.getDefaultProps();
|
||||
}
|
||||
|
||||
if (spec.contextTypes) {
|
||||
// @ts-ignore
|
||||
ReactClass.contextTypes = spec.contextTypes;
|
||||
}
|
||||
|
||||
if (spec.childContextTypes) {
|
||||
// @ts-ignore
|
||||
ReactClass.childContextTypes = spec.childContextTypes;
|
||||
}
|
||||
|
||||
if (spec.displayName) {
|
||||
// @ts-ignore
|
||||
ReactClass.displayName = spec.displayName;
|
||||
}
|
||||
|
||||
return ReactClass as ClassicComponentClass<P>;
|
||||
}
|
||||
export default createReactClass;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@
|
|||
*/
|
||||
|
||||
import { expect, it, describe } from 'vitest';
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from '../src/create-class';
|
||||
|
||||
describe('createReactClass', () => {
|
||||
it('basic', () => {
|
||||
it('the simplest usage', () => {
|
||||
const ReactClass = createReactClass({
|
||||
name: '',
|
||||
id: '',
|
||||
|
|
@ -17,9 +18,310 @@ describe('createReactClass', () => {
|
|||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
const wrapper = render(<ReactClass id="reactClassId" name="raxCompat" />);
|
||||
let res = wrapper.getAllByTestId('reactClassId');
|
||||
|
||||
expect(res.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should copy `displayName` onto the Constructor', () => {
|
||||
const TestComponent = createReactClass({
|
||||
displayName: 'TestComponent',
|
||||
render: function () {
|
||||
return <div />;
|
||||
},
|
||||
});
|
||||
|
||||
expect(TestComponent.displayName).toBe('TestComponent');
|
||||
});
|
||||
|
||||
it('should support statics', () => {
|
||||
const Component = createReactClass({
|
||||
statics: {
|
||||
abc: 'def',
|
||||
def: 0,
|
||||
ghi: null,
|
||||
jkl: 'mno',
|
||||
pqr: function () {
|
||||
return this;
|
||||
},
|
||||
},
|
||||
|
||||
render: function () {
|
||||
return <span />;
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
expect(Component.abc).toBe('def');
|
||||
// @ts-ignore
|
||||
expect(Component.def).toBe(0);
|
||||
// @ts-ignore
|
||||
expect(Component.ghi).toBe(null);
|
||||
// @ts-ignore
|
||||
expect(Component.jkl).toBe('mno');
|
||||
// @ts-ignore
|
||||
expect(Component.pqr()).toBe(Component);
|
||||
});
|
||||
|
||||
it('should work with object getInitialState() return values', () => {
|
||||
const Component = createReactClass({
|
||||
getInitialState: function () {
|
||||
return {
|
||||
occupation: 'clown',
|
||||
};
|
||||
},
|
||||
render: function () {
|
||||
return <span data-testid="testerClown">{this.state.occupation}</span>;
|
||||
},
|
||||
});
|
||||
|
||||
const instance = render(<Component />);
|
||||
const el = instance.getByTestId('testerClown');
|
||||
expect(el.innerHTML).toEqual('clown');
|
||||
});
|
||||
|
||||
it('renders based on context getInitialState', () => {
|
||||
const Foo = createReactClass({
|
||||
contextTypes: {
|
||||
className: PropTypes.string,
|
||||
},
|
||||
getInitialState() {
|
||||
return { className: this.context.className };
|
||||
},
|
||||
render() {
|
||||
return <span className={this.state.className} data-testid="testerFoo" />;
|
||||
},
|
||||
});
|
||||
|
||||
const Outer = createReactClass({
|
||||
childContextTypes: {
|
||||
className: PropTypes.string,
|
||||
},
|
||||
getChildContext() {
|
||||
return { className: 'foo' };
|
||||
},
|
||||
render() {
|
||||
return <Foo />;
|
||||
},
|
||||
});
|
||||
|
||||
const instance = render(<Outer />);
|
||||
const el = instance.getByTestId('testerFoo');
|
||||
expect(el.className).toEqual('foo');
|
||||
});
|
||||
|
||||
it('should support statics in mixins', () => {
|
||||
const Mixin = {
|
||||
statics: {
|
||||
foo: 'bar',
|
||||
},
|
||||
};
|
||||
const Component = createReactClass({
|
||||
mixins: [Mixin],
|
||||
|
||||
statics: {
|
||||
abc: 'def',
|
||||
},
|
||||
|
||||
render: function () {
|
||||
return <span />;
|
||||
},
|
||||
});
|
||||
|
||||
render(<Component />);
|
||||
|
||||
expect(Component.foo).toBe('bar');
|
||||
expect(Component.abc).toBe('def');
|
||||
});
|
||||
|
||||
it('should include the mixin keys in even if their values are falsy', () => {
|
||||
const mixin = {
|
||||
keyWithNullValue: null,
|
||||
randomCounter: 0,
|
||||
};
|
||||
|
||||
const Component = createReactClass({
|
||||
mixins: [mixin],
|
||||
componentDidMount: function () {
|
||||
expect(this.randomCounter).toBe(0);
|
||||
expect(this.keyWithNullValue).toBeNull();
|
||||
},
|
||||
render: function () {
|
||||
return <span />;
|
||||
},
|
||||
});
|
||||
|
||||
render(<Component />);
|
||||
});
|
||||
|
||||
it('should work with a null getInitialState return value and a mixin', () => {
|
||||
let Component;
|
||||
|
||||
let currentState;
|
||||
|
||||
const Mixin = {
|
||||
getInitialState: function () {
|
||||
return { foo: 'bar' };
|
||||
},
|
||||
};
|
||||
Component = createReactClass({
|
||||
mixins: [Mixin],
|
||||
getInitialState: function () {
|
||||
return null;
|
||||
},
|
||||
render: function () {
|
||||
currentState = this.state;
|
||||
return <span />;
|
||||
},
|
||||
});
|
||||
|
||||
render(<Component />);
|
||||
expect(currentState).toEqual({ foo: 'bar' });
|
||||
currentState = null;
|
||||
|
||||
// Also the other way round should work
|
||||
const Mixin2 = {
|
||||
getInitialState: function () {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
Component = createReactClass({
|
||||
mixins: [Mixin2],
|
||||
getInitialState: function () {
|
||||
return { foo: 'bar' };
|
||||
},
|
||||
render: function () {
|
||||
currentState = this.state;
|
||||
return <span />;
|
||||
},
|
||||
});
|
||||
|
||||
render(<Component />);
|
||||
expect(currentState).toEqual({ foo: 'bar' });
|
||||
currentState = null;
|
||||
|
||||
// Multiple mixins should be fine too
|
||||
Component = createReactClass({
|
||||
mixins: [Mixin, Mixin2],
|
||||
getInitialState: function () {
|
||||
return { x: true };
|
||||
},
|
||||
render: function () {
|
||||
currentState = this.state;
|
||||
return <span />;
|
||||
},
|
||||
});
|
||||
|
||||
render(<Component />);
|
||||
expect(currentState).toEqual({ foo: 'bar', x: true });
|
||||
currentState = null;
|
||||
});
|
||||
|
||||
it('should have bound the mixin methods to the component', () => {
|
||||
const mixin = {
|
||||
mixinFunc: function () {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
|
||||
const Component = createReactClass({
|
||||
mixins: [mixin],
|
||||
componentDidMount: function () {
|
||||
expect(this.mixinFunc()).toBe(this);
|
||||
},
|
||||
render: function () {
|
||||
return <span />;
|
||||
},
|
||||
});
|
||||
|
||||
render(<Component />);
|
||||
});
|
||||
|
||||
it('should support mixins with getInitialState()', () => {
|
||||
let currentState;
|
||||
const Mixin = {
|
||||
getInitialState: function () {
|
||||
return { mixin: true };
|
||||
},
|
||||
};
|
||||
const Component = createReactClass({
|
||||
mixins: [Mixin],
|
||||
getInitialState: function () {
|
||||
return { component: true };
|
||||
},
|
||||
render: function () {
|
||||
currentState = this.state;
|
||||
return <span />;
|
||||
},
|
||||
});
|
||||
render(<Component />);
|
||||
expect(currentState.component).toBeTruthy();
|
||||
expect(currentState.mixin).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should support merging propTypes and statics', () => {
|
||||
const MixinA = {
|
||||
propTypes: {
|
||||
propA: function () {},
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.props.listener('MixinA didMount');
|
||||
},
|
||||
};
|
||||
|
||||
const MixinB = {
|
||||
mixins: [MixinA],
|
||||
propTypes: {
|
||||
propB: function () {},
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.props.listener('MixinB didMount');
|
||||
},
|
||||
};
|
||||
|
||||
const MixinC = {
|
||||
statics: {
|
||||
staticC: function () {},
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.props.listener('MixinC didMount');
|
||||
},
|
||||
};
|
||||
|
||||
const MixinD = {
|
||||
propTypes: {
|
||||
value: PropTypes.string,
|
||||
},
|
||||
};
|
||||
|
||||
const Component = createReactClass({
|
||||
mixins: [MixinB, MixinC, MixinD],
|
||||
statics: {
|
||||
staticComponent: function () {},
|
||||
},
|
||||
propTypes: {
|
||||
propComponent: function () {},
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.props.listener('Component didMount');
|
||||
},
|
||||
render: function () {
|
||||
return <div />;
|
||||
},
|
||||
});
|
||||
|
||||
const listener = function () {};
|
||||
render(<Component listener={listener} />);
|
||||
const instancePropTypes = Component.propTypes;
|
||||
|
||||
expect('propA' in instancePropTypes).toBeTruthy();
|
||||
expect('propB' in instancePropTypes).toBeTruthy();
|
||||
expect('propComponent' in instancePropTypes).toBeTruthy();
|
||||
|
||||
expect('staticC' in Component).toBeTruthy();
|
||||
expect('staticComponent' in Component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1541,24 +1541,24 @@ importers:
|
|||
specifiers:
|
||||
'@ice/appear': ^0.2.0
|
||||
'@ice/pkg': ^1.5.0
|
||||
'@swc/helpers': ^0.4.3
|
||||
'@swc/helpers': ^0.5.1
|
||||
'@types/rax': ^1.0.8
|
||||
'@types/react': ^18.0.0
|
||||
'@types/react-dom': ^18.0.0
|
||||
create-react-class: ^15.7.0
|
||||
prop-types: ^15.8.1
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
style-unit: ^3.0.5
|
||||
dependencies:
|
||||
'@ice/appear': link:../appear
|
||||
'@swc/helpers': 0.4.14
|
||||
create-react-class: 15.7.0
|
||||
'@swc/helpers': 0.5.1
|
||||
style-unit: 3.0.5
|
||||
devDependencies:
|
||||
'@ice/pkg': 1.5.5
|
||||
'@types/rax': 1.0.10
|
||||
'@types/react': 18.0.28
|
||||
'@types/react-dom': 18.0.11
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
|
||||
|
|
@ -7652,12 +7652,6 @@ packages:
|
|||
'@swc/core-win32-ia32-msvc': 1.3.80
|
||||
'@swc/core-win32-x64-msvc': 1.3.80
|
||||
|
||||
/@swc/helpers/0.4.14:
|
||||
resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==}
|
||||
dependencies:
|
||||
tslib: 2.5.0
|
||||
dev: false
|
||||
|
||||
/@swc/helpers/0.5.1:
|
||||
resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==}
|
||||
dependencies:
|
||||
|
|
@ -10553,13 +10547,6 @@ packages:
|
|||
path-type: 4.0.0
|
||||
dev: true
|
||||
|
||||
/create-react-class/15.7.0:
|
||||
resolution: {integrity: sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==}
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
object-assign: 4.1.1
|
||||
dev: false
|
||||
|
||||
/create-require/1.1.1:
|
||||
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||
dev: true
|
||||
|
|
|
|||
Loading…
Reference in New Issue