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:
ZeroLing 2023-09-11 10:53:58 +08:00 committed by GitHub
parent 42718fd65d
commit b70bba1efd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 445 additions and 26 deletions

View File

@ -0,0 +1,6 @@
---
'rax-compat': patch
---
1. update @swc/helpers version to latest.
2. relplace create react class with simple impl.

View File

@ -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"
},

View File

@ -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;

View File

@ -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();
});
});

View File

@ -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