├── src ├── cjs.js └── index.js ├── .gitignore ├── .editorconfig ├── .github └── workflows │ ├── compressed-size.yml │ └── ci.yml ├── README.md ├── LICENSE ├── package.json └── test └── index.test.js /src/cjs.js: -------------------------------------------------------------------------------- 1 | import compat from './index.js'; 2 | compat.__esModule = true; 3 | compat.default = compat; 4 | export default compat; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | dist 5 | yarn.lock 6 | .vscode 7 | .idea 8 | coverage 9 | *.sw[op] 10 | *.log 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{*.json,.*rc,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | insert_final_newline = false 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/workflows/compressed-size.yml: -------------------------------------------------------------------------------- 1 | name: Compressed Size 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - dev 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: preactjs/compressed-size-action@v1 15 | with: 16 | repo-token: '${{ secrets.GITHUB_TOKEN }}' 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | pull_request: 8 | branches: 9 | - dev 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: 12 20 | - name: npm install, build, and test 21 | run: | 22 | npm ci 23 | npm run build --if-present 24 | npm test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@preact/legacy-compat` npm 2 | 3 | Extended compatibility layer for Preact, supporting deprecated and removed APIs from React 15 and prior. 4 | 5 | This module provides a bridging solution for older codebases. For new projects, please use [preact/compat]. 6 | 7 | ### Extended Compatibility Features: 8 | 9 | - `createClass()` - including mixins, getInitialState, etc 10 | - string refs 11 | - Built-in PropTypes (`React.PropTypes`) 12 | - [Immutable.js] support 13 | - `renderSubtreeIntoContainer()` 14 | 15 | [preact/compat]: https://preactjs.com/guide/v10/switching-to-preact 16 | [immutable.js]: https://immutable-js.github.io/immutable-js/ 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present The Preact Authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@preact/legacy-compat", 3 | "public": true, 4 | "version": "0.3.0", 5 | "description": "Preact compatibility layer that supports deprecated React APIs'", 6 | "module": "./dist/legacy-compat.module.js", 7 | "main": "./dist/legacy-compat.js", 8 | "unpkg": "./dist/legacy-compat.umd.js", 9 | "scripts": { 10 | "build": "microbundle -f es && microbundle -f cjs,umd src/cjs.js", 11 | "pretest": "npm run build", 12 | "test": "eslint src test && jest" 13 | }, 14 | "repository": "preactjs/legacy-compat", 15 | "author": "The Preact Authors ", 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 | '
  • a
  • b
  • c
' 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 | '
    • A
    • B
    • C
    • D
    ' 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 | --------------------------------------------------------------------------------