",
16 | "license": "MIT",
17 | "homepage": "https://github.com/preactjs/legacy-compat",
18 | "files": [
19 | "dist"
20 | ],
21 | "peerDependencies": {
22 | "preact": ">=10.26.0 < 11"
23 | },
24 | "dependencies": {
25 | "prop-types": "^15.7.2"
26 | },
27 | "devDependencies": {
28 | "@babel/plugin-transform-react-jsx": "^7.10.4",
29 | "@types/jest": "^26.0.13",
30 | "eslint": "^7.8.1",
31 | "eslint-config-developit": "^1.2.0",
32 | "eslint-config-prettier": "^6.11.0",
33 | "immutable": "^4.0.0-rc.12",
34 | "jest": "^26.4.2",
35 | "microbundle": "^0.13.0",
36 | "npm-merge-driver-install": "^1.1.1",
37 | "preact": "^10.26.0",
38 | "prettier": "^2.1.1"
39 | },
40 | "babel": {
41 | "env": {
42 | "test": {
43 | "presets": [
44 | [
45 | "@babel/preset-env",
46 | {
47 | "targets": {
48 | "node": "current"
49 | }
50 | }
51 | ]
52 | ],
53 | "plugins": [
54 | "@babel/plugin-transform-react-jsx"
55 | ]
56 | }
57 | }
58 | },
59 | "eslintConfig": {
60 | "extends": [
61 | "developit",
62 | "prettier"
63 | ],
64 | "settings": {
65 | "react": {
66 | "pragma": "createElement"
67 | }
68 | },
69 | "rules": {
70 | "camelcase": [
71 | 1,
72 | {
73 | "allow": [
74 | "__test__*",
75 | "unstable_*",
76 | "UNSAFE_*",
77 | "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED"
78 | ]
79 | }
80 | ],
81 | "prefer-rest-params": 0,
82 | "prefer-spread": 0,
83 | "no-cond-assign": 0,
84 | "no-prototype-builtins": 0,
85 | "no-duplicate-imports": 0,
86 | "react/jsx-no-bind": 0,
87 | "react/no-danger": "off",
88 | "react/prefer-stateless-function": 0
89 | }
90 | },
91 | "eslintIgnore": [
92 | "test/fixtures",
93 | "test/ts/",
94 | "*.ts",
95 | "dist"
96 | ],
97 | "prettier": {
98 | "singleQuote": true,
99 | "trailingComma": "none",
100 | "useTabs": true,
101 | "tabWidth": 2
102 | }
103 | }
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | import { act } from 'preact/test-utils';
2 | import React from '../src';
3 | import ReactDOM from '../src';
4 | import Immutable from 'immutable';
5 |
6 | /* eslint-disable new-cap */
7 |
8 | describe('Component', () => {
9 | let scratch;
10 | beforeEach(() => {
11 | scratch = document.createElement('div');
12 | document.body.appendChild(scratch);
13 | });
14 | afterEach(() => {
15 | scratch.parentNode.removeChild(scratch);
16 | scratch = null;
17 | });
18 |
19 | describe('string refs', () => {
20 | it('should have refs object', () => {
21 | let c;
22 | class Foo extends React.Component {
23 | constructor() {
24 | super();
25 | c = this;
26 | }
27 | render() {
28 | return null;
29 | }
30 | }
31 | ReactDOM.render(, scratch);
32 | expect(c).toBeDefined();
33 | expect(c.refs).toBeInstanceOf(Object);
34 | });
35 |
36 | it('should populate string refs', () => {
37 | let c;
38 | class Foo extends React.Component {
39 | render() {
40 | c = this;
41 | // @ts-ignore-next
42 | return foo;
43 | }
44 | }
45 | ReactDOM.render(, scratch);
46 | expect(c).toBeDefined();
47 | expect(c.refs).toHaveProperty('foo', scratch.firstElementChild);
48 | });
49 | });
50 |
51 | describe('Attribute semantics for width & height props on media elements', () => {
52 | describe.each(['img', 'video', 'canvas'])('%s', (Tag) => {
53 | it('should remove px in width/height', () => {
54 | ReactDOM.render(, scratch);
55 | expect(scratch.firstElementChild.getAttribute('height')).toEqual(
56 | '100px'
57 | );
58 | expect(scratch.firstElementChild.getAttribute('width')).toEqual(
59 | '200px'
60 | );
61 | // Note: can't use width/height properties, since they report incorrect values in JSDOM
62 | //expect(scratch.firstElementChild.height).toEqual(100);
63 | //expect(scratch.firstElementChild.width).toEqual(200);
64 | });
65 |
66 | it('should move % in width/height to style', () => {
67 | ReactDOM.render(, scratch);
68 | expect(scratch.firstElementChild.getAttribute('height')).toEqual('50%');
69 | expect(scratch.firstElementChild.getAttribute('width')).toEqual('100%');
70 | // Note: can't use width/height properties, since they report incorrect values in JSDOM
71 | });
72 | });
73 | });
74 |
75 | describe('getInitialState', () => {
76 | it('should be called during creation', () => {
77 | let c;
78 | const Foo = React.createClass({
79 | getInitialState() {
80 | return { a: 1, b: this.props.b + 1 };
81 | },
82 | render() {
83 | c = this;
84 | return null;
85 | }
86 | });
87 | ReactDOM.render(, scratch);
88 | expect(c).toBeDefined();
89 | expect(c.state).toMatchObject({ a: 1, b: 3 });
90 | });
91 |
92 | it('should allow setState()', async () => {
93 | let c;
94 | const Foo = React.createClass({
95 | getInitialState() {
96 | return { a: 1, b: 2 };
97 | },
98 | render() {
99 | c = this;
100 | return null;
101 | }
102 | });
103 | ReactDOM.render(, scratch);
104 | await act(() => {
105 | c.setState({ a: 2 });
106 | });
107 | expect(c.state).toMatchObject({ a: 2, b: 2 });
108 | await act(() => {
109 | c.setState({ b: 3 });
110 | });
111 | expect(c.state).toMatchObject({ a: 2, b: 3 });
112 | });
113 | });
114 |
115 | describe('Immutable support', () => {
116 | it('should render Immutable.List values as text', () => {
117 | const ref = React.createRef();
118 | ReactDOM.render(
119 | {Immutable.List(['a', 'b', 'c'])}
,
120 | scratch
121 | );
122 | expect(ref.current).toHaveProperty('textContent', 'abc');
123 | });
124 |
125 | it('should render Immutable.List values as elements/components', () => {
126 | const ref = React.createRef();
127 | ReactDOM.render(
128 |
129 | {Immutable.List(['a', 'b', 'c']).map((v) => (
130 | - {v}
131 | ))}
132 |
,
133 | scratch
134 | );
135 | expect(ref.current).toHaveProperty(
136 | 'outerHTML',
137 | ''
138 | );
139 | });
140 |
141 | it('should render Immutable.Map values', () => {
142 | ReactDOM.render(
143 | {Immutable.Map({ a: 'foo', b: 2, c: three })}
,
144 | scratch
145 | );
146 | expect(scratch.innerHTML).toEqual('foo2three
');
147 | });
148 |
149 | it('should render Immutable.List containing component children', () => {
150 | const A = jest.fn().mockReturnValue(A);
151 | const B = jest.fn().mockReturnValue(B);
152 | const C = jest.fn().mockReturnValue(C);
153 | const Root = (props) => props.children;
154 | ReactDOM.render(
155 |
156 | {Immutable.Set([
157 | , , ])} />,
158 | - D
159 | ])}
160 |
,
161 | scratch
162 | );
163 | expect(scratch).toHaveProperty(
164 | 'innerHTML',
165 | ''
166 | );
167 | });
168 | });
169 | });
170 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | useState,
3 | useId,
4 | useReducer,
5 | useEffect,
6 | useLayoutEffect,
7 | useInsertionEffect,
8 | useTransition,
9 | useDeferredValue,
10 | useSyncExternalStore,
11 | startTransition,
12 | useRef,
13 | useImperativeHandle,
14 | useMemo,
15 | useCallback,
16 | useContext,
17 | useDebugValue,
18 | version,
19 | Children,
20 | render,
21 | hydrate,
22 | unmountComponentAtNode,
23 | createPortal,
24 | createElement,
25 | createContext,
26 | createFactory,
27 | cloneElement,
28 | createRef,
29 | Fragment,
30 | isValidElement,
31 | isElement,
32 | isFragment,
33 | isMemo,
34 | findDOMNode,
35 | Component,
36 | PureComponent,
37 | memo,
38 | forwardRef,
39 | flushSync,
40 | unstable_batchedUpdates,
41 | StrictMode,
42 | Suspense,
43 | SuspenseList,
44 | lazy,
45 | __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
46 | } from 'preact/compat';
47 |
48 | import PropTypes from 'prop-types';
49 |
50 | import { options } from 'preact';
51 |
52 | const DEV = process.env.NODE_ENV === 'development';
53 |
54 | function replaceIterables(obj) {
55 | if (typeof obj !== 'object' || obj === null) {
56 | } else if (Array.isArray(obj)) {
57 | for (let i = obj.length; i--; ) {
58 | obj[i] = replaceIterables(obj[i]);
59 | }
60 | } else if (typeof obj.toJS === 'function') {
61 | if ('valueSeq' in obj) obj = obj.valueSeq();
62 | obj = obj.toJS();
63 | }
64 | return obj;
65 | }
66 |
67 | const HOOK_RENDER = '__r'; // _render
68 | const VNODE_COMPONENT = '__c'; // _component
69 | const NEXT_STATE = '__s'; // _nextState
70 | const REF_SETTERS = '__rs';
71 |
72 | const oldRenderHook = options[HOOK_RENDER];
73 | let currentComponent = null;
74 | options[HOOK_RENDER] = (vnode) => {
75 | currentComponent = vnode[VNODE_COMPONENT];
76 | if (!currentComponent[REF_SETTERS]) {
77 | currentComponent[REF_SETTERS] = {};
78 | currentComponent.refs = {};
79 | }
80 | if (oldRenderHook) oldRenderHook(vnode);
81 | };
82 |
83 | const oldVNodeHook = options.vnode;
84 | options.vnode = (vnode) => {
85 | const type = vnode.type;
86 | const props = vnode.props;
87 |
88 | if (typeof vnode.ref === 'string') {
89 | const refSetters = currentComponent[REF_SETTERS];
90 | vnode.ref =
91 | refSetters[name] ||
92 | (refSetters[name] = setRef.bind(currentComponent.refs, vnode.ref));
93 | }
94 |
95 | if (props) {
96 | if (type == 'img' || type == 'canvas' || type == 'video') {
97 | if (props.width && isNaN(props.width)) {
98 | props.Width = props.width;
99 | props.width = undefined;
100 | }
101 | if (props.height && isNaN(props.height)) {
102 | props.Height = props.height;
103 | props.height = undefined;
104 | }
105 | }
106 |
107 | if (DEV) {
108 | if (typeof type === 'function' && type.propTypes) {
109 | PropTypes.checkPropTypes(
110 | type.propTypes,
111 | props,
112 | 'prop',
113 | type.displayName || type.name
114 | );
115 | }
116 | }
117 |
118 | if (props.children != null) {
119 | props.children = replaceIterables(props.children);
120 | }
121 | }
122 | if (oldVNodeHook) oldVNodeHook(vnode);
123 | };
124 |
125 | function setRef(name, value) {
126 | this[name] = value;
127 | }
128 |
129 | function ContextProvider() {}
130 | ContextProvider.prototype.getChildContext = function () {
131 | return this.props.c;
132 | };
133 | ContextProvider.prototype.render = function () {
134 | return this.props.v;
135 | };
136 |
137 | function unstable_renderSubtreeIntoContainer(
138 | parentComponent,
139 | vnode,
140 | container,
141 | callback
142 | ) {
143 | let wrap = createElement(ContextProvider, {
144 | c: parentComponent.context,
145 | v: vnode
146 | });
147 | let renderContainer = render(wrap, container);
148 | let component = renderContainer.__c || renderContainer.base;
149 | if (callback) callback.call(component, renderContainer);
150 | return component;
151 | }
152 |
153 | function assign(obj, props) {
154 | for (let i in props) obj[i] = props[i];
155 | }
156 |
157 | // patch the actual Component prototype (there is no dedicated Compat one anymore)
158 | assign(Component.prototype, {
159 | refs: null,
160 | [REF_SETTERS]: null,
161 | isReactComponent: {},
162 | replaceState(state, callback) {
163 | this.setState({}, callback);
164 | assign((this[NEXT_STATE] = {}), state);
165 | },
166 | getDOMNode() {
167 | return this.base;
168 | },
169 | isMounted() {
170 | return !!this.base;
171 | }
172 | });
173 |
174 | /** @returns {typeof Component} */
175 | function createClass(obj) {
176 | function cl(props, context) {
177 | Component.call(this, props, context);
178 | if (this.getInitialState) {
179 | this.state = this.getInitialState() || {};
180 | }
181 | for (let i in this) {
182 | if (
183 | typeof this[i] === 'function' &&
184 | !/^(constructor$|render$|shouldComponentUpda|component(Did|Will)(Mou|Unmou|Upda|Recei))/.test(
185 | i
186 | )
187 | ) {
188 | this[i] = this[i].bind(this);
189 | }
190 | }
191 | }
192 |
193 | // We need to apply mixins here so that getDefaultProps is correctly mixed
194 | if (obj.mixins) {
195 | applyMixins(obj, collateMixins(obj.mixins));
196 | }
197 | if (obj.statics) assign(cl, obj.statics);
198 | cl.displayName = obj.displayName;
199 | cl.propTypes = obj.propTypes;
200 | cl.defaultProps = obj.getDefaultProps
201 | ? obj.getDefaultProps.call(cl)
202 | : obj.defaultProps;
203 | assign((cl.prototype = new Component()), obj);
204 | cl.prototype.constructor = cl;
205 |
206 | return cl;
207 | }
208 |
209 | // Flatten an Array of mixins to a map of method name to mixin implementations
210 | function collateMixins(mixins) {
211 | let keyed = {};
212 | for (let i = 0; i < mixins.length; i++) {
213 | let mixin = mixins[i];
214 | for (let key in mixin) {
215 | if (mixin.hasOwnProperty(key) && typeof mixin[key] === 'function') {
216 | (keyed[key] || (keyed[key] = [])).push(mixin[key]);
217 | }
218 | }
219 | }
220 | return keyed;
221 | }
222 |
223 | // apply a mapping of Arrays of mixin methods to a component prototype
224 | function applyMixins(proto, mixins) {
225 | for (let key in mixins)
226 | if (mixins.hasOwnProperty(key)) {
227 | proto[key] = multihook(
228 | mixins[key].concat(proto[key] || []),
229 | key === 'getDefaultProps' ||
230 | key === 'getInitialState' ||
231 | key === 'getChildContext'
232 | );
233 | }
234 | }
235 |
236 | function callMethod(ctx, m, args) {
237 | if (typeof m === 'string') {
238 | m = ctx.constructor.prototype[m];
239 | }
240 | if (typeof m === 'function') {
241 | return m.apply(ctx, args);
242 | }
243 | }
244 |
245 | function multihook(hooks, skipDuplicates) {
246 | return function () {
247 | let ret;
248 | for (let i = 0; i < hooks.length; i++) {
249 | let r = callMethod(this, hooks[i], arguments);
250 |
251 | if (skipDuplicates && r != null) {
252 | if (!ret) ret = {};
253 | for (let key in r)
254 | if (r.hasOwnProperty(key)) {
255 | ret[key] = r[key];
256 | }
257 | } else if (typeof r !== 'undefined') ret = r;
258 | }
259 | return ret;
260 | };
261 | }
262 |
263 | export default {
264 | PropTypes,
265 | createClass,
266 | unstable_renderSubtreeIntoContainer,
267 | useState,
268 | useId,
269 | useReducer,
270 | useEffect,
271 | useLayoutEffect,
272 | useInsertionEffect,
273 | useTransition,
274 | useDeferredValue,
275 | useSyncExternalStore,
276 | startTransition,
277 | useRef,
278 | useImperativeHandle,
279 | useMemo,
280 | useCallback,
281 | useContext,
282 | useDebugValue,
283 | version,
284 | Children,
285 | render,
286 | hydrate,
287 | unmountComponentAtNode,
288 | createPortal,
289 | createElement,
290 | createContext,
291 | createFactory,
292 | cloneElement,
293 | createRef,
294 | Fragment,
295 | isValidElement,
296 | isElement,
297 | isFragment,
298 | isMemo,
299 | findDOMNode,
300 | Component,
301 | PureComponent,
302 | memo,
303 | forwardRef,
304 | flushSync,
305 | unstable_batchedUpdates,
306 | StrictMode,
307 | Suspense,
308 | SuspenseList,
309 | lazy,
310 | __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
311 | };
312 |
313 | export {
314 | PropTypes,
315 | createClass,
316 | unstable_renderSubtreeIntoContainer,
317 | useState,
318 | useId,
319 | useReducer,
320 | useEffect,
321 | useLayoutEffect,
322 | useInsertionEffect,
323 | useTransition,
324 | useDeferredValue,
325 | useSyncExternalStore,
326 | startTransition,
327 | useRef,
328 | useImperativeHandle,
329 | useMemo,
330 | useCallback,
331 | useContext,
332 | useDebugValue,
333 | version,
334 | Children,
335 | render,
336 | hydrate,
337 | unmountComponentAtNode,
338 | createPortal,
339 | createElement,
340 | createContext,
341 | createFactory,
342 | cloneElement,
343 | createRef,
344 | Fragment,
345 | isValidElement,
346 | isElement,
347 | isFragment,
348 | isMemo,
349 | findDOMNode,
350 | Component,
351 | PureComponent,
352 | memo,
353 | forwardRef,
354 | flushSync,
355 | unstable_batchedUpdates,
356 | StrictMode,
357 | Suspense,
358 | SuspenseList,
359 | lazy,
360 | __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
361 | };
362 |
--------------------------------------------------------------------------------