├── benches ├── .gitignore ├── proxy-packages │ └── preact-v8-proxy │ │ ├── index.js │ │ ├── package.json │ │ └── package-lock.json ├── scripts │ ├── global.d.ts │ ├── bench.js │ └── utils.js ├── package.json ├── LICENSE └── src │ ├── text_update.html │ ├── 02_replace1k.html │ ├── 03_update10th1k_x16.html │ ├── util.js │ ├── keyed-children │ └── store.js │ └── many_updates.html ├── compat ├── test-utils.js ├── test │ ├── ts │ │ ├── react-default.tsx │ │ ├── react-star.tsx │ │ ├── tsconfig.json │ │ ├── lazy.tsx │ │ ├── forward-ref.tsx │ │ ├── suspense.tsx │ │ └── memo.tsx │ └── browser │ │ ├── unstable_batchedUpdates.test.js │ │ ├── testUtils.js │ │ ├── unmountComponentAtNode.test.js │ │ ├── createFactory.test.js │ │ ├── isValidElement.test.js │ │ ├── hydrate.test.js │ │ ├── select.test.js │ │ ├── createElement.test.js │ │ ├── textarea.test.js │ │ ├── findDOMNode.test.js │ │ ├── compat.options.test.js │ │ ├── svg.test.js │ │ └── cloneElement.test.js ├── src │ ├── suspense-list.d.ts │ ├── suspense.d.ts │ ├── PureComponent.js │ ├── Children.js │ ├── util.js │ ├── memo.js │ ├── internal.d.ts │ └── forwardRef.js ├── server.js ├── package.json ├── mangle.json └── LICENSE ├── devtools ├── src │ ├── index.js │ └── devtools.js ├── package.json ├── mangle.json └── LICENSE ├── debug ├── test │ └── browser │ │ ├── fakeDevTools.js │ │ ├── component-stack-2.test.js │ │ ├── serializeVNode.test.js │ │ ├── debug-compat.test.js │ │ └── component-stack.test.js ├── src │ ├── constants.js │ ├── index.js │ ├── util.js │ ├── check-props.js │ └── internal.d.ts ├── package.json ├── mangle.json └── LICENSE ├── src ├── cjs.js ├── constants.js ├── index.js ├── options.js ├── util.js ├── clone-element.js ├── diff │ └── catch-error.js ├── create-context.js └── render.js ├── test ├── extensions.d.ts ├── browser │ ├── isValidElement.test.js │ ├── lifecycles │ │ ├── componentDidMount.test.js │ │ ├── componentWillMount.test.js │ │ ├── componentWillUnmount.test.js │ │ └── componentWillUpdate.test.js │ ├── customBuiltInElements.test.js │ ├── select.test.js │ └── cloneElement.test.js ├── shared │ ├── isValidElement.test.js │ ├── exports.test.js │ ├── createContext.test.js │ └── isValidElementTests.js ├── ts │ ├── tsconfig.json │ ├── jsx-namespacce-test.tsx │ ├── hoc-test.tsx │ ├── refs.tsx │ └── custom-elements.tsx ├── TODO.md ├── node │ └── index.test.js ├── _util │ ├── bench.js │ ├── optionSpies.js │ ├── logCall.js │ └── dom.js ├── polyfills.js └── benchmarks │ └── text.test.js ├── sizereport.config.js ├── demo ├── nested-suspense │ ├── editor.js │ ├── subcomponent.js │ ├── dropzone.js │ ├── addnewcomponent.js │ ├── component-container.js │ └── index.js ├── people │ ├── Readme.md │ ├── styles │ │ ├── avatar.scss │ │ ├── profile.scss │ │ ├── animations.scss │ │ ├── app.scss │ │ └── button.scss │ ├── profile.tsx │ ├── index.tsx │ └── store.ts ├── suspense-router │ ├── bye.js │ ├── hello.js │ ├── index.js │ └── simple-router.js ├── tsconfig.json ├── style.css ├── fragments.js ├── contenteditable.js ├── styled-components.js ├── key_bug.js ├── textFields.js ├── devtools.js ├── preact.js ├── todo.js ├── redux.js ├── package.json ├── context.js ├── reduxUpdate.js ├── list.js ├── mobx.js ├── stateOrderBug.js ├── profiler.js ├── pythagoras │ ├── index.js │ └── pythagoras.js ├── suspense.js ├── old.js.bak ├── reorder.js └── style.scss ├── test-utils ├── src │ └── index.d.ts ├── package.json └── test │ └── shared │ └── rerender.test.js ├── .gitignore ├── hooks ├── test │ ├── _util │ │ └── useEffectUtil.js │ └── browser │ │ ├── useCallback.test.js │ │ ├── useRef.test.js │ │ ├── useDebugValue.test.js │ │ └── errorBoundary.test.js ├── mangle.json ├── package.json ├── LICENSE └── src │ └── internal.d.ts ├── .editorconfig ├── .github ├── workflows │ ├── main.yml │ └── saucelabs.yml ├── FUNDING.yml └── ISSUE_TEMPLATE.md ├── jsconfig.json ├── jsx-runtime ├── package.json ├── mangle.json ├── LICENSE ├── src │ ├── index.d.ts │ └── index.js └── test │ └── browser │ └── jsx-runtime.test.jsx ├── config ├── codemod-let-name.js ├── node-13-exports.js ├── codemod-const.js └── codemod-strip-tdz.js ├── babel.config.js ├── LICENSE └── mangle.json /benches/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | results/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /compat/test-utils.js: -------------------------------------------------------------------------------- 1 | module.exports = require('preact/test-utils'); 2 | -------------------------------------------------------------------------------- /devtools/src/index.js: -------------------------------------------------------------------------------- 1 | import { initDevTools } from './devtools'; 2 | 3 | initDevTools(); 4 | -------------------------------------------------------------------------------- /debug/test/browser/fakeDevTools.js: -------------------------------------------------------------------------------- 1 | window.__PREACT_DEVTOOLS__ = { attachPreact: sinon.spy() }; 2 | -------------------------------------------------------------------------------- /src/cjs.js: -------------------------------------------------------------------------------- 1 | import * as preact from './index.js'; 2 | if (typeof module < 'u') module.exports = preact; 3 | else self.preact = preact; 4 | -------------------------------------------------------------------------------- /debug/src/constants.js: -------------------------------------------------------------------------------- 1 | export const ELEMENT_NODE = 1; 2 | export const DOCUMENT_NODE = 9; 3 | export const DOCUMENT_FRAGMENT_NODE = 11; 4 | -------------------------------------------------------------------------------- /test/extensions.d.ts: -------------------------------------------------------------------------------- 1 | declare module Chai { 2 | export interface Assertion { 3 | equalNode(node: Node | null, message?: string): void; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /sizereport.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | repo: 'preactjs/preact', 3 | path: ['./{compat,debug,hooks,}/dist/**/!(*.map)'], 4 | branch: 'master' 5 | }; 6 | -------------------------------------------------------------------------------- /compat/test/ts/react-default.tsx: -------------------------------------------------------------------------------- 1 | import React from '../../src'; 2 | class ReactIsh extends React.Component { 3 | render() { 4 | return
Text
5 | } 6 | } 7 | -------------------------------------------------------------------------------- /debug/src/index.js: -------------------------------------------------------------------------------- 1 | import { initDebug } from './debug'; 2 | import 'preact/devtools'; 3 | 4 | initDebug(); 5 | 6 | export { resetPropWarnings } from './check-props'; 7 | -------------------------------------------------------------------------------- /benches/proxy-packages/preact-v8-proxy/index.js: -------------------------------------------------------------------------------- 1 | // import { createElement, render, Component, Fragment, cloneElement, createContext, createRef, } from 'preact'; 2 | export * from 'preact'; 3 | -------------------------------------------------------------------------------- /demo/nested-suspense/editor.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | 3 | export default function Editor({ children }) { 4 | return
{children}
; 5 | } 6 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const EMPTY_OBJ = {}; 2 | export const EMPTY_ARR = []; 3 | export const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i; 4 | -------------------------------------------------------------------------------- /test-utils/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export function setupRerender(): () => void; 2 | export function act(callback: () => void | Promise): Promise; 3 | export function teardown(): void; 4 | -------------------------------------------------------------------------------- /demo/nested-suspense/subcomponent.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | 3 | export default function SubComponent({ onClick }) { 4 | return
Lazy loaded sub component
; 5 | } 6 | -------------------------------------------------------------------------------- /demo/nested-suspense/dropzone.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | 3 | export default function DropZone({ appearance }) { 4 | return
DropZone (component #{appearance})
; 5 | } 6 | -------------------------------------------------------------------------------- /compat/test/ts/react-star.tsx: -------------------------------------------------------------------------------- 1 | // import React from '../../src'; 2 | import * as React from '../../src'; 3 | class ReactIsh extends React.Component { 4 | render() { 5 | return
Text
6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/nested-suspense/addnewcomponent.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | 3 | export default function AddNewComponent({ appearance }) { 4 | return
AddNewComponent (component #{appearance})
; 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | dist 5 | */package-lock.json 6 | yarn.lock 7 | .vscode 8 | .idea 9 | test/ts/**/*.js 10 | coverage 11 | *.sw[op] 12 | *.log 13 | package/ 14 | preact-*.tgz 15 | -------------------------------------------------------------------------------- /benches/proxy-packages/preact-v8-proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-v8-proxy", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "main": "index.js", 6 | "dependencies": { 7 | "preact": "^8.5.3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/browser/isValidElement.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, isValidElement, Component } from 'preact'; 2 | import { isValidElementTests } from '../shared/isValidElementTests'; 3 | 4 | isValidElementTests(expect, isValidElement, createElement, Component); 5 | -------------------------------------------------------------------------------- /hooks/test/_util/useEffectUtil.js: -------------------------------------------------------------------------------- 1 | export function scheduleEffectAssert(assertFn) { 2 | return new Promise(resolve => { 3 | requestAnimationFrame(() => 4 | setTimeout(() => { 5 | assertFn(); 6 | resolve(); 7 | }, 0) 8 | ); 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /test/shared/isValidElement.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, isValidElement, Component } from '../../'; 2 | import { expect } from 'chai'; 3 | import { isValidElementTests } from './isValidElementTests'; 4 | 5 | isValidElementTests(expect, isValidElement, createElement, Component); 6 | -------------------------------------------------------------------------------- /demo/people/Readme.md: -------------------------------------------------------------------------------- 1 | # People demo page 2 | 3 | This section of our demo was originally made by [phaux](https://github.com/phaux) in the [web-app-boilerplate](https://github.com/phaux/web-app-boilerplate) repo. It has been slightly modified from it's original to better work inside of our demo app 4 | -------------------------------------------------------------------------------- /demo/suspense-router/bye.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | import { Link } from './simple-router'; 3 | 4 | /** @jsx createElement */ 5 | 6 | export default function Bye() { 7 | return ( 8 |
9 | Bye! Go to Hello! 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /demo/suspense-router/hello.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | import { Link } from './simple-router'; 3 | 4 | /** @jsx createElement */ 5 | 6 | export default function Hello() { 7 | return ( 8 |
9 | Hello! Go to Bye! 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /devtools/src/devtools.js: -------------------------------------------------------------------------------- 1 | import { options, Fragment, Component } from 'preact'; 2 | 3 | export function initDevTools() { 4 | if (typeof window != 'undefined' && window.__PREACT_DEVTOOLS__) { 5 | window.__PREACT_DEVTOOLS__.attachPreact('10.5.3', options, { 6 | Fragment, 7 | Component 8 | }); 9 | } 10 | } 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 17 | -------------------------------------------------------------------------------- /benches/scripts/global.d.ts: -------------------------------------------------------------------------------- 1 | interface TachometerOptions { 2 | browser: string | string[]; 3 | framework: string | string[]; 4 | 'window-size': string; 5 | 'sample-size': number; 6 | horizon: string; 7 | timeout: number; 8 | } 9 | 10 | interface DeoptOptions { 11 | framework: string; 12 | timeout: number; 13 | open: boolean; 14 | } 15 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "jsx": "react", 5 | "jsxFactory": "h", 6 | "baseUrl": ".", 7 | "target": "es2018", 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "paths": { 11 | "preact/hooks": ["../hooks/src/index.js"], 12 | "preact": ["../src/index.js"], 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Compressed Size 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2-beta 11 | with: 12 | fetch-depth: 1 13 | - uses: preactjs/compressed-size-action@v1 14 | with: 15 | repo-token: '${{ secrets.GITHUB_TOKEN }}' 16 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "checkJs": true, 5 | "jsx": "react", 6 | "lib": ["dom", "es5"], 7 | "moduleResolution": "node", 8 | "paths": { 9 | "preact": ["."], 10 | "preact/*": ["./*"] 11 | }, 12 | "reactNamespace": "createElement", 13 | "target": "es5" 14 | }, 15 | "exclude": ["node_modules", "dist", "demo"] 16 | } 17 | -------------------------------------------------------------------------------- /debug/src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Assign properties from `props` to `obj` 3 | * @template O, P The obj and props types 4 | * @param {O} obj The object to copy properties to 5 | * @param {P} props The object to copy properties from 6 | * @returns {O & P} 7 | */ 8 | export function assign(obj, props) { 9 | for (let i in props) obj[i] = props[i]; 10 | return /** @type {O & P} */ (obj); 11 | } 12 | -------------------------------------------------------------------------------- /test/ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "lib": ["es6", "dom"], 7 | "strict": true, 8 | "typeRoots": ["../../"], 9 | "types": [], 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "react", 12 | "jsxFactory": "createElement" 13 | }, 14 | "include": ["./**/*.ts", "./**/*.tsx"] 15 | } 16 | -------------------------------------------------------------------------------- /test/ts/jsx-namespacce-test.tsx: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from '../../'; 2 | 3 | // declare global JSX types that should not be mixed with preact's internal types 4 | declare global { 5 | namespace JSX { 6 | interface Element { 7 | unknownProperty: string; 8 | } 9 | } 10 | } 11 | 12 | class SimpleComponent extends Component { 13 | render() { 14 | return
It works
; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /compat/src/suspense-list.d.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentChild } from '../../src'; 2 | 3 | // 4 | // SuspenseList 5 | // ----------------------------------- 6 | 7 | export interface SuspenseListProps { 8 | children?: preact.ComponentChildren; 9 | revealOrder?: 'forwards' | 'backwards' | 'together'; 10 | } 11 | 12 | export class SuspenseList extends Component { 13 | render(): ComponentChild; 14 | } 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: preact 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: # Replace with a single custom sponsorship URL 9 | -------------------------------------------------------------------------------- /compat/test/ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "es6", 8 | "dom", 9 | ], 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "jsx": "react", 13 | "noEmit": true, 14 | "allowSyntheticDefaultImports": true 15 | }, 16 | "include": [ 17 | "./**/*.ts", 18 | "./**/*.tsx" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /devtools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-devtools", 3 | "amdName": "preactDevtools", 4 | "version": "1.0.0", 5 | "private": true, 6 | "description": "Preact bridge for Preact devtools", 7 | "main": "dist/devtools.js", 8 | "module": "dist/devtools.module.js", 9 | "umd:main": "dist/devtools.umd.js", 10 | "source": "src/index.js", 11 | "license": "MIT", 12 | "peerDependencies": { 13 | "preact": "^10.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /benches/proxy-packages/preact-v8-proxy/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-v8-proxy", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "preact": { 8 | "version": "8.5.3", 9 | "resolved": "https://registry.npmjs.org/preact/-/preact-8.5.3.tgz", 10 | "integrity": "sha512-O3kKP+1YdgqHOFsZF2a9JVdtqD+RPzCQc3rP+Ualf7V6rmRDchZ9MJbiGTT7LuyqFKZqlHSOyO/oMFmI2lVTsw==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /compat/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var renderToString; 3 | try { 4 | renderToString = dep(require('preact-render-to-string')); 5 | } catch (e) { 6 | throw Error( 7 | 'renderToString() error: missing "preact-render-to-string" dependency.' 8 | ); 9 | } 10 | 11 | function dep(obj) { 12 | return obj['default'] || obj; 13 | } 14 | 15 | module.exports = { 16 | renderToString: renderToString, 17 | renderToStaticMarkup: renderToString 18 | }; 19 | -------------------------------------------------------------------------------- /compat/test/ts/lazy.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "../../src"; 2 | 3 | export interface LazyProps { 4 | isProp: boolean; 5 | } 6 | 7 | interface LazyState { 8 | forState: string; 9 | } 10 | export default class IsLazyComponent extends React.Component { 11 | render ({ isProp }: LazyProps) { 12 | return ( 13 |
{ 14 | isProp ? 15 | 'Super Lazy TRUE' : 16 | 'Super Lazy FALSE' 17 | }
18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /compat/src/suspense.d.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentChild } from '../../src'; 2 | 3 | // 4 | // Suspense/lazy 5 | // ----------------------------------- 6 | export function lazy(loader: () => Promise<{ default: T }>): T; 7 | 8 | export interface SuspenseProps { 9 | children?: preact.ComponentChildren; 10 | fallback: preact.ComponentChildren; 11 | } 12 | 13 | export class Suspense extends Component { 14 | render(): ComponentChild; 15 | } 16 | -------------------------------------------------------------------------------- /debug/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-debug", 3 | "amdName": "preactDebug", 4 | "version": "1.0.0", 5 | "private": true, 6 | "description": "Preact extensions for development", 7 | "main": "dist/debug.js", 8 | "module": "dist/debug.module.js", 9 | "umd:main": "dist/debug.umd.js", 10 | "source": "src/index.js", 11 | "license": "MIT", 12 | "mangle": { 13 | "regex": "^(?!_renderer)^_" 14 | }, 15 | "peerDependencies": { 16 | "preact": "^10.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font: 14px system-ui, sans-serif; 3 | } 4 | .list { 5 | list-style: none; 6 | padding: 0; 7 | } 8 | .list > li { 9 | position: relative; 10 | padding: 5px 10px; 11 | animation: fadeIn 1s ease; 12 | } 13 | @keyframes fadeIn { 14 | 0% { 15 | box-shadow: inset 0 0 2px 2px red, 16 | 0 0 2px 2px red; 17 | } 18 | } 19 | .list > .odd { 20 | background-color: #def; 21 | } 22 | .list > .even { 23 | background-color: #fed; 24 | } 25 | -------------------------------------------------------------------------------- /jsx-runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsx-runtime", 3 | "amdName": "jsxRuntime", 4 | "version": "1.0.0", 5 | "private": true, 6 | "description": "Preact JSX runtime", 7 | "main": "dist/jsxRuntime.js", 8 | "module": "dist/jsxRuntime.module.js", 9 | "umd:main": "dist/jsxRuntime.umd.js", 10 | "source": "src/index.js", 11 | "types": "src/index.d.ts", 12 | "license": "MIT", 13 | "peerDependencies": { 14 | "preact": "^10.0.0" 15 | }, 16 | "mangle": { 17 | "regex": "^_" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { render, hydrate } from './render'; 2 | export { 3 | createElement, 4 | createElement as h, 5 | Fragment, 6 | createRef, 7 | isValidElement 8 | } from './create-element'; 9 | export { Component } from './component'; 10 | export { cloneElement } from './clone-element'; 11 | export { createContext } from './create-context'; 12 | export { toChildArray } from './diff/children'; 13 | export { unmount as __u } from './diff'; 14 | export { default as options } from './options'; 15 | -------------------------------------------------------------------------------- /demo/people/styles/avatar.scss: -------------------------------------------------------------------------------- 1 | #people-app { 2 | .avatar { 3 | display: inline-block; 4 | overflow: hidden; 5 | width: var(--avatar-size, 32px); 6 | height: var(--avatar-size, 32px); 7 | background-color: var(--avatar-color, var(--app-primary)); 8 | border-radius: 50%; 9 | font-size: calc(var(--avatar-size, 32px) * 0.5); 10 | line-height: var(--avatar-size, 32px); 11 | object-fit: cover; 12 | text-align: center; 13 | text-transform: uppercase; 14 | white-space: nowrap; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo/nested-suspense/component-container.js: -------------------------------------------------------------------------------- 1 | import { createElement, lazy } from 'react'; 2 | 3 | const pause = timeout => 4 | new Promise(d => setTimeout(d, timeout), console.log(timeout)); 5 | 6 | const SubComponent = lazy(() => 7 | pause(Math.random() * 1000).then(() => import('./subcomponent.js')) 8 | ); 9 | 10 | export default function ComponentContainer({ appearance }) { 11 | return ( 12 |
13 | GenerateComponents (component #{appearance}) 14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /test-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-utils", 3 | "amdName": "preactTestUtils", 4 | "version": "0.1.0", 5 | "private": true, 6 | "description": "Test-utils for Preact", 7 | "main": "dist/testUtils.js", 8 | "module": "dist/testUtils.module.js", 9 | "umd:main": "dist/testUtils.umd.js", 10 | "source": "src/index.js", 11 | "license": "MIT", 12 | "types": "src/index.d.ts", 13 | "peerDependencies": { 14 | "preact": "^10.0.0" 15 | }, 16 | "mangle": { 17 | "regex": "^_" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /compat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-compat", 3 | "amdName": "preactCompat", 4 | "version": "4.0.0", 5 | "private": true, 6 | "description": "A React compatibility layer for Preact", 7 | "main": "dist/compat.js", 8 | "module": "dist/compat.module.js", 9 | "umd:main": "dist/compat.umd.js", 10 | "source": "src/index.js", 11 | "types": "src/index.d.ts", 12 | "license": "MIT", 13 | "mangle": { 14 | "regex": "^_" 15 | }, 16 | "peerDependencies": { 17 | "preact": "^10.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /compat/test/browser/unstable_batchedUpdates.test.js: -------------------------------------------------------------------------------- 1 | import { unstable_batchedUpdates } from 'preact/compat'; 2 | 3 | describe('unstable_batchedUpdates', () => { 4 | it('should call the callback', () => { 5 | const spy = sinon.spy(); 6 | unstable_batchedUpdates(spy); 7 | expect(spy).to.be.calledOnce; 8 | }); 9 | 10 | it('should call callback with only one arg', () => { 11 | const spy = sinon.spy(); 12 | unstable_batchedUpdates(spy, 'foo', 'bar'); 13 | expect(spy).to.be.calledWithExactly('foo'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /config/codemod-let-name.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Restores var names transformed by babel's let block scoping 3 | */ 4 | export default (file, api) => { 5 | let j = api.jscodeshift; 6 | let code = j(file.source); 7 | 8 | // @TODO unsafe, but without it we gain 20b gzipped: https://www.diffchecker.com/bVrOJWTO 9 | code 10 | .findVariableDeclarators() 11 | .filter(d => /^_i/.test(d.value.id.name)) 12 | .renameTo('i'); 13 | code.findVariableDeclarators('_key').renameTo('key'); 14 | 15 | return code.toSource({ quote: 'single' }); 16 | }; 17 | -------------------------------------------------------------------------------- /demo/people/styles/profile.scss: -------------------------------------------------------------------------------- 1 | #people-app { 2 | .profile { 3 | display: flex; 4 | flex-flow: column; 5 | align-items: center; 6 | margin: 32px 0; 7 | animation: appear-from-left 0.5s forwards; 8 | --avatar-size: 80px; 9 | } 10 | 11 | .profile h2 { 12 | text-transform: capitalize; 13 | } 14 | 15 | .profile .details { 16 | display: flex; 17 | flex-flow: column; 18 | align-items: stretch; 19 | margin: 16px auto; 20 | } 21 | 22 | .profile .details p { 23 | margin-top: 8px; 24 | margin-bottom: 8px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/TODO.md: -------------------------------------------------------------------------------- 1 | # Tests skipped to get CI to pass 2 | 3 | - Fragment 4 | - ✖ should not preserve state between array nested in fragment and double nested array 5 | - ✖ should preserve state between double nested fragment and double nested array 6 | - hydrate 7 | - ✖ should override incorrect pre-existing DOM with VNodes passed into render 8 | 9 | Also: 10 | 11 | - Extend Fragment preserving state tests to track unmounting lifecycle callbacks to verify 12 | components are properly unmounted. I think all 'should not preserve' tests are the ones 13 | that will have unmount operations. 14 | -------------------------------------------------------------------------------- /demo/fragments.js: -------------------------------------------------------------------------------- 1 | import { createElement, Component, Fragment } from 'preact'; 2 | 3 | export default class extends Component { 4 | state = { number: 0 }; 5 | 6 | componentDidMount() { 7 | setInterval(_ => this.updateChildren(), 1000); 8 | } 9 | 10 | updateChildren() { 11 | this.setState(state => ({ number: state.number + 1 })); 12 | } 13 | 14 | render(props, state) { 15 | return ( 16 |
17 |
{state.number}
18 | <> 19 |
one
20 |
{state.number}
21 |
three
22 | 23 |
24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /compat/src/PureComponent.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'preact'; 2 | import { shallowDiffers } from './util'; 3 | 4 | /** 5 | * Component class with a predefined `shouldComponentUpdate` implementation 6 | */ 7 | export function PureComponent(p) { 8 | this.props = p; 9 | } 10 | PureComponent.prototype = new Component(); 11 | // Some third-party libraries check if this property is present 12 | PureComponent.prototype.isPureReactComponent = true; 13 | PureComponent.prototype.shouldComponentUpdate = function(props, state) { 14 | return shallowDiffers(this.props, props) || shallowDiffers(this.state, state); 15 | }; 16 | -------------------------------------------------------------------------------- /demo/people/styles/animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes popup { 2 | from { 3 | box-shadow: 0 0 0 black; 4 | opacity: 0; 5 | transform: scale(0.9); 6 | } 7 | to { 8 | box-shadow: 0 30px 70px rgba(0, 0, 0, 0.5); 9 | opacity: 1; 10 | transform: none; 11 | } 12 | } 13 | 14 | @keyframes zoom { 15 | from { 16 | opacity: 0; 17 | transform: scale(0.8); 18 | } 19 | to { 20 | opacity: 1; 21 | transform: none; 22 | } 23 | } 24 | 25 | @keyframes appear-from-left { 26 | from { 27 | opacity: 0; 28 | transform: translateX(-25px); 29 | } 30 | to { 31 | opacity: 1; 32 | transform: none; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/node/index.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as preact from '../../'; 3 | 4 | describe('build artifact', () => { 5 | // #1075 Check that the build artifact has the correct exports 6 | it('should have exported properties', () => { 7 | expect(preact).to.be.an('object'); 8 | expect(preact).to.have.property('createElement'); 9 | expect(preact).to.have.property('h'); 10 | expect(preact).to.have.property('Component'); 11 | expect(preact).to.have.property('render'); 12 | expect(preact).to.have.property('hydrate'); 13 | // expect(preact).to.have.property('options'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /compat/src/Children.js: -------------------------------------------------------------------------------- 1 | import { toChildArray } from 'preact'; 2 | 3 | const mapFn = (children, fn) => { 4 | if (children == null) return null; 5 | return toChildArray(toChildArray(children).map(fn)); 6 | }; 7 | 8 | // This API is completely unnecessary for Preact, so it's basically passthrough. 9 | export const Children = { 10 | map: mapFn, 11 | forEach: mapFn, 12 | count(children) { 13 | return children ? toChildArray(children).length : 0; 14 | }, 15 | only(children) { 16 | const normalized = toChildArray(children); 17 | if (normalized.length !== 1) throw 'Children.only'; 18 | return normalized[0]; 19 | }, 20 | toArray: toChildArray 21 | }; 22 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | import { _catchError } from './diff/catch-error'; 2 | 3 | /** 4 | * The `option` object can potentially contain callback functions 5 | * that are called during various stages of our renderer. This is the 6 | * foundation on which all our addons like `preact/debug`, `preact/compat`, 7 | * and `preact/hooks` are based on. See the `Options` type in `internal.d.ts` 8 | * for a full list of available option hooks (most editors/IDEs allow you to 9 | * ctrl+click or cmd+click on mac the type definition below). 10 | * @type {import('./internal').Options} 11 | */ 12 | const options = { 13 | _catchError 14 | }; 15 | 16 | export default options; 17 | -------------------------------------------------------------------------------- /compat/test/ts/forward-ref.tsx: -------------------------------------------------------------------------------- 1 | import React from '../../src'; 2 | 3 | const MyInput: React.ForwardFn<{ id: string }, { focus(): void }> = (props, ref) => { 4 | const inputRef = React.useRef() 5 | 6 | React.useImperativeHandle(ref, () => ({ 7 | focus: () => { 8 | if (inputRef.current) { 9 | inputRef.current.focus() 10 | } 11 | } 12 | })) 13 | 14 | return 15 | } 16 | 17 | export const foo = React.forwardRef(MyInput) 18 | 19 | export const Bar = React.forwardRef((props, ref) => { 20 | return
{props.children}
21 | }) 22 | -------------------------------------------------------------------------------- /demo/contenteditable.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'preact'; 2 | import { useState } from 'preact/hooks'; 3 | 4 | export default function Contenteditable() { 5 | const [value, setValue] = useState("Hey there
I'm editable!"); 6 | 7 | return ( 8 |
9 |
10 | 11 |
12 |
setValue(e.currentTarget.innerHTML)} 21 | dangerouslySetInnerHTML={{ __html: value }} 22 | /> 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /demo/styled-components.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'preact'; 2 | import styled, { css } from 'styled-components'; 3 | 4 | const Button = styled.button` 5 | background: transparent; 6 | border-radius: 3px; 7 | border: 2px solid palevioletred; 8 | color: palevioletred; 9 | margin: 0.5em 1em; 10 | padding: 0.25em 1em; 11 | 12 | ${props => 13 | props.primary && 14 | css` 15 | background: palevioletred; 16 | color: white; 17 | `} 18 | `; 19 | 20 | const Container = styled.div` 21 | text-align: center; 22 | `; 23 | 24 | export default function StyledComp() { 25 | return ( 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /demo/key_bug.js: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from 'preact'; 2 | 3 | function Foo(props) { 4 | return
This is: {props.children}
; 5 | } 6 | 7 | export default class KeyBug extends Component { 8 | constructor() { 9 | super(); 10 | this.onClick = this.onClick.bind(this); 11 | this.state = { active: false }; 12 | } 13 | 14 | onClick() { 15 | this.setState(prev => ({ active: !prev.active })); 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 | {this.state.active && foo} 22 |

Hello World

23 |
24 | 25 | bar bar 26 | 27 |
28 | 29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/shared/exports.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | createElement, 3 | h, 4 | createContext, 5 | Component, 6 | Fragment, 7 | render, 8 | hydrate, 9 | cloneElement, 10 | options 11 | } from '../../'; 12 | import { expect } from 'chai'; 13 | 14 | describe('preact', () => { 15 | it('should be available as named exports', () => { 16 | expect(h).to.be.a('function'); 17 | expect(createElement).to.be.a('function'); 18 | expect(Component).to.be.a('function'); 19 | expect(Fragment).to.exist; 20 | expect(render).to.be.a('function'); 21 | expect(hydrate).to.be.a('function'); 22 | expect(cloneElement).to.be.a('function'); 23 | expect(createContext).to.be.a('function'); 24 | expect(options).to.exist.and.be.an('object'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /compat/src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Assign properties from `props` to `obj` 3 | * @template O, P The obj and props types 4 | * @param {O} obj The object to copy properties to 5 | * @param {P} props The object to copy properties from 6 | * @returns {O & P} 7 | */ 8 | export function assign(obj, props) { 9 | for (let i in props) obj[i] = props[i]; 10 | return /** @type {O & P} */ (obj); 11 | } 12 | 13 | /** 14 | * Check if two objects have a different shape 15 | * @param {object} a 16 | * @param {object} b 17 | * @returns {boolean} 18 | */ 19 | export function shallowDiffers(a, b) { 20 | for (let i in a) if (i !== '__source' && !(i in b)) return true; 21 | for (let i in b) if (i !== '__source' && a[i] !== b[i]) return true; 22 | return false; 23 | } 24 | -------------------------------------------------------------------------------- /compat/test/browser/testUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieve a Symbol if supported or use the fallback value 3 | * @param {string} name The name of the Symbol to look up 4 | * @param {number} fallback Fallback value if Symbols are not supported 5 | */ 6 | export function getSymbol(name, fallback) { 7 | let out = fallback; 8 | 9 | try { 10 | // eslint-disable-next-line 11 | if ( 12 | Function.prototype.toString 13 | .call(eval('Symbol.for')) 14 | .match(/\[native code\]/) 15 | ) { 16 | // Concatenate these string literals to prevent the test 17 | // harness and/or Babel from modifying the symbol value. 18 | // eslint-disable-next-line 19 | out = eval('Sym' + 'bol.for("' + name + '")'); 20 | } 21 | } catch (e) {} 22 | 23 | return out; 24 | } 25 | -------------------------------------------------------------------------------- /compat/mangle.json: -------------------------------------------------------------------------------- 1 | { 2 | "help": { 3 | "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", 4 | "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." 5 | }, 6 | "minify": { 7 | "mangle": { 8 | "properties": { 9 | "regex": "^_[^_]", 10 | "reserved": [ 11 | "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", 12 | "__REACT_DEVTOOLS_GLOBAL_HOOK__", 13 | "__PREACT_DEVTOOLS__", 14 | "_renderers", 15 | "__source", 16 | "__self" 17 | ] 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /debug/mangle.json: -------------------------------------------------------------------------------- 1 | { 2 | "help": { 3 | "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", 4 | "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." 5 | }, 6 | "minify": { 7 | "mangle": { 8 | "properties": { 9 | "regex": "^_[^_]", 10 | "reserved": [ 11 | "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", 12 | "__REACT_DEVTOOLS_GLOBAL_HOOK__", 13 | "__PREACT_DEVTOOLS__", 14 | "_renderers", 15 | "__source", 16 | "__self" 17 | ] 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /hooks/mangle.json: -------------------------------------------------------------------------------- 1 | { 2 | "help": { 3 | "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", 4 | "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." 5 | }, 6 | "minify": { 7 | "mangle": { 8 | "properties": { 9 | "regex": "^_[^_]", 10 | "reserved": [ 11 | "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", 12 | "__REACT_DEVTOOLS_GLOBAL_HOOK__", 13 | "__PREACT_DEVTOOLS__", 14 | "_renderers", 15 | "__source", 16 | "__self" 17 | ] 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /devtools/mangle.json: -------------------------------------------------------------------------------- 1 | { 2 | "help": { 3 | "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", 4 | "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." 5 | }, 6 | "minify": { 7 | "mangle": { 8 | "properties": { 9 | "regex": "^_[^_]", 10 | "reserved": [ 11 | "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", 12 | "__REACT_DEVTOOLS_GLOBAL_HOOK__", 13 | "__PREACT_DEVTOOLS__", 14 | "_renderers", 15 | "__source", 16 | "__self" 17 | ] 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /jsx-runtime/mangle.json: -------------------------------------------------------------------------------- 1 | { 2 | "help": { 3 | "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", 4 | "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." 5 | }, 6 | "minify": { 7 | "mangle": { 8 | "properties": { 9 | "regex": "^_[^_]", 10 | "reserved": [ 11 | "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", 12 | "__REACT_DEVTOOLS_GLOBAL_HOOK__", 13 | "__PREACT_DEVTOOLS__", 14 | "_renderers", 15 | "__source", 16 | "__self" 17 | ] 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /demo/suspense-router/index.js: -------------------------------------------------------------------------------- 1 | import { createElement, Suspense, lazy } from 'react'; 2 | 3 | import { Router, Route, Switch } from './simple-router'; 4 | 5 | /** @jsx createElement */ 6 | 7 | let Hello = lazy(() => import('./hello.js')); 8 | let Bye = lazy(() => import('./bye.js')); 9 | 10 | function Loading() { 11 | return
Hey! This is a fallback because we're loading things! :D
; 12 | } 13 | 14 | export default function SuspenseRouterBug() { 15 | return ( 16 | 17 |

Suspense Router bug

18 | }> 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Assign properties from `props` to `obj` 3 | * @template O, P The obj and props types 4 | * @param {O} obj The object to copy properties to 5 | * @param {P} props The object to copy properties from 6 | * @returns {O & P} 7 | */ 8 | export function assign(obj, props) { 9 | for (let i in props) obj[i] = props[i]; 10 | return /** @type {O & P} */ (obj); 11 | } 12 | 13 | /** 14 | * Remove a child node from its parent if attached. This is a workaround for 15 | * IE11 which doesn't support `Element.prototype.remove()`. Using this function 16 | * is smaller than including a dedicated polyfill. 17 | * @param {Node} node The node to remove 18 | */ 19 | export function removeNode(node) { 20 | let parentNode = node.parentNode; 21 | if (parentNode) parentNode.removeChild(node); 22 | } 23 | -------------------------------------------------------------------------------- /hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-hooks", 3 | "amdName": "preactHooks", 4 | "version": "0.1.0", 5 | "private": true, 6 | "description": "Hook addon for Preact", 7 | "main": "dist/hooks.js", 8 | "module": "dist/hooks.module.js", 9 | "umd:main": "dist/hooks.umd.js", 10 | "source": "src/index.js", 11 | "license": "MIT", 12 | "types": "src/index.d.ts", 13 | "scripts": { 14 | "build": "microbundle build --raw", 15 | "dev": "microbundle watch --raw --format cjs", 16 | "test": "npm-run-all build --parallel test:karma", 17 | "test:karma": "karma start test/karma.conf.js --single-run", 18 | "test:karma:watch": "karma start test/karma.conf.js --no-single-run" 19 | }, 20 | "peerDependencies": { 21 | "preact": "^10.0.0" 22 | }, 23 | "mangle": { 24 | "regex": "^_" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /benches/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-benchmarks", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "description": "Benchmarks for Preact", 7 | "scripts": { 8 | "prepare": "cd proxy-packages/preact-v8-proxy && npm ci", 9 | "start": "node ./scripts config text_update.html && tach --config dist/text_update.config.json --manual", 10 | "bench": "node ./scripts bench", 11 | "deopts": "node ./scripts deopts", 12 | "help": "node ./scripts --help" 13 | }, 14 | "license": "MIT", 15 | "dependencies": { 16 | "afterframe": "^1.0.1" 17 | }, 18 | "devDependencies": { 19 | "@kristoferbaxter/async": "^1.0.0", 20 | "escalade": "^3.0.2", 21 | "escape-string-regexp": "^4.0.0", 22 | "globby": "^11.0.0", 23 | "sade": "^1.7.3", 24 | "strip-ansi": "^6.0.0", 25 | "tachometer": "^0.5.1", 26 | "v8-deopt-viewer": "^0.1.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /compat/test/browser/unmountComponentAtNode.test.js: -------------------------------------------------------------------------------- 1 | import React, { createElement, unmountComponentAtNode } from 'preact/compat'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | 4 | describe('unmountComponentAtNode', () => { 5 | /** @type {HTMLDivElement} */ 6 | let scratch; 7 | 8 | beforeEach(() => { 9 | scratch = setupScratch(); 10 | }); 11 | 12 | afterEach(() => { 13 | teardown(scratch); 14 | }); 15 | 16 | it('should unmount a root node', () => { 17 | const App = () =>
foo
; 18 | React.render(, scratch); 19 | 20 | expect(unmountComponentAtNode(scratch)).to.equal(true); 21 | expect(scratch.innerHTML).to.equal(''); 22 | }); 23 | 24 | it('should do nothing if root is not mounted', () => { 25 | expect(unmountComponentAtNode(scratch)).to.equal(false); 26 | expect(scratch.innerHTML).to.equal(''); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /demo/textFields.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import TextField from '@material-ui/core/TextField'; 3 | 4 | /** @jsx React.createElement */ 5 | 6 | const PatchedTextField = props => { 7 | const [value, set] = useState(props.value); 8 | return ( 9 | set(e.target.value)} /> 10 | ); 11 | }; 12 | 13 | const TextFields = () => ( 14 |
15 | 21 | 27 | 34 |
35 | ); 36 | 37 | export default TextFields; 38 | -------------------------------------------------------------------------------- /config/node-13-exports.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const subRepositories = ['compat', 'debug', 'devtools', 'hooks', 'jsx-runtime', 'test-utils']; 4 | const snakeCaseToCamelCase = str => 5 | str.replace(/([-_][a-z])/g, group => group.toUpperCase().replace('-', '')); 6 | 7 | const copyPreact = () => { 8 | // Copy .module.js --> .mjs for Node 13 compat. 9 | fs.writeFileSync( 10 | `${process.cwd()}/dist/preact.mjs`, 11 | fs.readFileSync(`${process.cwd()}/dist/preact.module.js`) 12 | ); 13 | }; 14 | 15 | const copy = name => { 16 | // Copy .module.js --> .mjs for Node 13 compat. 17 | const filename = name.includes('-') ? snakeCaseToCamelCase(name) : name; 18 | fs.writeFileSync( 19 | `${process.cwd()}/${name}/dist/${filename}.mjs`, 20 | fs.readFileSync(`${process.cwd()}/${name}/dist/${filename}.module.js`) 21 | ); 22 | }; 23 | 24 | copyPreact(); 25 | subRepositories.forEach(copy); 26 | -------------------------------------------------------------------------------- /compat/test/browser/createFactory.test.js: -------------------------------------------------------------------------------- 1 | import React, { render, createElement, createFactory } from 'preact/compat'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | 4 | describe('createFactory', () => { 5 | /** @type {HTMLDivElement} */ 6 | let scratch; 7 | 8 | beforeEach(() => { 9 | scratch = setupScratch(); 10 | }); 11 | 12 | afterEach(() => { 13 | teardown(scratch); 14 | }); 15 | 16 | it('should create a DOM element', () => { 17 | render(createFactory('span')({ class: 'foo' }, '1'), scratch); 18 | expect(scratch.innerHTML).to.equal('1'); 19 | }); 20 | 21 | it('should create a component', () => { 22 | const Foo = ({ id, children }) =>
foo {children}
; 23 | render(createFactory(Foo)({ id: 'value' }, 'bar'), scratch); 24 | expect(scratch.innerHTML).to.equal('
foo bar
'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /demo/devtools.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import { 3 | createElement, 4 | Component, 5 | memo, 6 | Fragment, 7 | Suspense, 8 | lazy 9 | } from 'react'; 10 | 11 | function Foo() { 12 | return
I'm memoed
; 13 | } 14 | 15 | function LazyComp() { 16 | return
I'm (fake) lazy loaded
; 17 | } 18 | 19 | const Lazy = lazy(() => Promise.resolve({ default: LazyComp })); 20 | 21 | const Memoed = memo(Foo); 22 | 23 | export default class DevtoolsDemo extends Component { 24 | render() { 25 | return ( 26 |
27 |

memo()

28 |

29 | functional component: 30 |

31 | 32 |

lazy()

33 |

34 | functional component: 35 |

36 | Loading (fake) lazy loaded component...
}> 37 | 38 | 39 |
40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /compat/test/browser/isValidElement.test.js: -------------------------------------------------------------------------------- 1 | import { createElement as preactCreateElement } from 'preact'; 2 | import React, { isValidElement } from 'preact/compat'; 3 | 4 | describe('isValidElement', () => { 5 | it('should check return false for invalid arguments', () => { 6 | expect(isValidElement(null)).to.equal(false); 7 | expect(isValidElement(false)).to.equal(false); 8 | expect(isValidElement(true)).to.equal(false); 9 | expect(isValidElement('foo')).to.equal(false); 10 | expect(isValidElement(123)).to.equal(false); 11 | expect(isValidElement([])).to.equal(false); 12 | expect(isValidElement({})).to.equal(false); 13 | }); 14 | 15 | it('should detect a preact vnode', () => { 16 | expect(isValidElement(preactCreateElement('div'))).to.equal(true); 17 | }); 18 | 19 | it('should detect a compat vnode', () => { 20 | expect(isValidElement(React.createElement('div'))).to.equal(true); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/browser/lifecycles/componentDidMount.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, Component } from 'preact'; 2 | import { setupScratch, teardown } from '../../_util/helpers'; 3 | 4 | /** @jsx createElement */ 5 | 6 | describe('Lifecycle methods', () => { 7 | /** @type {HTMLDivElement} */ 8 | let scratch; 9 | 10 | beforeEach(() => { 11 | scratch = setupScratch(); 12 | }); 13 | 14 | afterEach(() => { 15 | teardown(scratch); 16 | }); 17 | 18 | describe('#componentDidMount', () => { 19 | it('is invoked after refs are set', () => { 20 | const spy = sinon.spy(); 21 | 22 | class App extends Component { 23 | componentDidMount() { 24 | expect(spy).to.have.been.calledOnceWith(scratch.firstChild); 25 | } 26 | 27 | render() { 28 | return
; 29 | } 30 | } 31 | 32 | render(, scratch); 33 | expect(spy).to.have.been.calledOnceWith(scratch.firstChild); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /compat/test/browser/hydrate.test.js: -------------------------------------------------------------------------------- 1 | import React, { hydrate } from 'preact/compat'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | 4 | describe('compat hydrate', () => { 5 | /** @type {HTMLDivElement} */ 6 | let scratch; 7 | 8 | beforeEach(() => { 9 | scratch = setupScratch(); 10 | }); 11 | 12 | afterEach(() => { 13 | teardown(scratch); 14 | }); 15 | 16 | it('should render react-style jsx', () => { 17 | const input = document.createElement('input'); 18 | scratch.appendChild(input); 19 | input.focus(); 20 | expect(document.activeElement).to.equal(input); 21 | 22 | hydrate(, scratch); 23 | expect(document.activeElement).to.equal(input); 24 | }); 25 | 26 | it('should call the callback', () => { 27 | scratch.innerHTML = '
'; 28 | 29 | let spy = sinon.spy(); 30 | hydrate(
, scratch, spy); 31 | expect(spy).to.be.calledOnce; 32 | expect(spy).to.be.calledWithExactly(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/shared/createContext.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, createContext } from '../../'; 2 | import { expect } from 'chai'; 3 | 4 | /** @jsx createElement */ 5 | /* eslint-env browser, mocha */ 6 | 7 | describe('createContext', () => { 8 | it('should return a Provider and a Consumer', () => { 9 | const context = createContext(); 10 | expect(context).to.have.property('Provider'); 11 | expect(context).to.have.property('Consumer'); 12 | }); 13 | 14 | it('should return a valid Provider Component', () => { 15 | const { Provider } = createContext(); 16 | const contextValue = { value: 'test' }; 17 | const children = [
child1
,
child2
]; 18 | 19 | const providerComponent = {children}; 20 | //expect(providerComponent).to.have.property('tag', 'Provider'); 21 | expect(providerComponent.props.value).to.equal(contextValue.value); 22 | expect(providerComponent.props.children).to.equal(children); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/browser/customBuiltInElements.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, Component } from 'preact'; 2 | import { setupScratch, teardown } from '../_util/helpers'; 3 | 4 | /** @jsx createElement */ 5 | 6 | const runSuite = typeof customElements == 'undefined' ? xdescribe : describe; 7 | 8 | runSuite('customised built-in elements', () => { 9 | let scratch; 10 | 11 | beforeEach(() => { 12 | scratch = setupScratch(); 13 | }); 14 | 15 | afterEach(() => { 16 | teardown(scratch); 17 | }); 18 | 19 | it('should create built in elements correctly', () => { 20 | class Foo extends Component { 21 | render() { 22 | return
; 23 | } 24 | } 25 | 26 | const spy = sinon.spy(); 27 | 28 | class BuiltIn extends HTMLDivElement { 29 | connectedCallback() { 30 | spy(); 31 | } 32 | } 33 | 34 | customElements.define('built-in', BuiltIn, { extends: 'div' }); 35 | 36 | render(, scratch); 37 | 38 | expect(spy).to.have.been.calledOnce; 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /compat/test/browser/select.test.js: -------------------------------------------------------------------------------- 1 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 2 | import React, { createElement, render } from 'preact/compat'; 3 | 4 | describe('Select', () => { 5 | let scratch; 6 | 7 | beforeEach(() => { 8 | scratch = setupScratch(); 9 | }); 10 | 11 | afterEach(() => { 12 | teardown(scratch); 13 | }); 14 | 15 | it('should work with multiple selected (array of values)', () => { 16 | function App() { 17 | return ( 18 | 23 | ); 24 | } 25 | 26 | render(, scratch); 27 | const options = scratch.firstChild.children; 28 | expect(options[0]).to.have.property('selected', false); 29 | expect(options[1]).to.have.property('selected', true); 30 | expect(options[2]).to.have.property('selected', true); 31 | expect(scratch.firstChild.value).to.equal('B'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/browser/lifecycles/componentWillMount.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, Component } from 'preact'; 2 | import { setupScratch, teardown } from '../../_util/helpers'; 3 | 4 | /** @jsx createElement */ 5 | 6 | describe('Lifecycle methods', () => { 7 | /** @type {HTMLDivElement} */ 8 | let scratch; 9 | 10 | beforeEach(() => { 11 | scratch = setupScratch(); 12 | }); 13 | 14 | afterEach(() => { 15 | teardown(scratch); 16 | }); 17 | 18 | describe('#componentWillMount', () => { 19 | it('should update state when called setState in componentWillMount', () => { 20 | let componentState; 21 | 22 | class Foo extends Component { 23 | constructor(props) { 24 | super(props); 25 | this.state = { 26 | value: 0 27 | }; 28 | } 29 | componentWillMount() { 30 | this.setState({ value: 1 }); 31 | } 32 | render() { 33 | componentState = this.state; 34 | return
; 35 | } 36 | } 37 | 38 | render(, scratch); 39 | 40 | expect(componentState).to.deep.equal({ value: 1 }); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /demo/preact.js: -------------------------------------------------------------------------------- 1 | import { 2 | options, 3 | createElement, 4 | cloneElement, 5 | Component as CevicheComponent, 6 | render 7 | } from 'preact'; 8 | 9 | options.vnode = vnode => { 10 | vnode.nodeName = vnode.type; 11 | vnode.attributes = vnode.props; 12 | vnode.children = vnode._children || [].concat(vnode.props.children || []); 13 | }; 14 | 15 | function asArray(arr) { 16 | return Array.isArray(arr) ? arr : [arr]; 17 | } 18 | 19 | function normalize(obj) { 20 | if (Array.isArray(obj)) { 21 | return obj.map(normalize); 22 | } 23 | if ('type' in obj && !('attributes' in obj)) { 24 | obj.attributes = obj.props; 25 | } 26 | return obj; 27 | } 28 | 29 | export function Component(props, context) { 30 | CevicheComponent.call(this, props, context); 31 | const render = this.render; 32 | this.render = function(props, state, context) { 33 | if (props.children) props.children = asArray(normalize(props.children)); 34 | return render.call(this, props, state, context); 35 | }; 36 | } 37 | Component.prototype = new CevicheComponent(); 38 | 39 | export { createElement, createElement as h, cloneElement, render }; 40 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | 4 | const minify = String(process.env.MINIFY) === 'true'; 5 | 6 | const rename = {}; 7 | const mangle = require('./mangle.json'); 8 | for (let prop in mangle.props.props) { 9 | let name = prop; 10 | if (name[0] === '$') { 11 | name = name.slice(1); 12 | } 13 | 14 | rename[name] = mangle.props.props[prop]; 15 | } 16 | 17 | return { 18 | presets: [ 19 | [ 20 | '@babel/preset-env', 21 | { 22 | loose: true, 23 | exclude: ['@babel/plugin-transform-typeof-symbol'], 24 | targets: { 25 | browsers: ['last 2 versions', 'IE >= 9'] 26 | } 27 | } 28 | ] 29 | ], 30 | plugins: [ 31 | '@babel/plugin-proposal-object-rest-spread', 32 | '@babel/plugin-transform-react-jsx', 33 | 'babel-plugin-transform-async-to-promises', 34 | ['babel-plugin-transform-rename-properties', { rename }] 35 | ], 36 | include: ['**/src/**/*.js', '**/test/**/*.js'], 37 | overrides: [ 38 | { 39 | test: /(component-stack|debug)\.test\.js$/, 40 | plugins: ['@babel/plugin-transform-react-jsx-source'] 41 | } 42 | ] 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Jason Miller 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. 22 | -------------------------------------------------------------------------------- /benches/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Jason Miller 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. 22 | -------------------------------------------------------------------------------- /compat/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Jason Miller 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. 22 | -------------------------------------------------------------------------------- /debug/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Jason Miller 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. 22 | -------------------------------------------------------------------------------- /hooks/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Jason Miller 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. 22 | -------------------------------------------------------------------------------- /devtools/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Jason Miller 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. 22 | -------------------------------------------------------------------------------- /jsx-runtime/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Jason Miller 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. 22 | -------------------------------------------------------------------------------- /hooks/test/browser/useCallback.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render } from 'preact'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | import { useCallback } from 'preact/hooks'; 4 | 5 | /** @jsx createElement */ 6 | 7 | describe('useCallback', () => { 8 | /** @type {HTMLDivElement} */ 9 | let scratch; 10 | 11 | beforeEach(() => { 12 | scratch = setupScratch(); 13 | }); 14 | 15 | afterEach(() => { 16 | teardown(scratch); 17 | }); 18 | 19 | it('only recomputes the callback when inputs change', () => { 20 | const callbacks = []; 21 | 22 | function Comp({ a, b }) { 23 | const cb = useCallback(() => a + b, [a, b]); 24 | callbacks.push(cb); 25 | return null; 26 | } 27 | 28 | render(, scratch); 29 | render(, scratch); 30 | 31 | expect(callbacks[0]).to.equal(callbacks[1]); 32 | expect(callbacks[0]()).to.equal(2); 33 | 34 | render(, scratch); 35 | render(, scratch); 36 | 37 | expect(callbacks[1]).to.not.equal(callbacks[2]); 38 | expect(callbacks[2]).to.equal(callbacks[3]); 39 | expect(callbacks[2]()).to.equal(3); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /hooks/test/browser/useRef.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render } from 'preact'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | import { useRef } from 'preact/hooks'; 4 | 5 | /** @jsx createElement */ 6 | 7 | describe('useRef', () => { 8 | /** @type {HTMLDivElement} */ 9 | let scratch; 10 | 11 | beforeEach(() => { 12 | scratch = setupScratch(); 13 | }); 14 | 15 | afterEach(() => { 16 | teardown(scratch); 17 | }); 18 | 19 | it('provides a stable reference', () => { 20 | const values = []; 21 | 22 | function Comp() { 23 | const ref = useRef(1); 24 | values.push(ref.current); 25 | ref.current = 2; 26 | return null; 27 | } 28 | 29 | render(, scratch); 30 | render(, scratch); 31 | 32 | expect(values).to.deep.equal([1, 2]); 33 | }); 34 | 35 | it('defaults to undefined', () => { 36 | const values = []; 37 | 38 | function Comp() { 39 | const ref = useRef(); 40 | values.push(ref.current); 41 | ref.current = 2; 42 | return null; 43 | } 44 | 45 | render(, scratch); 46 | render(, scratch); 47 | 48 | expect(values).to.deep.equal([undefined, 2]); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /benches/src/text_update.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Text Updates 7 | 8 | 9 |
10 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /compat/src/memo.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'preact'; 2 | import { shallowDiffers } from './util'; 3 | 4 | /** 5 | * Memoize a component, so that it only updates when the props actually have 6 | * changed. This was previously known as `React.pure`. 7 | * @param {import('./internal').FunctionalComponent} c functional component 8 | * @param {(prev: object, next: object) => boolean} [comparer] Custom equality function 9 | * @returns {import('./internal').FunctionalComponent} 10 | */ 11 | export function memo(c, comparer) { 12 | function shouldUpdate(nextProps) { 13 | let ref = this.props.ref; 14 | let updateRef = ref == nextProps.ref; 15 | if (!updateRef && ref) { 16 | ref.call ? ref(null) : (ref.current = null); 17 | } 18 | 19 | if (!comparer) { 20 | return shallowDiffers(this.props, nextProps); 21 | } 22 | 23 | return !comparer(this.props, nextProps) || !updateRef; 24 | } 25 | 26 | function Memoed(props) { 27 | this.shouldComponentUpdate = shouldUpdate; 28 | return createElement(c, props); 29 | } 30 | Memoed.displayName = 'Memo(' + (c.displayName || c.name) + ')'; 31 | Memoed.prototype.isReactComponent = true; 32 | Memoed._forwarded = true; 33 | return Memoed; 34 | } 35 | -------------------------------------------------------------------------------- /src/clone-element.js: -------------------------------------------------------------------------------- 1 | import { assign } from './util'; 2 | import { createVNode } from './create-element'; 3 | 4 | /** 5 | * Clones the given VNode, optionally adding attributes/props and replacing its children. 6 | * @param {import('./internal').VNode} vnode The virtual DOM element to clone 7 | * @param {object} props Attributes/props to add when cloning 8 | * @param {Array} rest Any additional arguments will be used as replacement children. 9 | * @returns {import('./internal').VNode} 10 | */ 11 | export function cloneElement(vnode, props, children) { 12 | let normalizedProps = assign({}, vnode.props), 13 | key, 14 | ref, 15 | i; 16 | for (i in props) { 17 | if (i == 'key') key = props[i]; 18 | else if (i == 'ref') ref = props[i]; 19 | else normalizedProps[i] = props[i]; 20 | } 21 | 22 | if (arguments.length > 3) { 23 | children = [children]; 24 | for (i = 3; i < arguments.length; i++) { 25 | children.push(arguments[i]); 26 | } 27 | } 28 | if (children != null) { 29 | normalizedProps.children = children; 30 | } 31 | 32 | return createVNode( 33 | vnode.type, 34 | normalizedProps, 35 | key || vnode.key, 36 | ref || vnode.ref, 37 | null 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /test/shared/isValidElementTests.js: -------------------------------------------------------------------------------- 1 | /** @jsx createElement */ 2 | 3 | export function isValidElementTests( 4 | expect, 5 | isValidElement, 6 | createElement, 7 | Component 8 | ) { 9 | describe('isValidElement', () => { 10 | it('should check if the argument is a valid vnode', () => { 11 | // Failure cases 12 | expect(isValidElement(123)).to.equal(false); 13 | expect(isValidElement(0)).to.equal(false); 14 | expect(isValidElement('')).to.equal(false); 15 | expect(isValidElement('abc')).to.equal(false); 16 | expect(isValidElement(null)).to.equal(false); 17 | expect(isValidElement(undefined)).to.equal(false); 18 | expect(isValidElement(true)).to.equal(false); 19 | expect(isValidElement(false)).to.equal(false); 20 | expect(isValidElement([])).to.equal(false); 21 | expect(isValidElement([123])).to.equal(false); 22 | expect(isValidElement([null])).to.equal(false); 23 | 24 | // Success cases 25 | expect(isValidElement(
)).to.equal(true); 26 | 27 | const Foo = () => 123; 28 | expect(isValidElement()).to.equal(true); 29 | class Bar extends Component { 30 | render() { 31 | return
; 32 | } 33 | } 34 | expect(isValidElement()).to.equal(true); 35 | }); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /demo/todo.js: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from 'preact'; 2 | 3 | let counter = 0; 4 | 5 | export default class TodoList extends Component { 6 | state = { todos: [], text: '' }; 7 | 8 | setText = e => { 9 | this.setState({ text: e.target.value }); 10 | }; 11 | 12 | addTodo = () => { 13 | let { todos, text } = this.state; 14 | todos = todos.concat({ text, id: ++counter }); 15 | this.setState({ todos, text: '' }); 16 | }; 17 | 18 | removeTodo = e => { 19 | let id = e.target.getAttribute('data-id'); 20 | this.setState({ todos: this.state.todos.filter(t => t.id != id) }); 21 | }; 22 | 23 | render({}, { todos, text }) { 24 | return ( 25 |
26 | 27 | 28 |
    29 | 30 |
31 |
32 | ); 33 | } 34 | } 35 | 36 | class TodoItems extends Component { 37 | render({ todos, removeTodo }) { 38 | return todos.map(todo => ( 39 |
  • 40 | {' '} 43 | {todo.text} 44 |
  • 45 | )); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/saucelabs.yml: -------------------------------------------------------------------------------- 1 | name: Saucelabs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build_test: 10 | name: Build & Test 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 1 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: '14.x' 19 | - name: Cache node modules 20 | uses: actions/cache@v1 21 | env: 22 | cache-name: cache-node-modules 23 | with: 24 | path: ~/.npm 25 | # This uses the same name as the build-action so we can share the caches. 26 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 27 | restore-keys: | 28 | ${{ runner.os }}-build-${{ env.cache-name }}- 29 | ${{ runner.os }}-build- 30 | ${{ runner.os }}- 31 | - run: npm ci 32 | - name: test 33 | env: 34 | CI: true 35 | COVERAGE: true 36 | FLAKEY: false 37 | SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} 38 | SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} 39 | # Not using `npm test` since it rebuilds source which npm ci has already done 40 | run: npm run test:unit 41 | -------------------------------------------------------------------------------- /demo/redux.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'preact'; 2 | import React from 'react'; 3 | import { createStore } from 'redux'; 4 | import { connect, Provider } from 'react-redux'; 5 | 6 | const store = createStore((state = { value: 0 }, action) => { 7 | switch (action.type) { 8 | case 'increment': 9 | return { value: state.value + 1 }; 10 | case 'decrement': 11 | return { value: state.value - 1 }; 12 | default: 13 | return state; 14 | } 15 | }); 16 | 17 | class Child extends React.Component { 18 | render() { 19 | return ( 20 |
    21 |
    Child #1: {this.props.foo}
    22 | 23 |
    24 | ); 25 | } 26 | } 27 | const ConnectedChild = connect(store => ({ foo: store.value }))(Child); 28 | 29 | class Child2 extends React.Component { 30 | render() { 31 | return
    Child #2: {this.props.foo}
    ; 32 | } 33 | } 34 | const ConnectedChild2 = connect(store => ({ foo: store.value }))(Child2); 35 | 36 | export default function Redux() { 37 | return ( 38 |
    39 |

    Counter

    40 | 41 | 42 | 43 |
    44 | 45 | 46 |
    47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /test/ts/hoc-test.tsx: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { 3 | createElement, 4 | ComponentFactory, 5 | ComponentConstructor, 6 | Component 7 | } from '../../'; 8 | import { SimpleComponent, SimpleComponentProps } from './Component-test'; 9 | 10 | export interface highlightedProps { 11 | isHighlighted: boolean; 12 | } 13 | 14 | export function highlighted( 15 | Wrappable: ComponentFactory 16 | ): ComponentConstructor { 17 | return class extends Component { 18 | constructor(props: T & highlightedProps) { 19 | super(props); 20 | } 21 | 22 | render() { 23 | let className = this.props.isHighlighted ? 'highlighted' : ''; 24 | return ( 25 |
    26 | 27 |
    28 | ); 29 | } 30 | 31 | toString() { 32 | return `Highlighted ${Wrappable.name}`; 33 | } 34 | }; 35 | } 36 | 37 | const HighlightedSimpleComponent = highlighted( 38 | SimpleComponent 39 | ); 40 | 41 | describe('hoc', () => { 42 | it('wraps the given component', () => { 43 | const highlight = new HighlightedSimpleComponent({ 44 | initialName: 'initial name', 45 | isHighlighted: true 46 | }); 47 | 48 | expect(highlight.toString()).to.eq('Highlighted SimpleComponent'); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /config/codemod-const.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | 3 | /** Find constants (identified by ALL_CAPS_DECLARATIONS), and inline them globally. 4 | * This is safe because Preact *only* uses global constants. 5 | */ 6 | export default (file, api) => { 7 | let j = api.jscodeshift, 8 | code = j(file.source), 9 | constants = {}, 10 | found = 0; 11 | 12 | code 13 | .find(j.VariableDeclaration) 14 | .filter(decl => { 15 | for (let i = decl.value.declarations.length; i--; ) { 16 | let node = decl.value.declarations[i], 17 | name = node.id && node.id.name, 18 | init = node.init; 19 | if (name && init && name.match(/^[A-Z0-9_$]+$/g) && !init.regex) { 20 | if (init.type === 'Literal') { 21 | // console.log(`Inlining constant: ${name}=${init.raw}`); 22 | found++; 23 | constants[name] = init; 24 | // remove declaration 25 | decl.value.declarations.splice(i, 1); 26 | // if it's the last, we'll remove the whole statement 27 | return !decl.value.declarations.length; 28 | } 29 | } 30 | } 31 | return false; 32 | }) 33 | .remove(); 34 | 35 | code 36 | .find(j.Identifier) 37 | .filter( 38 | path => path.value.name && constants.hasOwnProperty(path.value.name) 39 | ) 40 | .replaceWith(path => (found++, constants[path.value.name])); 41 | 42 | return found ? code.toSource({ quote: 'single' }) : null; 43 | }; 44 | -------------------------------------------------------------------------------- /compat/src/internal.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component as PreactComponent, 3 | VNode as PreactVNode, 4 | FunctionalComponent as PreactFunctionalComponent 5 | } from '../../src/internal'; 6 | import { SuspenseProps } from './suspense'; 7 | 8 | export { ComponentChildren } from '../..'; 9 | 10 | export { PreactElement } from '../../src/internal'; 11 | 12 | export interface Component

    extends PreactComponent { 13 | isReactComponent?: object; 14 | isPureReactComponent?: true; 15 | _patchedLifecycles?: true; 16 | 17 | _childDidSuspend?( 18 | error: Promise, 19 | suspendingComponent: Component, 20 | oldVNode?: VNode 21 | ): void; 22 | _suspendedComponentWillUnmount?(): void; 23 | } 24 | 25 | export interface FunctionalComponent

    26 | extends PreactFunctionalComponent

    { 27 | shouldComponentUpdate?(nextProps: Readonly

    ): boolean; 28 | _forwarded?: boolean; 29 | _patchedLifecycles?: true; 30 | } 31 | 32 | export interface VNode extends PreactVNode { 33 | $$typeof?: symbol | string; 34 | preactCompatNormalized?: boolean; 35 | } 36 | 37 | export interface SuspenseState { 38 | _suspended?: null | VNode; 39 | } 40 | 41 | export interface SuspenseComponent 42 | extends PreactComponent { 43 | _pendingSuspensionCount: number; 44 | _suspenders: Component[]; 45 | _detachOnNextRender: null | VNode; 46 | } 47 | -------------------------------------------------------------------------------- /benches/src/02_replace1k.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | replace all rows 7 | 8 | 9 | 10 |

    11 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/diff/catch-error.js: -------------------------------------------------------------------------------- 1 | // import { enqueueRender } from '../component'; 2 | 3 | /** 4 | * Find the closest error boundary to a thrown error and call it 5 | * @param {object} error The thrown value 6 | * @param {import('../internal').VNode} vnode The vnode that threw 7 | * the error that was caught (except for unmounting when this parameter 8 | * is the highest parent that was being unmounted) 9 | */ 10 | export function _catchError(error, vnode) { 11 | /** @type {import('../internal').Component} */ 12 | let component, ctor, handled; 13 | 14 | const wasHydrating = vnode._hydrating; 15 | 16 | for (; (vnode = vnode._parent); ) { 17 | if ((component = vnode._component) && !component._processingException) { 18 | try { 19 | ctor = component.constructor; 20 | 21 | if (ctor && ctor.getDerivedStateFromError != null) { 22 | component.setState(ctor.getDerivedStateFromError(error)); 23 | handled = component._dirty; 24 | } 25 | 26 | if (component.componentDidCatch != null) { 27 | component.componentDidCatch(error); 28 | handled = component._dirty; 29 | } 30 | 31 | // This is an error boundary. Mark it as having bailed out, and whether it was mid-hydration. 32 | if (handled) { 33 | vnode._hydrating = wasHydrating; 34 | return (component._pendingError = component); 35 | } 36 | } catch (e) { 37 | error = e; 38 | } 39 | } 40 | } 41 | 42 | throw error; 43 | } 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 14 | 15 | ### Reproduction 16 | 17 | 24 | 25 | ### Steps to reproduce 26 | 27 | 31 | 32 | ### Expected Behavior 33 | 34 | 35 | 36 | ### Actual Behavior 37 | 38 | 39 | -------------------------------------------------------------------------------- /compat/test/ts/suspense.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "../../src"; 2 | 3 | interface LazyProps { 4 | isProp: boolean; 5 | } 6 | 7 | const IsLazyFunctional = (props: LazyProps) => 8 |
    { 9 | props.isProp ? 10 | 'Super Lazy TRUE' : 11 | 'Super Lazy FALSE' 12 | }
    13 | 14 | const FallBack = () =>
    Still working...
    ; 15 | /** 16 | * Have to mock dynamic import as import() throws a syntax error in the test runner 17 | */ 18 | const componentPromise = new Promise<{default: typeof IsLazyFunctional}>(resolve=>{ 19 | setTimeout(()=>{ 20 | resolve({ default: IsLazyFunctional}); 21 | },800); 22 | }); 23 | 24 | /** 25 | * For usage with import: 26 | * const IsLazyComp = lazy(() => import('./lazy')); 27 | */ 28 | const IsLazyFunc = React.lazy(() => componentPromise); 29 | 30 | // Suspense using lazy component 31 | class SuspensefulFunc extends React.Component { 32 | render() { 33 | return }> 34 | } 35 | } 36 | 37 | //SuspenseList using lazy components 38 | function SuspenseListTester(props: any) { 39 | return ( 40 | 41 | }> 42 | 43 | 44 | }> 45 | 46 | 47 | 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /test/_util/bench.js: -------------------------------------------------------------------------------- 1 | global._ = require('lodash'); 2 | const Benchmark = (global.Benchmark = require('benchmark')); 3 | 4 | export default function bench(benches, callback) { 5 | return new Promise(resolve => { 6 | const suite = new Benchmark.Suite(); 7 | 8 | let i = 0; 9 | Object.keys(benches).forEach(name => { 10 | let run = benches[name]; 11 | suite.add(name, () => { 12 | run(++i); 13 | }); 14 | }); 15 | 16 | suite.on('complete', () => { 17 | const result = { 18 | fastest: suite.filter('fastest')[0], 19 | results: [], 20 | text: '' 21 | }; 22 | const useKilo = suite.filter(b => b.hz < 10000).length === 0; 23 | suite.forEach((bench, index) => { 24 | let r = { 25 | name: bench.name, 26 | slowdown: 27 | bench.name === result.fastest.name 28 | ? 0 29 | : (((result.fastest.hz - bench.hz) / result.fastest.hz) * 100) | 30 | 0, 31 | hz: bench.hz.toFixed(bench.hz < 100 ? 2 : 0), 32 | rme: bench.stats.rme.toFixed(2), 33 | size: bench.stats.sample.length, 34 | error: bench.error ? String(bench.error) : undefined 35 | }; 36 | result.text += `\n ${r.name}: ${ 37 | useKilo ? `${(r.hz / 1000) | 0} kHz` : `${r.hz} Hz` 38 | }${r.slowdown ? ` (-${r.slowdown}%)` : ''}`; 39 | result.results[index] = result.results[r.name] = r; 40 | }); 41 | resolve(result); 42 | if (callback) callback(result); 43 | }); 44 | suite.run({ async: true }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /test/_util/optionSpies.js: -------------------------------------------------------------------------------- 1 | import { options as rawOptions } from 'preact'; 2 | 3 | /** @type {import('preact/src/internal').Options} */ 4 | let options = rawOptions; 5 | 6 | let oldVNode = options.vnode; 7 | let oldEvent = options.event || (e => e); 8 | let oldAfterDiff = options.diffed; 9 | let oldUnmount = options.unmount; 10 | 11 | let oldRoot = options._root; 12 | let oldBeforeDiff = options._diff; 13 | let oldBeforeRender = options._render; 14 | let oldBeforeCommit = options._commit; 15 | let oldHook = options._hook; 16 | let oldCatchError = options._catchError; 17 | 18 | export const vnodeSpy = sinon.spy(oldVNode); 19 | export const eventSpy = sinon.spy(oldEvent); 20 | export const afterDiffSpy = sinon.spy(oldAfterDiff); 21 | export const unmountSpy = sinon.spy(oldUnmount); 22 | 23 | export const rootSpy = sinon.spy(oldRoot); 24 | export const beforeDiffSpy = sinon.spy(oldBeforeDiff); 25 | export const beforeRenderSpy = sinon.spy(oldBeforeRender); 26 | export const beforeCommitSpy = sinon.spy(oldBeforeCommit); 27 | export const hookSpy = sinon.spy(oldHook); 28 | export const catchErrorSpy = sinon.spy(oldCatchError); 29 | 30 | options.vnode = vnodeSpy; 31 | options.event = eventSpy; 32 | options.diffed = afterDiffSpy; 33 | options.unmount = unmountSpy; 34 | options._root = rootSpy; 35 | options._diff = beforeDiffSpy; 36 | options._render = beforeRenderSpy; 37 | options._commit = beforeCommitSpy; 38 | options._hook = hookSpy; 39 | options._catchError = catchErrorSpy; 40 | -------------------------------------------------------------------------------- /debug/test/browser/component-stack-2.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, Component } from 'preact'; 2 | import 'preact/debug'; 3 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 4 | 5 | /** @jsx createElement */ 6 | 7 | // This test is not part of component-stack.test.js to avoid it being 8 | // transpiled with '@babel/plugin-transform-react-jsx-source' enabled. 9 | 10 | describe('component stack', () => { 11 | /** @type {HTMLDivElement} */ 12 | let scratch; 13 | 14 | let errors = []; 15 | let warnings = []; 16 | 17 | beforeEach(() => { 18 | scratch = setupScratch(); 19 | 20 | errors = []; 21 | warnings = []; 22 | sinon.stub(console, 'error').callsFake(e => errors.push(e)); 23 | sinon.stub(console, 'warn').callsFake(w => warnings.push(w)); 24 | }); 25 | 26 | afterEach(() => { 27 | console.error.restore(); 28 | console.warn.restore(); 29 | teardown(scratch); 30 | }); 31 | 32 | it('should print a warning when "@babel/plugin-transform-react-jsx-source" is not installed', () => { 33 | function Foo() { 34 | return ; 35 | } 36 | 37 | class Thrower extends Component { 38 | constructor(props) { 39 | super(props); 40 | this.setState({ foo: 1 }); 41 | } 42 | 43 | render() { 44 | return
    foo
    ; 45 | } 46 | } 47 | 48 | render(, scratch); 49 | 50 | expect( 51 | warnings[0].indexOf('@babel/plugin-transform-react-jsx-source') > -1 52 | ).to.equal(true); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "main": "index.js", 4 | "scripts": { 5 | "start": "webpack-dev-server --inline" 6 | }, 7 | "devDependencies": { 8 | "@babel/core": "^7.0.0-beta.55", 9 | "@babel/plugin-proposal-class-properties": "^7.0.0-beta.55", 10 | "@babel/plugin-proposal-decorators": "^7.4.0", 11 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 12 | "@babel/plugin-transform-react-constant-elements": "^7.0.0-beta.55", 13 | "@babel/plugin-transform-react-jsx": "^7.0.0-beta.55", 14 | "@babel/plugin-transform-runtime": "^7.4.0", 15 | "@babel/preset-env": "^7.0.0-beta.55", 16 | "@babel/preset-react": "^7.0.0-beta.55", 17 | "@babel/preset-typescript": "^7.3.3", 18 | "babel-loader": "^8.0.0-beta.0", 19 | "css-loader": "2.1.1", 20 | "html-webpack-plugin": "3.2.0", 21 | "node-sass": "^4.12.0", 22 | "sass-loader": "7.1.0", 23 | "style-loader": "0.23.1", 24 | "webpack": "4.33.0", 25 | "webpack-cli": "^3.3.4", 26 | "webpack-dev-server": "^3.7.1" 27 | }, 28 | "dependencies": { 29 | "@material-ui/core": "4.9.5", 30 | "d3-scale": "^1.0.7", 31 | "d3-selection": "^1.2.0", 32 | "htm": "2.1.1", 33 | "mobx": "^5.15.4", 34 | "mobx-react": "^6.2.2", 35 | "mobx-state-tree": "^3.16.0", 36 | "preact-render-to-string": "^5.0.2", 37 | "preact-router": "^3.0.0", 38 | "react-redux": "^7.1.0", 39 | "react-router": "^5.0.1", 40 | "react-router-dom": "^5.0.1", 41 | "redux": "^4.0.1", 42 | "styled-components": "^4.2.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /compat/test/browser/createElement.test.js: -------------------------------------------------------------------------------- 1 | import React, { createElement, render } from 'preact/compat'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | import { getSymbol } from './testUtils'; 4 | 5 | describe('compat createElement()', () => { 6 | /** @type {HTMLDivElement} */ 7 | let scratch; 8 | 9 | beforeEach(() => { 10 | scratch = setupScratch(); 11 | }); 12 | 13 | afterEach(() => { 14 | teardown(scratch); 15 | }); 16 | 17 | it('should normalize vnodes', () => { 18 | let vnode = ( 19 |
    20 | t 21 |
    22 | ); 23 | 24 | const $$typeof = getSymbol('react.element', 0xeac7); 25 | expect(vnode).to.have.property('$$typeof', $$typeof); 26 | expect(vnode).to.have.property('type', 'div'); 27 | expect(vnode) 28 | .to.have.property('props') 29 | .that.is.an('object'); 30 | expect(vnode.props).to.have.property('children'); 31 | expect(vnode.props.children).to.have.property('$$typeof', $$typeof); 32 | expect(vnode.props.children).to.have.property('type', 'a'); 33 | expect(vnode.props.children) 34 | .to.have.property('props') 35 | .that.is.an('object'); 36 | expect(vnode.props.children.props).to.eql({ children: 't' }); 37 | }); 38 | 39 | it('should not normalize text nodes', () => { 40 | String.prototype.capFLetter = function() { 41 | return this.charAt(0).toUpperCase() + this.slice(1); 42 | }; 43 | let vnode =
    hi buddy
    ; 44 | 45 | render(vnode, scratch); 46 | 47 | expect(scratch.innerHTML).to.equal('
    hi buddy
    '); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /compat/test/ts/memo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from '../../src'; 2 | 3 | function ExpectType(v: T) {} // type assertion helper 4 | 5 | interface MemoProps { 6 | required: string; 7 | optional?: string; 8 | defaulted: string; 9 | } 10 | 11 | const ReadonlyBaseComponent = (props: Readonly) => ( 12 |
    {props.required + props.optional + props.defaulted}
    13 | ); 14 | ReadonlyBaseComponent.defaultProps = { defaulted: '' }; 15 | 16 | const BaseComponent = (props: MemoProps) => ( 17 |
    {props.required + props.optional + props.defaulted}
    18 | ); 19 | BaseComponent.defaultProps = { defaulted: '' }; 20 | 21 | // memo for readonly component with default comparison 22 | const MemoedReadonlyComponent = React.memo(ReadonlyBaseComponent); 23 | ExpectType>(MemoedReadonlyComponent); 24 | export const memoedReadonlyComponent = ( 25 | 26 | ); 27 | 28 | // memo for non-readonly component with default comparison 29 | const MemoedComponent = React.memo(BaseComponent); 30 | ExpectType>(MemoedComponent); 31 | export const memoedComponent = ; 32 | 33 | // memo with custom comparison 34 | const CustomMemoedComponent = React.memo(BaseComponent, (a, b) => { 35 | ExpectType(a); 36 | ExpectType(b); 37 | return a.required === b.required; 38 | }); 39 | ExpectType>(CustomMemoedComponent); 40 | export const customMemoedComponent = ; 41 | -------------------------------------------------------------------------------- /demo/context.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import { createElement, Component, createContext, Fragment } from 'preact'; 3 | const { Provider, Consumer } = createContext(); 4 | 5 | class ThemeProvider extends Component { 6 | state = { 7 | value: this.props.value 8 | }; 9 | 10 | onClick = () => { 11 | this.setState(prev => ({ 12 | value: 13 | prev.value === this.props.value ? this.props.next : this.props.value 14 | })); 15 | }; 16 | 17 | render() { 18 | return ( 19 |
    20 | 21 | {this.props.children} 22 |
    23 | ); 24 | } 25 | } 26 | 27 | class Child extends Component { 28 | shouldComponentUpdate() { 29 | return false; 30 | } 31 | 32 | render() { 33 | return ( 34 | <> 35 |

    (blocked update)

    36 | {this.props.children} 37 | 38 | ); 39 | } 40 | } 41 | 42 | export default class ContextDemo extends Component { 43 | render() { 44 | return ( 45 | 46 | 47 | 48 | {data => ( 49 |
    50 |

    51 | current theme: {data} 52 |

    53 | 54 | 55 | {data => ( 56 |

    57 | current sub theme: {data} 58 |

    59 | )} 60 |
    61 |
    62 |
    63 | )} 64 |
    65 |
    66 |
    67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /jsx-runtime/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export { Fragment } from '../../'; 2 | import { 3 | ComponentType, 4 | ComponentChild, 5 | ComponentChildren, 6 | VNode, 7 | Attributes 8 | } from '../../'; 9 | import { JSXInternal } from '../../src/jsx'; 10 | 11 | export function jsx( 12 | type: string, 13 | props: JSXInternal.HTMLAttributes & 14 | JSXInternal.SVGAttributes & 15 | Record & { children?: ComponentChild }, 16 | key?: string 17 | ): VNode; 18 | export function jsx

    ( 19 | type: ComponentType

    , 20 | props: Attributes & P & { children?: ComponentChild }, 21 | key?: string 22 | ): VNode; 23 | export namespace jsx { 24 | export import JSX = JSXInternal; 25 | } 26 | 27 | export function jsxs( 28 | type: string, 29 | props: JSXInternal.HTMLAttributes & 30 | JSXInternal.SVGAttributes & 31 | Record & { children?: ComponentChild[] }, 32 | key?: string 33 | ): VNode; 34 | export function jsxs

    ( 35 | type: ComponentType

    , 36 | props: Attributes & P & { children?: ComponentChild[] }, 37 | key?: string 38 | ): VNode; 39 | export namespace jsxs { 40 | export import JSX = JSXInternal; 41 | } 42 | 43 | export function jsxDEV( 44 | type: string, 45 | props: JSXInternal.HTMLAttributes & 46 | JSXInternal.SVGAttributes & 47 | Record & { children?: ComponentChildren }, 48 | key?: string 49 | ): VNode; 50 | export function jsxDEV

    ( 51 | type: ComponentType

    , 52 | props: Attributes & P & { children?: ComponentChildren }, 53 | key?: string 54 | ): VNode; 55 | export namespace jsxDEV { 56 | export import JSX = JSXInternal; 57 | } 58 | -------------------------------------------------------------------------------- /debug/src/check-props.js: -------------------------------------------------------------------------------- 1 | const ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'; 2 | 3 | let loggedTypeFailures = {}; 4 | 5 | /** 6 | * Reset the history of which prop type warnings have been logged. 7 | */ 8 | export function resetPropWarnings() { 9 | loggedTypeFailures = {}; 10 | } 11 | 12 | /** 13 | * Assert that the values match with the type specs. 14 | * Error messages are memorized and will only be shown once. 15 | * 16 | * Adapted from https://github.com/facebook/prop-types/blob/master/checkPropTypes.js 17 | * 18 | * @param {object} typeSpecs Map of name to a ReactPropType 19 | * @param {object} values Runtime values that need to be type-checked 20 | * @param {string} location e.g. "prop", "context", "child context" 21 | * @param {string} componentName Name of the component for error messages. 22 | * @param {?Function} getStack Returns the component stack. 23 | */ 24 | export function checkPropTypes( 25 | typeSpecs, 26 | values, 27 | location, 28 | componentName, 29 | getStack 30 | ) { 31 | Object.keys(typeSpecs).forEach(typeSpecName => { 32 | let error; 33 | try { 34 | error = typeSpecs[typeSpecName]( 35 | values, 36 | typeSpecName, 37 | componentName, 38 | location, 39 | null, 40 | ReactPropTypesSecret 41 | ); 42 | } catch (e) { 43 | error = e; 44 | } 45 | if (error && !(error.message in loggedTypeFailures)) { 46 | loggedTypeFailures[error.message] = true; 47 | console.error( 48 | `Failed ${location} type: ${error.message}${(getStack && 49 | `\n${getStack()}`) || 50 | ''}` 51 | ); 52 | } 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /demo/reduxUpdate.js: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from 'preact'; 2 | import { connect, Provider } from 'react-redux'; 3 | import { createStore } from 'redux'; 4 | import { HashRouter, Route, Link } from 'react-router-dom'; 5 | 6 | const store = createStore( 7 | (state, action) => ({ ...state, display: action.display }), 8 | { display: false } 9 | ); 10 | 11 | function _Redux({ showMe, counter }) { 12 | if (!showMe) return null; 13 | return

    showMe {counter}
    ; 14 | } 15 | const Redux = connect( 16 | state => console.log('injecting', state.display) || { showMe: state.display } 17 | )(_Redux); 18 | 19 | let display = false; 20 | class Test extends Component { 21 | componentDidUpdate(prevProps) { 22 | if (this.props.start != prevProps.start) { 23 | this.setState({ f: (this.props.start || 0) + 1 }); 24 | setTimeout(() => this.setState({ i: (this.state.i || 0) + 1 })); 25 | } 26 | } 27 | 28 | render() { 29 | const { f } = this.state; 30 | return ( 31 |
    32 | 40 | Click me 41 | 42 | 43 |
    44 | ); 45 | } 46 | } 47 | 48 | function App() { 49 | return ( 50 | 51 | 52 | } 55 | /> 56 | 57 | 58 | ); 59 | } 60 | 61 | export default App; 62 | -------------------------------------------------------------------------------- /demo/people/profile.tsx: -------------------------------------------------------------------------------- 1 | import { computed, observable } from "mobx" 2 | import { observer } from "mobx-react" 3 | import { Component, h } from "preact" 4 | import { RouteChildProps } from "./router" 5 | import { store } from "./store" 6 | 7 | export type ProfileProps = RouteChildProps 8 | @observer 9 | export class Profile extends Component { 10 | @observable id = "" 11 | @observable busy = false 12 | 13 | componentDidMount() { 14 | this.id = this.props.route 15 | } 16 | 17 | componentWillReceiveProps(props: ProfileProps) { 18 | this.id = props.route 19 | } 20 | 21 | render() { 22 | const user = this.user 23 | if (user == null) return null 24 | return ( 25 |
    26 | 27 |

    28 | {user.name.first} {user.name.last} 29 |

    30 |
    31 |

    32 | {user.gender === "female" ? "👩" : "👨"} {user.id} 33 |

    34 |

    🖂 {user.email}

    35 |
    36 |

    37 | 44 |

    45 |
    46 | ) 47 | } 48 | 49 | @computed get user() { 50 | return store.users.find(u => u.id === this.id) 51 | } 52 | 53 | remove = async () => { 54 | this.busy = true 55 | await new Promise(cb => setTimeout(cb, 1500)) 56 | store.deleteUser(this.id) 57 | this.busy = false 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /config/codemod-strip-tdz.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | 3 | // parent node types that we don't want to remove pointless initializations from (because it breaks hoisting) 4 | const BLOCKED = ['ForStatement', 'WhileStatement']; // 'IfStatement', 'SwitchStatement' 5 | 6 | /** Removes var initialization to `void 0`, which Babel adds for TDZ strictness. */ 7 | export default (file, api) => { 8 | let { jscodeshift } = api, 9 | found = 0; 10 | 11 | let code = jscodeshift(file.source) 12 | .find(jscodeshift.VariableDeclaration) 13 | .forEach(handleDeclaration); 14 | 15 | function handleDeclaration(decl) { 16 | let p = decl, 17 | remove = true; 18 | 19 | while ((p = p.parentPath)) { 20 | if (~BLOCKED.indexOf(p.value.type)) { 21 | remove = false; 22 | break; 23 | } 24 | } 25 | 26 | decl.value.declarations.filter(isPointless).forEach(node => { 27 | if (remove === false) { 28 | console.log( 29 | `> Skipping removal of undefined init for "${node.id.name}": within ${p.value.type}` 30 | ); 31 | } else { 32 | removeNodeInitialization(node); 33 | } 34 | }); 35 | } 36 | 37 | function removeNodeInitialization(node) { 38 | node.init = null; 39 | found++; 40 | } 41 | 42 | function isPointless(node) { 43 | let { init } = node; 44 | if (init) { 45 | if ( 46 | init.type === 'UnaryExpression' && 47 | init.operator === 'void' && 48 | init.argument.value == 0 49 | ) { 50 | return true; 51 | } 52 | if (init.type === 'Identifier' && init.name === 'undefined') { 53 | return true; 54 | } 55 | } 56 | return false; 57 | } 58 | 59 | return found ? code.toSource({ quote: 'single' }) : null; 60 | }; 61 | -------------------------------------------------------------------------------- /test/_util/logCall.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Serialize an object 3 | * @param {Object} obj 4 | * @return {string} 5 | */ 6 | function serialize(obj) { 7 | if (obj instanceof Text) return '#text'; 8 | if (obj instanceof Element) return `<${obj.localName}>${obj.textContent}`; 9 | if (obj === document) return 'document'; 10 | if (typeof obj == 'string') return obj; 11 | return Object.prototype.toString.call(obj).replace(/(^\[object |\]$)/g, ''); 12 | } 13 | 14 | /** @type {string[]} */ 15 | let log = []; 16 | 17 | /** 18 | * Modify obj's original method to log calls and arguments on logger object 19 | * @template T 20 | * @param {T} obj 21 | * @param {keyof T} method 22 | */ 23 | export function logCall(obj, method) { 24 | let old = obj[method]; 25 | obj[method] = function(...args) { 26 | let c = ''; 27 | for (let i = 0; i < args.length; i++) { 28 | if (c) c += ', '; 29 | c += serialize(args[i]); 30 | } 31 | 32 | // Normalize removeChild -> remove to keep output clean and readable 33 | const operation = 34 | method != 'removeChild' 35 | ? `${serialize(this)}.${method}(${c})` 36 | : `${serialize(c)}.remove()`; 37 | log.push(operation); 38 | return old.apply(this, args); 39 | }; 40 | } 41 | 42 | /** 43 | * Return log object 44 | * @return {string[]} log 45 | */ 46 | export function getLog() { 47 | return log; 48 | } 49 | 50 | /** Clear log object */ 51 | export function clearLog() { 52 | log = []; 53 | } 54 | 55 | export function getLogSummary() { 56 | /** @type {{ [key: string]: number }} */ 57 | const summary = {}; 58 | 59 | for (let entry of log) { 60 | summary[entry] = (summary[entry] || 0) + 1; 61 | } 62 | 63 | return summary; 64 | } 65 | -------------------------------------------------------------------------------- /demo/list.js: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact'; 2 | import htm from 'htm'; 3 | import './style.css'; 4 | 5 | const html = htm.bind(h); 6 | const createRoot = parent => ({ 7 | render: v => render(v, parent) 8 | }); 9 | 10 | function List({ items, renders, useKeys, useCounts, update }) { 11 | const toggleKeys = () => update({ useKeys: !useKeys }); 12 | const toggleCounts = () => update({ useCounts: !useCounts }); 13 | const swap = () => { 14 | const u = { items: items.slice() }; 15 | u.items[1] = items[8]; 16 | u.items[8] = items[1]; 17 | update(u); 18 | }; 19 | return html` 20 |
    21 | 22 | 23 | 27 | 31 |
      32 | ${items.map( 33 | (item, i) => html` 34 |
    • 38 | ${item.name} ${useCounts ? ` (${renders} renders)` : ''} 39 |
    • 40 | ` 41 | )} 42 |
    43 |
    44 | `; 45 | } 46 | 47 | const root = createRoot(document.body); 48 | 49 | let data = { 50 | items: new Array(1000).fill(null).map((x, i) => ({ name: `Item ${i + 1}` })), 51 | renders: 0, 52 | useKeys: false, 53 | useCounts: false 54 | }; 55 | 56 | function update(partial) { 57 | if (partial) Object.assign(data, partial); 58 | data.renders++; 59 | data.update = update; 60 | root.render(List(data)); 61 | } 62 | 63 | update(); 64 | -------------------------------------------------------------------------------- /test-utils/test/shared/rerender.test.js: -------------------------------------------------------------------------------- 1 | import { options, createElement, render, Component } from 'preact'; 2 | import { teardown, setupRerender } from 'preact/test-utils'; 3 | 4 | /** @jsx createElement */ 5 | 6 | describe('setupRerender & teardown', () => { 7 | /** @type {HTMLDivElement} */ 8 | let scratch; 9 | 10 | beforeEach(() => { 11 | scratch = document.createElement('div'); 12 | }); 13 | 14 | it('should restore previous debounce', () => { 15 | let spy = (options.debounceRendering = sinon.spy()); 16 | 17 | setupRerender(); 18 | teardown(); 19 | 20 | expect(options.debounceRendering).to.equal(spy); 21 | }); 22 | 23 | it('teardown should flush the queue', () => { 24 | /** @type {() => void} */ 25 | let increment; 26 | class Counter extends Component { 27 | constructor(props) { 28 | super(props); 29 | 30 | this.state = { count: 0 }; 31 | increment = () => this.setState({ count: this.state.count + 1 }); 32 | } 33 | 34 | render() { 35 | return
    {this.state.count}
    ; 36 | } 37 | } 38 | 39 | sinon.spy(Counter.prototype, 'render'); 40 | 41 | // Setup rerender 42 | setupRerender(); 43 | 44 | // Initial render 45 | render(, scratch); 46 | expect(Counter.prototype.render).to.have.been.calledOnce; 47 | expect(scratch.innerHTML).to.equal('
    0
    '); 48 | 49 | // queue rerender 50 | increment(); 51 | expect(Counter.prototype.render).to.have.been.calledOnce; 52 | expect(scratch.innerHTML).to.equal('
    0
    '); 53 | 54 | // Pretend test forgot to call rerender. Teardown should do that 55 | teardown(); 56 | expect(Counter.prototype.render).to.have.been.calledTwice; 57 | expect(scratch.innerHTML).to.equal('
    1
    '); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/ts/refs.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createElement, 3 | Component, 4 | createRef, 5 | FunctionalComponent, 6 | Fragment, 7 | RefObject, 8 | RefCallback 9 | } from '../../'; 10 | 11 | // Test Fixtures 12 | const Foo: FunctionalComponent = () => Foo; 13 | class Bar extends Component { 14 | render() { 15 | return Bar; 16 | } 17 | } 18 | 19 | // Using Refs 20 | class CallbackRef extends Component { 21 | divRef: RefCallback = div => { 22 | if (div !== null) { 23 | console.log(div.tagName); 24 | } 25 | }; 26 | fooRef: RefCallback = foo => { 27 | if (foo !== null) { 28 | console.log(foo.base); 29 | } 30 | }; 31 | barRef: RefCallback = bar => { 32 | if (bar !== null) { 33 | console.log(bar.base); 34 | } 35 | }; 36 | 37 | render() { 38 | return ( 39 | 40 |
    41 | 42 | 43 | 44 | ); 45 | } 46 | } 47 | 48 | class CreateRefComponent extends Component { 49 | private divRef: RefObject = createRef(); 50 | private fooRef: RefObject = createRef(); 51 | private barRef: RefObject = createRef(); 52 | 53 | componentDidMount() { 54 | if (this.divRef.current != null) { 55 | console.log(this.divRef.current.tagName); 56 | } 57 | 58 | if (this.fooRef.current != null) { 59 | console.log(this.fooRef.current.base); 60 | } 61 | 62 | if (this.barRef.current != null) { 63 | console.log(this.barRef.current.base); 64 | } 65 | } 66 | 67 | render() { 68 | return ( 69 | 70 |
    71 | 72 | 73 | 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /hooks/src/internal.d.ts: -------------------------------------------------------------------------------- 1 | import { Component as PreactComponent } from '../../src/internal'; 2 | import { Reducer } from '.'; 3 | 4 | export { PreactContext } from '../../src/internal'; 5 | 6 | /** 7 | * The type of arguments passed to a Hook function. While this type is not 8 | * strictly necessary, they are given a type name to make it easier to read 9 | * the following types and trace the flow of data. 10 | */ 11 | export type HookArgs = any; 12 | 13 | /** 14 | * The return type of a Hook function. While this type is not 15 | * strictly necessary, they are given a type name to make it easier to read 16 | * the following types and trace the flow of data. 17 | */ 18 | export type HookReturnValue = any; 19 | 20 | /** The public function a user invokes to use a Hook */ 21 | export type Hook = (...args: HookArgs[]) => HookReturnValue; 22 | 23 | // Hook tracking 24 | 25 | export interface ComponentHooks { 26 | /** The list of hooks a component uses */ 27 | _list: HookState[]; 28 | /** List of Effects to be invoked after the next frame is rendered */ 29 | _pendingEffects: EffectHookState[]; 30 | } 31 | 32 | export interface Component extends PreactComponent { 33 | __hooks?: ComponentHooks; 34 | } 35 | 36 | export type HookState = EffectHookState | MemoHookState | ReducerHookState; 37 | 38 | export type Effect = () => void | Cleanup; 39 | export type Cleanup = () => void; 40 | 41 | export interface EffectHookState { 42 | _value?: Effect; 43 | _args?: any[]; 44 | _cleanup?: Cleanup; 45 | } 46 | 47 | export interface MemoHookState { 48 | _value?: any; 49 | _args?: any[]; 50 | _factory?: () => any; 51 | } 52 | 53 | export interface ReducerHookState { 54 | _value?: any; 55 | _component?: Component; 56 | _reducer?: Reducer; 57 | } 58 | -------------------------------------------------------------------------------- /jsx-runtime/src/index.js: -------------------------------------------------------------------------------- 1 | import { options, Fragment } from 'preact'; 2 | 3 | /** @typedef {import('preact').VNode} VNode */ 4 | 5 | /** 6 | * @fileoverview 7 | * This file exports various methods that implement Babel's "automatic" JSX runtime API: 8 | * - jsx(type, props, key) 9 | * - jsxs(type, props, key) 10 | * - jsxDEV(type, props, key, __source, __self) 11 | * 12 | * The implementation of createVNode here is optimized for performance. 13 | * Benchmarks: https://esbench.com/bench/5f6b54a0b4632100a7dcd2b3 14 | */ 15 | 16 | /** 17 | * JSX.Element factory used by Babel's {runtime:"automatic"} JSX transform 18 | * @param {VNode['type']} type 19 | * @param {VNode['props']} props 20 | * @param {VNode['key']} [key] 21 | * @param {string} [__source] 22 | * @param {string} [__self] 23 | */ 24 | function createVNode(type, props, key, __source, __self) { 25 | const vnode = { 26 | type, 27 | props, 28 | key, 29 | ref: props && props.ref, 30 | _children: null, 31 | _parent: null, 32 | _depth: 0, 33 | _dom: null, 34 | _nextDom: undefined, 35 | _component: null, 36 | _hydrating: null, 37 | constructor: undefined, 38 | _original: undefined, 39 | __source, 40 | __self 41 | }; 42 | vnode._original = vnode; 43 | 44 | // If a Component VNode, check for and apply defaultProps. 45 | // Note: `type` is often a String, and can be `undefined` in development. 46 | let defaults, i; 47 | if (typeof type === 'function' && (defaults = type.defaultProps)) { 48 | for (i in defaults) if (props[i] === undefined) props[i] = defaults[i]; 49 | } 50 | 51 | if (options.vnode) options.vnode(vnode); 52 | return vnode; 53 | } 54 | 55 | export { 56 | createVNode as jsx, 57 | createVNode as jsxs, 58 | createVNode as jsxDEV, 59 | Fragment 60 | }; 61 | -------------------------------------------------------------------------------- /demo/mobx.js: -------------------------------------------------------------------------------- 1 | import React, { createElement, forwardRef, useRef, useState } from 'react'; 2 | import { decorate, observable } from 'mobx'; 3 | import { observer, useObserver } from 'mobx-react'; 4 | import 'mobx-react-lite/batchingForReactDom'; 5 | 6 | class Todo { 7 | constructor() { 8 | this.id = Math.random(); 9 | this.title = 'initial'; 10 | this.finished = false; 11 | } 12 | } 13 | decorate(Todo, { 14 | title: observable, 15 | finished: observable 16 | }); 17 | 18 | const Forward = observer( 19 | // eslint-disable-next-line react/display-name 20 | forwardRef(({ todo }, ref) => { 21 | return ( 22 |

    23 | Forward: "{todo.title}" {'' + todo.finished} 24 |

    25 | ); 26 | }) 27 | ); 28 | 29 | const todo = new Todo(); 30 | 31 | const TodoView = observer(({ todo }) => { 32 | return ( 33 |

    34 | Todo View: "{todo.title}" {'' + todo.finished} 35 |

    36 | ); 37 | }); 38 | 39 | const HookView = ({ todo }) => { 40 | return useObserver(() => { 41 | return ( 42 |

    43 | Todo View: "{todo.title}" {'' + todo.finished} 44 |

    45 | ); 46 | }); 47 | }; 48 | 49 | export function MobXDemo() { 50 | const ref = useRef(null); 51 | let [v, set] = useState(0); 52 | 53 | const success = ref.current && ref.current.nodeName === 'P'; 54 | 55 | return ( 56 |
    57 | { 61 | todo.title = e.target.value; 62 | set(v + 1); 63 | }} 64 | /> 65 |

    66 | 67 | {success ? 'SUCCESS' : 'FAIL'} 68 | 69 |

    70 | 71 | 72 | 73 |
    74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /compat/test/browser/textarea.test.js: -------------------------------------------------------------------------------- 1 | import React, { render, useState } from 'preact/compat'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | import { act } from 'preact/test-utils'; 4 | 5 | describe('Textarea', () => { 6 | let scratch; 7 | 8 | beforeEach(() => { 9 | scratch = setupScratch(); 10 | }); 11 | 12 | afterEach(() => { 13 | teardown(scratch); 14 | }); 15 | 16 | it('should alias value to children', () => { 17 | render('); 38 | 39 | act(() => { 40 | set('hello'); 41 | }); 42 | // Note: This looks counterintuitive, but it's working correctly - the value 43 | // missing from HTML because innerHTML doesn't serialize form field values. 44 | // See demo: https://jsfiddle.net/4had2Lu8 45 | // Related renderToString PR: preactjs/preact-render-to-string#161 46 | expect(scratch.innerHTML).to.equal(''); 47 | expect(scratch.firstElementChild.value).to.equal('hello'); 48 | 49 | act(() => { 50 | set(''); 51 | }); 52 | expect(scratch.innerHTML).to.equal(''); 53 | expect(scratch.firstElementChild.value).to.equal(''); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /hooks/test/browser/useDebugValue.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, options } from 'preact'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | import { useDebugValue, useState } from 'preact/hooks'; 4 | 5 | /** @jsx createElement*/ 6 | 7 | describe('useDebugValue', () => { 8 | /** @type {HTMLDivElement} */ 9 | let scratch; 10 | 11 | beforeEach(() => { 12 | scratch = setupScratch(); 13 | }); 14 | 15 | afterEach(() => { 16 | teardown(scratch); 17 | delete options.useDebugValue; 18 | }); 19 | 20 | it('should do nothing when no options hook is present', () => { 21 | function useFoo() { 22 | useDebugValue('foo'); 23 | return useState(0); 24 | } 25 | 26 | function App() { 27 | let [v] = useFoo(); 28 | return
    {v}
    ; 29 | } 30 | 31 | expect(() => render(, scratch)).to.not.throw(); 32 | }); 33 | 34 | it('should call options hook with value', () => { 35 | let spy = (options.useDebugValue = sinon.spy()); 36 | 37 | function useFoo() { 38 | useDebugValue('foo'); 39 | return useState(0); 40 | } 41 | 42 | function App() { 43 | let [v] = useFoo(); 44 | return
    {v}
    ; 45 | } 46 | 47 | render(, scratch); 48 | 49 | expect(spy).to.be.calledOnce; 50 | expect(spy).to.be.calledWith('foo'); 51 | }); 52 | 53 | it('should apply optional formatter', () => { 54 | let spy = (options.useDebugValue = sinon.spy()); 55 | 56 | function useFoo() { 57 | useDebugValue('foo', x => x + 'bar'); 58 | return useState(0); 59 | } 60 | 61 | function App() { 62 | let [v] = useFoo(); 63 | return
    {v}
    ; 64 | } 65 | 66 | render(, scratch); 67 | 68 | expect(spy).to.be.calledOnce; 69 | expect(spy).to.be.calledWith('foobar'); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /benches/src/03_update10th1k_x16.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | partial update 7 | 11 | 12 | 13 |
    14 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /test/browser/select.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render } from 'preact'; 2 | import { setupScratch, teardown } from '../_util/helpers'; 3 | 4 | /** @jsx createElement */ 5 | 6 | describe('Select', () => { 7 | let scratch; 8 | 9 | beforeEach(() => { 10 | scratch = setupScratch(); 11 | }); 12 | 13 | afterEach(() => { 14 | teardown(scratch); 15 | }); 16 | 17 | it('should set 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | render(, scratch); 29 | expect(scratch.firstChild.value).to.equal('B'); 30 | }); 31 | 32 | it('should set value with selected', () => { 33 | function App() { 34 | return ( 35 | 42 | ); 43 | } 44 | 45 | render(, scratch); 46 | expect(scratch.firstChild.value).to.equal('B'); 47 | }); 48 | 49 | it('should work with multiple selected', () => { 50 | function App() { 51 | return ( 52 | 61 | ); 62 | } 63 | 64 | render(, scratch); 65 | Array.prototype.slice.call(scratch.firstChild.childNodes).forEach(node => { 66 | if (node.value === 'B' || node.value === 'C') { 67 | expect(node.selected).to.equal(true); 68 | } 69 | }); 70 | expect(scratch.firstChild.value).to.equal('B'); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /debug/test/browser/serializeVNode.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from 'preact'; 2 | import { serializeVNode } from '../../src/debug'; 3 | 4 | /** @jsx createElement */ 5 | 6 | describe('serializeVNode', () => { 7 | it("should prefer a function component's displayName", () => { 8 | function Foo() { 9 | return
    ; 10 | } 11 | Foo.displayName = 'Bar'; 12 | 13 | expect(serializeVNode()).to.equal(''); 14 | }); 15 | 16 | it("should prefer a class component's displayName", () => { 17 | class Bar extends Component { 18 | render() { 19 | return
    ; 20 | } 21 | } 22 | Bar.displayName = 'Foo'; 23 | 24 | expect(serializeVNode()).to.equal(''); 25 | }); 26 | 27 | it('should serialize vnodes without children', () => { 28 | expect(serializeVNode(
    )).to.equal('
    '); 29 | }); 30 | 31 | it('should serialize vnodes with children', () => { 32 | expect(serializeVNode(
    Hello World
    )).to.equal('
    ..
    '); 33 | }); 34 | 35 | it('should serialize components', () => { 36 | function Foo() { 37 | return
    ; 38 | } 39 | expect(serializeVNode()).to.equal(''); 40 | }); 41 | 42 | it('should serialize props', () => { 43 | expect(serializeVNode(
    )).to.equal('
    '); 44 | 45 | let noop = () => {}; 46 | expect(serializeVNode(
    )).to.equal( 47 | '
    ' 48 | ); 49 | 50 | function Foo(props) { 51 | return props.foo; 52 | } 53 | 54 | expect(serializeVNode()).to.equal( 55 | '' 56 | ); 57 | 58 | expect(serializeVNode(
    )).to.equal( 59 | '
    ' 60 | ); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /compat/src/forwardRef.js: -------------------------------------------------------------------------------- 1 | import { options } from 'preact'; 2 | import { assign } from './util'; 3 | 4 | let oldDiffHook = options._diff; 5 | options._diff = vnode => { 6 | if (vnode.type && vnode.type._forwarded && vnode.ref) { 7 | vnode.props.ref = vnode.ref; 8 | vnode.ref = null; 9 | } 10 | if (oldDiffHook) oldDiffHook(vnode); 11 | }; 12 | 13 | export const REACT_FORWARD_SYMBOL = 14 | (typeof Symbol != 'undefined' && 15 | Symbol.for && 16 | Symbol.for('react.forward_ref')) || 17 | 0xf47; 18 | 19 | /** 20 | * Pass ref down to a child. This is mainly used in libraries with HOCs that 21 | * wrap components. Using `forwardRef` there is an easy way to get a reference 22 | * of the wrapped component instead of one of the wrapper itself. 23 | * @param {import('./index').ForwardFn} fn 24 | * @returns {import('./internal').FunctionalComponent} 25 | */ 26 | export function forwardRef(fn) { 27 | // We always have ref in props.ref, except for 28 | // mobx-react. It will call this function directly 29 | // and always pass ref as the second argument. 30 | function Forwarded(props, ref) { 31 | let clone = assign({}, props); 32 | delete clone.ref; 33 | ref = props.ref || ref; 34 | return fn( 35 | clone, 36 | !ref || (typeof ref === 'object' && !('current' in ref)) ? null : ref 37 | ); 38 | } 39 | 40 | // mobx-react checks for this being present 41 | Forwarded.$$typeof = REACT_FORWARD_SYMBOL; 42 | // mobx-react heavily relies on implementation details. 43 | // It expects an object here with a `render` property, 44 | // and prototype.render will fail. Without this 45 | // mobx-react throws. 46 | Forwarded.render = Forwarded; 47 | 48 | Forwarded.prototype.isReactComponent = Forwarded._forwarded = true; 49 | Forwarded.displayName = 'ForwardRef(' + (fn.displayName || fn.name) + ')'; 50 | return Forwarded; 51 | } 52 | -------------------------------------------------------------------------------- /compat/test/browser/findDOMNode.test.js: -------------------------------------------------------------------------------- 1 | import React, { createElement, findDOMNode } from 'preact/compat'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | 4 | describe('findDOMNode()', () => { 5 | /** @type {HTMLDivElement} */ 6 | let scratch; 7 | 8 | class Helper extends React.Component { 9 | render({ something }) { 10 | if (something == null) return null; 11 | if (something === false) return null; 12 | return
    ; 13 | } 14 | } 15 | 16 | beforeEach(() => { 17 | scratch = setupScratch(); 18 | }); 19 | 20 | afterEach(() => { 21 | teardown(scratch); 22 | }); 23 | 24 | it.skip('should return DOM Node if render is not false nor null', () => { 25 | const helper = React.render(, scratch); 26 | expect(findDOMNode(helper)).to.be.instanceof(Node); 27 | }); 28 | 29 | it('should return null if given null', () => { 30 | expect(findDOMNode(null)).to.be.null; 31 | }); 32 | 33 | it('should return a regular DOM Element if given a regular DOM Element', () => { 34 | let scratch = document.createElement('div'); 35 | expect(findDOMNode(scratch)).to.equalNode(scratch); 36 | }); 37 | 38 | // NOTE: React.render() returning false or null has the component pointing 39 | // to no DOM Node, in contrast, Preact always render an empty Text DOM Node. 40 | it('should return null if render returns false', () => { 41 | const helper = React.render(, scratch); 42 | expect(findDOMNode(helper)).to.be.null; 43 | }); 44 | 45 | // NOTE: React.render() returning false or null has the component pointing 46 | // to no DOM Node, in contrast, Preact always render an empty Text DOM Node. 47 | it('should return null if render returns null', () => { 48 | const helper = React.render(, scratch); 49 | expect(findDOMNode(helper)).to.be.null; 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /debug/test/browser/debug-compat.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, createRef } from 'preact'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | import './fakeDevTools'; 4 | import 'preact/debug'; 5 | import * as PropTypes from 'prop-types'; 6 | 7 | // eslint-disable-next-line no-duplicate-imports 8 | import { resetPropWarnings } from 'preact/debug'; 9 | import { forwardRef } from 'preact/compat'; 10 | 11 | const h = createElement; 12 | /** @jsx createElement */ 13 | 14 | describe('debug compat', () => { 15 | let scratch; 16 | let errors = []; 17 | let warnings = []; 18 | 19 | beforeEach(() => { 20 | errors = []; 21 | warnings = []; 22 | scratch = setupScratch(); 23 | sinon.stub(console, 'error').callsFake(e => errors.push(e)); 24 | sinon.stub(console, 'warn').callsFake(w => warnings.push(w)); 25 | }); 26 | 27 | afterEach(() => { 28 | /** @type {*} */ 29 | (console.error).restore(); 30 | console.warn.restore(); 31 | teardown(scratch); 32 | }); 33 | 34 | describe('PropTypes', () => { 35 | beforeEach(() => { 36 | resetPropWarnings(); 37 | }); 38 | 39 | it('should not fail if ref is passed to comp wrapped in forwardRef', () => { 40 | // This test ensures compat with airbnb/prop-types-exact, mui exact prop types util, etc. 41 | 42 | const Foo = forwardRef(function Foo(props, ref) { 43 | return

    {props.text}

    ; 44 | }); 45 | 46 | Foo.propTypes = { 47 | text: PropTypes.string.isRequired, 48 | ref(props) { 49 | if ('ref' in props) { 50 | throw new Error( 51 | 'ref should not be passed to prop-types valiation!' 52 | ); 53 | } 54 | } 55 | }; 56 | 57 | const ref = createRef(); 58 | 59 | render(, scratch); 60 | 61 | expect(console.error).not.been.called; 62 | 63 | expect(ref.current).to.not.be.undefined; 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /benches/scripts/bench.js: -------------------------------------------------------------------------------- 1 | import { spawnSync } from 'child_process'; 2 | import { mkdir } from 'fs/promises'; 3 | import { 4 | globSrc, 5 | benchesRoot, 6 | allBenches, 7 | resultsPath, 8 | IS_CI 9 | } from './utils.js'; 10 | import { generateConfig } from './config.js'; 11 | 12 | export const defaultBenchOptions = { 13 | browser: 'chrome-headless', 14 | // Tachometer default is 50, but locally let's only do 10 15 | 'sample-size': !IS_CI ? 10 : 50, 16 | // Tachometer default is 10% but let's do 5% to save some GitHub action 17 | // minutes by reducing the likelihood of needing auto-sampling. See 18 | // https://github.com/Polymer/tachometer#auto-sampling 19 | horizon: '5%', 20 | // Tachometer default is 3 minutes, but let's shrink it to 1 here to save some 21 | // GitHub Action minutes 22 | timeout: 1, 23 | 'window-size': '1024,768', 24 | framework: IS_CI ? ['preact-master', 'preact-local'] : null 25 | }; 26 | 27 | /** 28 | * @param {string} bench1 29 | * @param {{ _: string[]; } & TachometerOptions} opts 30 | */ 31 | export async function runBenches(bench1 = 'all', opts) { 32 | const globs = bench1 === 'all' ? allBenches : [bench1].concat(opts._); 33 | const benchesToRun = await globSrc(globs); 34 | const configFileTasks = benchesToRun.map(async benchPath => { 35 | return generateConfig(benchesRoot('src', benchPath), opts); 36 | }); 37 | 38 | await mkdir(resultsPath(), { recursive: true }); 39 | 40 | const configFiles = await Promise.all(configFileTasks); 41 | for (const { name, configPath } of configFiles) { 42 | const args = [ 43 | benchesRoot('node_modules/tachometer/bin/tach.js'), 44 | '--config', 45 | configPath, 46 | '--json-file', 47 | benchesRoot('results', name + '.json') 48 | ]; 49 | 50 | console.log('\n$', process.execPath, ...args); 51 | 52 | spawnSync(process.execPath, args, { 53 | cwd: benchesRoot(), 54 | stdio: 'inherit' 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/browser/lifecycles/componentWillUnmount.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, Component } from 'preact'; 2 | import { setupScratch, teardown, spyAll } from '../../_util/helpers'; 3 | 4 | /** @jsx createElement */ 5 | 6 | describe('Lifecycle methods', () => { 7 | /** @type {HTMLDivElement} */ 8 | let scratch; 9 | 10 | beforeEach(() => { 11 | scratch = setupScratch(); 12 | }); 13 | 14 | afterEach(() => { 15 | teardown(scratch); 16 | }); 17 | 18 | describe('top-level componentWillUnmount', () => { 19 | it('should invoke componentWillUnmount for top-level components', () => { 20 | class Foo extends Component { 21 | componentDidMount() {} 22 | componentWillUnmount() {} 23 | render() { 24 | return 'foo'; 25 | } 26 | } 27 | class Bar extends Component { 28 | componentDidMount() {} 29 | componentWillUnmount() {} 30 | render() { 31 | return 'bar'; 32 | } 33 | } 34 | spyAll(Foo.prototype); 35 | spyAll(Bar.prototype); 36 | 37 | render(, scratch); 38 | expect(Foo.prototype.componentDidMount, 'initial render').to.have.been 39 | .calledOnce; 40 | 41 | render(, scratch); 42 | expect(Foo.prototype.componentWillUnmount, 'when replaced').to.have.been 43 | .calledOnce; 44 | expect(Bar.prototype.componentDidMount, 'when replaced').to.have.been 45 | .calledOnce; 46 | 47 | render(
    , scratch); 48 | expect(Bar.prototype.componentWillUnmount, 'when removed').to.have.been 49 | .calledOnce; 50 | }); 51 | 52 | it('should only remove dom after componentWillUnmount was called', () => { 53 | class Foo extends Component { 54 | componentWillUnmount() { 55 | expect(document.getElementById('foo')).to.not.equal(null); 56 | } 57 | 58 | render() { 59 | return
    ; 60 | } 61 | } 62 | 63 | render(, scratch); 64 | render(null, scratch); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /jsx-runtime/test/browser/jsx-runtime.test.jsx: -------------------------------------------------------------------------------- 1 | import { Component, createElement } from 'preact'; 2 | import { jsx, jsxs, jsxDEV, Fragment } from 'preact/jsx-runtime'; 3 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 4 | 5 | describe('Babel jsx/jsxDEV', () => { 6 | let scratch; 7 | 8 | beforeEach(() => { 9 | scratch = setupScratch(); 10 | }); 11 | 12 | afterEach(() => { 13 | teardown(scratch); 14 | }); 15 | 16 | it('should have needed exports', () => { 17 | expect(typeof jsx).to.equal('function'); 18 | expect(typeof jsxs).to.equal('function'); 19 | expect(typeof jsxDEV).to.equal('function'); 20 | expect(typeof Fragment).to.equal('function'); 21 | }); 22 | 23 | it('should keep ref in props', () => { 24 | const ref = () => null; 25 | const vnode = jsx('div', { ref }); 26 | expect(vnode.ref).to.equal(ref); 27 | }); 28 | 29 | it('should add keys', () => { 30 | const vnode = jsx('div', null, 'foo'); 31 | expect(vnode.key).to.equal('foo'); 32 | }); 33 | 34 | it('should apply defaultProps', () => { 35 | class Foo extends Component { 36 | render() { 37 | return
    ; 38 | } 39 | } 40 | 41 | Foo.defaultProps = { 42 | foo: 'bar' 43 | }; 44 | 45 | const vnode = jsx(Foo, {}, null); 46 | expect(vnode.props).to.deep.equal({ 47 | foo: 'bar' 48 | }); 49 | }); 50 | 51 | it('should set __source and __self', () => { 52 | const vnode = jsx('div', { class: 'foo' }, 'key', 'source', 'self'); 53 | expect(vnode.__source).to.equal('source'); 54 | expect(vnode.__self).to.equal('self'); 55 | }); 56 | 57 | it('should return a vnode like createElement', () => { 58 | const elementVNode = createElement('div', { 59 | class: 'foo', 60 | key: 'key' 61 | }); 62 | const jsxVNode = jsx('div', { class: 'foo' }, 'key'); 63 | delete jsxVNode.__self; 64 | delete jsxVNode.__source; 65 | expect(jsxVNode).to.deep.equal(elementVNode); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /demo/stateOrderBug.js: -------------------------------------------------------------------------------- 1 | import htm from 'htm'; 2 | import { h } from 'preact'; 3 | import { useState, useCallback } from 'preact/hooks'; 4 | 5 | const html = htm.bind(h); 6 | 7 | // configuration used to show behavior vs. workaround 8 | let childFirst = true; 9 | const Config = () => html` 10 | 20 | `; 21 | 22 | const Child = ({ items, setItems }) => { 23 | let [pendingId, setPendingId] = useState(null); 24 | if (!pendingId) { 25 | setPendingId( 26 | (pendingId = Math.random() 27 | .toFixed(20) 28 | .slice(2)) 29 | ); 30 | } 31 | 32 | const onInput = useCallback( 33 | evt => { 34 | let val = evt.target.value, 35 | _items = [...items, { _id: pendingId, val }]; 36 | if (childFirst) { 37 | setPendingId(null); 38 | setItems(_items); 39 | } else { 40 | setItems(_items); 41 | setPendingId(null); 42 | } 43 | }, 44 | [childFirst, setPendingId, setItems, items, pendingId] 45 | ); 46 | 47 | return html` 48 |
    49 | ${items.map( 50 | (item, idx) => html` 51 | { 55 | let val = evt.target.value, 56 | _items = [...items]; 57 | _items.splice(idx, 1, { ...item, val }); 58 | setItems(_items); 59 | }} 60 | /> 61 | ` 62 | )} 63 | 64 | 69 |
    70 | `; 71 | }; 72 | 73 | const Parent = () => { 74 | let [items, setItems] = useState([]); 75 | return html` 76 |
    <${Config} /><${Child} items=${items} setItems=${setItems} />
    77 | `; 78 | }; 79 | 80 | export default Parent; 81 | -------------------------------------------------------------------------------- /demo/people/index.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from "mobx-react" 2 | import { Component, h } from "preact" 3 | import { Profile } from "./profile" 4 | import { Link, Route, Router } from "./router" 5 | import { store } from "./store" 6 | 7 | import "./styles/index.scss"; 8 | 9 | @observer 10 | export default class App extends Component { 11 | componentDidMount() { 12 | store.loadUsers().catch(console.error) 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 |
    19 | 50 |
    51 | 52 | 53 | 54 |
    55 |
    56 |
    57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /demo/profiler.js: -------------------------------------------------------------------------------- 1 | import { createElement, Component, options } from 'preact'; 2 | 3 | function getPrimes(max) { 4 | let sieve = [], 5 | i, 6 | j, 7 | primes = []; 8 | for (i = 2; i <= max; ++i) { 9 | if (!sieve[i]) { 10 | // i has not been marked -- it is prime 11 | primes.push(i); 12 | for (j = i << 1; j <= max; j += i) { 13 | sieve[j] = true; 14 | } 15 | } 16 | } 17 | return primes.join(''); 18 | } 19 | 20 | function Foo(props) { 21 | return
    {props.children}
    ; 22 | } 23 | 24 | function Bar() { 25 | getPrimes(10000); 26 | return ( 27 |
    28 | ...yet another component 29 |
    30 | ); 31 | } 32 | 33 | function PrimeNumber(props) { 34 | // Slow down rendering of this component 35 | getPrimes(10); 36 | 37 | return ( 38 |
    39 | I'm a slow component 40 |
    41 | {props.children} 42 |
    43 | ); 44 | } 45 | 46 | export default class ProfilerDemo extends Component { 47 | constructor() { 48 | super(); 49 | this.onClick = this.onClick.bind(this); 50 | this.state = { counter: 0 }; 51 | } 52 | 53 | componentDidMount() { 54 | options._diff = vnode => (vnode.startTime = performance.now()); 55 | options.diffed = vnode => (vnode.endTime = performance.now()); 56 | } 57 | 58 | componentWillUnmount() { 59 | delete options._diff; 60 | delete options.diffed; 61 | } 62 | 63 | onClick() { 64 | this.setState(prev => ({ counter: ++prev.counter })); 65 | } 66 | 67 | render() { 68 | return ( 69 |
    70 |

    ⚛ Preact

    71 |

    72 | Devtools Profiler integration 🕒 73 |

    74 | 75 | 76 | I'm a fast component 77 | 78 | 79 | 80 | I'm the fastest component 🎉 81 | Counter: {this.state.counter} 82 |
    83 |
    84 | 85 |
    86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /demo/nested-suspense/index.js: -------------------------------------------------------------------------------- 1 | import { createElement, Suspense, lazy, Component } from 'react'; 2 | 3 | const Loading = function() { 4 | return
    Loading...
    ; 5 | }; 6 | const Error = function({ resetState }) { 7 | return ( 8 |
    9 | Error!  10 | 11 | Reset app 12 | 13 |
    14 | ); 15 | }; 16 | 17 | const pause = timeout => 18 | new Promise(d => setTimeout(d, timeout), console.log(timeout)); 19 | 20 | const DropZone = lazy(() => 21 | pause(Math.random() * 1000).then(() => import('./dropzone.js')) 22 | ); 23 | const Editor = lazy(() => 24 | pause(Math.random() * 1000).then(() => import('./editor.js')) 25 | ); 26 | const AddNewComponent = lazy(() => 27 | pause(Math.random() * 1000).then(() => import('./addnewcomponent.js')) 28 | ); 29 | const GenerateComponents = lazy(() => 30 | pause(Math.random() * 1000).then(() => import('./component-container.js')) 31 | ); 32 | 33 | export default class App extends Component { 34 | state = { hasError: false }; 35 | 36 | static getDerivedStateFromError(error) { 37 | // Update state so the next render will show the fallback UI. 38 | console.warn(error); 39 | return { hasError: true }; 40 | } 41 | 42 | render() { 43 | return this.state.hasError ? ( 44 | this.setState({ hasError: false })} /> 45 | ) : ( 46 | }> 47 | 48 | 49 |
    50 | }> 51 | 52 | 53 | 54 |
    55 | 63 |
    64 | 65 |
    Footer here
    66 |
    67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /demo/pythagoras/index.js: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from 'preact'; 2 | import { select as d3select, mouse as d3mouse } from 'd3-selection'; 3 | import { scaleLinear } from 'd3-scale'; 4 | import Pythagoras from './pythagoras'; 5 | 6 | export default class PythagorasDemo extends Component { 7 | svg = { 8 | width: 1280, 9 | height: 600 10 | }; 11 | 12 | state = { 13 | currentMax: 0, 14 | baseW: 80, 15 | heightFactor: 0, 16 | lean: 0 17 | }; 18 | 19 | realMax = 11; 20 | 21 | svgRef = c => { 22 | this.svgElement = c; 23 | }; 24 | 25 | scaleFactor = scaleLinear() 26 | .domain([this.svg.height, 0]) 27 | .range([0, 0.8]); 28 | 29 | scaleLean = scaleLinear() 30 | .domain([0, this.svg.width / 2, this.svg.width]) 31 | .range([0.5, 0, -0.5]); 32 | 33 | onMouseMove = event => { 34 | let [x, y] = d3mouse(this.svgElement); 35 | 36 | this.setState({ 37 | heightFactor: this.scaleFactor(y), 38 | lean: this.scaleLean(x) 39 | }); 40 | }; 41 | 42 | restart = () => { 43 | this.setState({ currentMax: 0 }); 44 | this.next(); 45 | }; 46 | 47 | next = () => { 48 | let { currentMax } = this.state; 49 | 50 | if (currentMax < this.realMax) { 51 | this.setState({ currentMax: currentMax + 1 }); 52 | this.timer = setTimeout(this.next, 500); 53 | } 54 | }; 55 | 56 | componentDidMount() { 57 | this.selected = d3select(this.svgElement).on('mousemove', this.onMouseMove); 58 | this.next(); 59 | } 60 | 61 | componentWillUnmount() { 62 | this.selected.on('mousemove', null); 63 | clearTimeout(this.timer); 64 | } 65 | 66 | render({}, { currentMax, baseW, heightFactor, lean }) { 67 | let { width, height } = this.svg; 68 | 69 | return ( 70 |
    71 | 72 | 82 | 83 |
    84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /debug/test/browser/component-stack.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render, Component } from 'preact'; 2 | import 'preact/debug'; 3 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 4 | 5 | /** @jsx createElement */ 6 | 7 | describe('component stack', () => { 8 | /** @type {HTMLDivElement} */ 9 | let scratch; 10 | 11 | let errors = []; 12 | let warnings = []; 13 | 14 | const getStack = arr => arr[0].split('\n\n')[1]; 15 | 16 | beforeEach(() => { 17 | scratch = setupScratch(); 18 | 19 | errors = []; 20 | warnings = []; 21 | sinon.stub(console, 'error').callsFake(e => errors.push(e)); 22 | sinon.stub(console, 'warn').callsFake(w => warnings.push(w)); 23 | }); 24 | 25 | afterEach(() => { 26 | console.error.restore(); 27 | console.warn.restore(); 28 | teardown(scratch); 29 | }); 30 | 31 | it('should print component stack', () => { 32 | function Foo() { 33 | return ; 34 | } 35 | 36 | class Thrower extends Component { 37 | constructor(props) { 38 | super(props); 39 | this.setState({ foo: 1 }); 40 | } 41 | 42 | render() { 43 | return
    foo
    ; 44 | } 45 | } 46 | 47 | render(, scratch); 48 | 49 | let lines = getStack(warnings).split('\n'); 50 | expect(lines[0].indexOf('Thrower') > -1).to.equal(true); 51 | expect(lines[1].indexOf('Foo') > -1).to.equal(true); 52 | }); 53 | 54 | it('should only print owners', () => { 55 | function Foo(props) { 56 | return
    {props.children}
    ; 57 | } 58 | 59 | function Bar() { 60 | return ( 61 | 62 | 63 | 64 | ); 65 | } 66 | 67 | class Thrower extends Component { 68 | render() { 69 | return ( 70 | 71 | foo 73 | 74 |
    72 |
    75 | ); 76 | } 77 | } 78 | 79 | render(, scratch); 80 | 81 | let lines = getStack(errors).split('\n'); 82 | expect(lines[0].indexOf('td') > -1).to.equal(true); 83 | expect(lines[1].indexOf('Thrower') > -1).to.equal(true); 84 | expect(lines[2].indexOf('Bar') > -1).to.equal(true); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /demo/pythagoras/pythagoras.js: -------------------------------------------------------------------------------- 1 | import { interpolateViridis } from 'd3-scale'; 2 | import { createElement } from 'preact'; 3 | 4 | Math.deg = function(radians) { 5 | return radians * (180 / Math.PI); 6 | }; 7 | 8 | const memoizedCalc = (function() { 9 | const memo = {}; 10 | 11 | const key = ({ w, heightFactor, lean }) => `${w}-${heightFactor}-${lean}`; 12 | 13 | return args => { 14 | let memoKey = key(args); 15 | 16 | if (memo[memoKey]) { 17 | return memo[memoKey]; 18 | } 19 | 20 | let { w, heightFactor, lean } = args; 21 | let trigH = heightFactor * w; 22 | 23 | let result = { 24 | nextRight: Math.sqrt(trigH ** 2 + (w * (0.5 + lean)) ** 2), 25 | nextLeft: Math.sqrt(trigH ** 2 + (w * (0.5 - lean)) ** 2), 26 | A: Math.deg(Math.atan(trigH / ((0.5 - lean) * w))), 27 | B: Math.deg(Math.atan(trigH / ((0.5 + lean) * w))) 28 | }; 29 | 30 | memo[memoKey] = result; 31 | return result; 32 | }; 33 | })(); 34 | 35 | export default function Pythagoras({ 36 | w, 37 | x, 38 | y, 39 | heightFactor, 40 | lean, 41 | left, 42 | right, 43 | lvl, 44 | maxlvl 45 | }) { 46 | if (lvl >= maxlvl || w < 1) { 47 | return null; 48 | } 49 | 50 | const { nextRight, nextLeft, A, B } = memoizedCalc({ 51 | w, 52 | heightFactor, 53 | lean 54 | }); 55 | 56 | let rotate = ''; 57 | 58 | if (left) { 59 | rotate = `rotate(${-A} 0 ${w})`; 60 | } else if (right) { 61 | rotate = `rotate(${B} ${w} ${w})`; 62 | } 63 | 64 | return ( 65 | 66 | 73 | 74 | 84 | 85 | 95 | 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /compat/test/browser/compat.options.test.js: -------------------------------------------------------------------------------- 1 | import { vnodeSpy, eventSpy } from '../../../test/_util/optionSpies'; 2 | import React, { 3 | createElement, 4 | render, 5 | Component, 6 | createRef 7 | } from 'preact/compat'; 8 | import { setupRerender } from 'preact/test-utils'; 9 | import { 10 | setupScratch, 11 | teardown, 12 | createEvent 13 | } from '../../../test/_util/helpers'; 14 | 15 | describe('compat options', () => { 16 | /** @type {HTMLDivElement} */ 17 | let scratch; 18 | 19 | /** @type {() => void} */ 20 | let rerender; 21 | 22 | /** @type {() => void} */ 23 | let increment; 24 | 25 | /** @type {import('../../src/index').PropRef} */ 26 | let buttonRef; 27 | 28 | beforeEach(() => { 29 | scratch = setupScratch(); 30 | rerender = setupRerender(); 31 | 32 | vnodeSpy.resetHistory(); 33 | eventSpy.resetHistory(); 34 | 35 | buttonRef = createRef(); 36 | }); 37 | 38 | afterEach(() => { 39 | teardown(scratch); 40 | }); 41 | 42 | class ClassApp extends Component { 43 | constructor() { 44 | super(); 45 | this.state = { count: 0 }; 46 | increment = () => 47 | this.setState(({ count }) => ({ 48 | count: count + 1 49 | })); 50 | } 51 | 52 | render() { 53 | return ( 54 | 57 | ); 58 | } 59 | } 60 | 61 | it('should call old options on mount', () => { 62 | render(, scratch); 63 | 64 | expect(vnodeSpy).to.have.been.called; 65 | }); 66 | 67 | it('should call old options on event and update', () => { 68 | render(, scratch); 69 | expect(scratch.innerHTML).to.equal(''); 70 | 71 | buttonRef.current.dispatchEvent(createEvent('click')); 72 | rerender(); 73 | expect(scratch.innerHTML).to.equal(''); 74 | 75 | expect(vnodeSpy).to.have.been.called; 76 | expect(eventSpy).to.have.been.called; 77 | }); 78 | 79 | it('should call old options on unmount', () => { 80 | render(, scratch); 81 | render(null, scratch); 82 | 83 | expect(vnodeSpy).to.have.been.called; 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/polyfills.js: -------------------------------------------------------------------------------- 1 | // ES2015 APIs used by developer tools integration 2 | import 'core-js/es6/map'; 3 | import 'core-js/es6/promise'; 4 | import 'core-js/fn/array/fill'; 5 | import 'core-js/fn/array/from'; 6 | import 'core-js/fn/array/find'; 7 | import 'core-js/fn/array/includes'; 8 | import 'core-js/fn/string/includes'; 9 | import 'core-js/fn/object/assign'; 10 | import 'core-js/fn/string/starts-with'; 11 | import 'core-js/fn/string/code-point-at'; 12 | import 'core-js/fn/string/from-code-point'; 13 | import 'core-js/fn/string/repeat'; 14 | 15 | // Fix Function#name on browsers that do not support it (IE). 16 | // Taken from: https://stackoverflow.com/a/17056530/755391 17 | if (!function f() {}.name) { 18 | Object.defineProperty(Function.prototype, 'name', { 19 | get() { 20 | let name = (this.toString().match(/^function\s*([^\s(]+)/) || [])[1]; 21 | // For better performance only parse once, and then cache the 22 | // result through a new accessor for repeated access. 23 | Object.defineProperty(this, 'name', { value: name }); 24 | return name; 25 | } 26 | }); 27 | } 28 | 29 | /* global chai */ 30 | chai.use((chai, util) => { 31 | const Assertion = chai.Assertion; 32 | 33 | Assertion.addMethod('equalNode', function(expectedNode, message) { 34 | const obj = this._obj; 35 | message = message || 'equalNode'; 36 | 37 | if (expectedNode == null) { 38 | this.assert( 39 | obj == null, 40 | `${message}: expected node to "== null" but got #{act} instead.`, 41 | `${message}: expected node to not "!= null".`, 42 | expectedNode, 43 | obj 44 | ); 45 | } else { 46 | new Assertion(obj).to.be.instanceof(Node, message); 47 | this.assert( 48 | obj.tagName === expectedNode.tagName, 49 | `${message}: expected node to have tagName #{exp} but got #{act} instead.`, 50 | `${message}: expected node to not have tagName #{act}.`, 51 | expectedNode.tagName, 52 | obj.tagName 53 | ); 54 | this.assert( 55 | obj === expectedNode, 56 | `${message}: expected #{this} to be #{exp} but got #{act}`, 57 | `${message}: expected #{this} not to be #{exp}`, 58 | expectedNode, 59 | obj 60 | ); 61 | } 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /demo/suspense.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import { 3 | createElement, 4 | Component, 5 | memo, 6 | Fragment, 7 | Suspense, 8 | lazy 9 | } from 'react'; 10 | 11 | function LazyComp() { 12 | return
    I'm (fake) lazy loaded
    ; 13 | } 14 | 15 | const Lazy = lazy(() => Promise.resolve({ default: LazyComp })); 16 | 17 | function createSuspension(name, timeout, error) { 18 | let done = false; 19 | let prom; 20 | 21 | return { 22 | name, 23 | timeout, 24 | start: () => { 25 | if (!prom) { 26 | prom = new Promise((res, rej) => { 27 | setTimeout(() => { 28 | done = true; 29 | if (error) { 30 | rej(error); 31 | } else { 32 | res(); 33 | } 34 | }, timeout); 35 | }); 36 | } 37 | 38 | return prom; 39 | }, 40 | getPromise: () => prom, 41 | isDone: () => done 42 | }; 43 | } 44 | 45 | function CustomSuspense({ isDone, start, timeout, name }) { 46 | if (!isDone()) { 47 | throw start(); 48 | } 49 | 50 | return ( 51 |
    52 | Hello from CustomSuspense {name}, loaded after {timeout / 1000}s 53 |
    54 | ); 55 | } 56 | 57 | function init() { 58 | return { 59 | s1: createSuspension('1', 1000, null), 60 | s2: createSuspension('2', 2000, null), 61 | s3: createSuspension('3', 3000, null) 62 | }; 63 | } 64 | 65 | export default class DevtoolsDemo extends Component { 66 | constructor(props) { 67 | super(props); 68 | this.state = init(); 69 | this.onRerun = this.onRerun.bind(this); 70 | } 71 | 72 | onRerun() { 73 | this.setState(init()); 74 | } 75 | 76 | render(props, state) { 77 | return ( 78 |
    79 |

    lazy()

    80 | Loading (fake) lazy loaded component...
    }> 81 | 82 | 83 |

    Suspense

    84 |
    85 | 86 |
    87 | Fallback 1
    }> 88 | 89 | Fallback 2
    }> 90 | 91 | 92 | 93 | 94 |
    95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /demo/suspense-router/simple-router.js: -------------------------------------------------------------------------------- 1 | import { 2 | createElement, 3 | cloneElement, 4 | createContext, 5 | useState, 6 | useContext, 7 | Children, 8 | useLayoutEffect 9 | } from 'react'; 10 | 11 | /** @jsx createElement */ 12 | 13 | const memoryHistory = { 14 | /** 15 | * @typedef {{ pathname: string }} Location 16 | * @typedef {(location: Location) => void} HistoryListener 17 | * @type {HistoryListener[]} 18 | */ 19 | listeners: [], 20 | 21 | /** 22 | * @param {HistoryListener} listener 23 | */ 24 | listen(listener) { 25 | const newLength = this.listeners.push(listener); 26 | return () => this.listeners.splice(newLength - 1, 1); 27 | }, 28 | 29 | /** 30 | * @param {Location} to 31 | */ 32 | navigate(to) { 33 | this.listeners.forEach(listener => listener(to)); 34 | } 35 | }; 36 | 37 | /** @type {import('react').Context<{ history: typeof memoryHistory; location: Location }>} */ 38 | const RouterContext = createContext(null); 39 | 40 | export function Router({ history = memoryHistory, children }) { 41 | const [location, setLocation] = useState({ pathname: '/' }); 42 | 43 | useLayoutEffect(() => { 44 | return history.listen(newLocation => setLocation(newLocation)); 45 | }, []); 46 | 47 | return ( 48 | 49 | {children} 50 | 51 | ); 52 | } 53 | 54 | export function Switch(props) { 55 | const { location } = useContext(RouterContext); 56 | 57 | let element = null; 58 | Children.forEach(props.children, child => { 59 | if (element == null && child.props.path == location.pathname) { 60 | element = child; 61 | } 62 | }); 63 | 64 | return element; 65 | } 66 | 67 | /** 68 | * @param {{ children: any; path: string; exact?: boolean; }} props 69 | */ 70 | export function Route({ children, path, exact }) { 71 | return children; 72 | } 73 | 74 | export function Link({ to, children }) { 75 | const { history } = useContext(RouterContext); 76 | const onClick = event => { 77 | event.preventDefault(); 78 | event.stopPropagation(); 79 | history.navigate({ pathname: to }); 80 | }; 81 | 82 | return ( 83 | 84 | {children} 85 | 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /benches/scripts/utils.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url'; 2 | import { stat, readFile } from 'fs/promises'; 3 | import * as path from 'path'; 4 | import escalade from 'escalade'; 5 | import globby from 'globby'; 6 | 7 | // TODO: Replace with import.meta.resolve when stable 8 | import { createRequire } from 'module'; 9 | // @ts-ignore 10 | const require = createRequire(import.meta.url); 11 | 12 | export const IS_CI = process.env.CI === 'true'; 13 | 14 | // @ts-ignore 15 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 16 | export const repoRoot = (...args) => path.join(__dirname, '..', '..', ...args); 17 | export const benchesRoot = (...args) => repoRoot('benches', ...args); 18 | export const resultsPath = (...args) => benchesRoot('results', ...args); 19 | 20 | export const toUrl = str => str.replace(/^[A-Za-z]+:/, '/').replace(/\\/g, '/'); 21 | 22 | export const allBenches = '**/*.html'; 23 | export function globSrc(patterns) { 24 | return globby(patterns, { cwd: benchesRoot('src') }); 25 | } 26 | 27 | export async function getPkgBinPath(pkgName) { 28 | /** @type {string | void} */ 29 | let packageJsonPath; 30 | try { 31 | // TODO: Replace with import.meta.resolve when stable 32 | const pkgMainPath = require.resolve(pkgName); 33 | packageJsonPath = await escalade(pkgMainPath, (dir, names) => { 34 | if (names.includes('package.json')) { 35 | return 'package.json'; 36 | } 37 | }); 38 | } catch (e) { 39 | // Tachometer doesn't have a valid 'main' entry 40 | packageJsonPath = benchesRoot('node_modules', pkgName, 'package.json'); 41 | } 42 | 43 | if (!packageJsonPath || !(await stat(packageJsonPath)).isFile()) { 44 | throw new Error( 45 | `Could not locate "${pkgName}" package.json at "${packageJsonPath}".` 46 | ); 47 | } 48 | 49 | const pkg = JSON.parse(await readFile(packageJsonPath, 'utf8')); 50 | if (!pkg.bin) { 51 | throw new Error(`${pkgName} package.json does not contain a "bin" entry.`); 52 | } 53 | 54 | let binSubPath = pkg.bin; 55 | if (typeof pkg.bin == 'object') { 56 | binSubPath = pkg.bin[pkgName]; 57 | } 58 | 59 | const binPath = path.join(path.dirname(packageJsonPath), binSubPath); 60 | if (!(await stat(binPath)).isFile()) { 61 | throw new Error(`Bin path for ${pkgName} is not a file: ${binPath}`); 62 | } 63 | 64 | return binPath; 65 | } 66 | -------------------------------------------------------------------------------- /src/create-context.js: -------------------------------------------------------------------------------- 1 | import { enqueueRender } from './component'; 2 | 3 | export let i = 0; 4 | 5 | export function createContext(defaultValue, contextId) { 6 | contextId = '__cC' + i++; 7 | 8 | const context = { 9 | _id: contextId, 10 | _defaultValue: defaultValue, 11 | Consumer(props, contextValue) { 12 | // return props.children( 13 | // context[contextId] ? context[contextId].props.value : defaultValue 14 | // ); 15 | return props.children(contextValue); 16 | }, 17 | Provider(props, subs, ctx) { 18 | if (!this.getChildContext) { 19 | subs = []; 20 | ctx = {}; 21 | ctx[contextId] = this; 22 | 23 | this.getChildContext = () => ctx; 24 | 25 | this.shouldComponentUpdate = function(_props) { 26 | if (this.props.value !== _props.value) { 27 | // I think the forced value propagation here was only needed when `options.debounceRendering` was being bypassed: 28 | // https://github.com/preactjs/preact/commit/4d339fb803bea09e9f198abf38ca1bf8ea4b7771#diff-54682ce380935a717e41b8bfc54737f6R358 29 | // In those cases though, even with the value corrected, we're double-rendering all nodes. 30 | // It might be better to just tell folks not to use force-sync mode. 31 | // Currently, using `useContext()` in a class component will overwrite its `this.context` value. 32 | // subs.some(c => { 33 | // c.context = _props.value; 34 | // enqueueRender(c); 35 | // }); 36 | 37 | // subs.some(c => { 38 | // c.context[contextId] = _props.value; 39 | // enqueueRender(c); 40 | // }); 41 | subs.some(enqueueRender); 42 | } 43 | }; 44 | 45 | this.sub = c => { 46 | subs.push(c); 47 | let old = c.componentWillUnmount; 48 | c.componentWillUnmount = () => { 49 | subs.splice(subs.indexOf(c), 1); 50 | if (old) old.call(c); 51 | }; 52 | }; 53 | } 54 | 55 | return props.children; 56 | } 57 | }; 58 | 59 | // Devtools needs access to the context object when it 60 | // encounters a Provider. This is necessary to support 61 | // setting `displayName` on the context object instead 62 | // of on the component itself. See: 63 | // https://reactjs.org/docs/context.html#contextdisplayname 64 | 65 | return (context.Provider._contextRef = context.Consumer.contextType = context); 66 | } 67 | -------------------------------------------------------------------------------- /demo/people/styles/app.scss: -------------------------------------------------------------------------------- 1 | #people-app { 2 | position: relative; 3 | overflow: hidden; 4 | min-height: 100vh; 5 | animation: popup 300ms cubic-bezier(0.3, 0.7, 0.3, 1) forwards; 6 | background: var(--app-background); 7 | --menu-width: 260px; 8 | --menu-item-height: 50px; 9 | 10 | @media (min-width: 1280px) { 11 | max-width: 1280px; 12 | min-height: calc(100vh - 64px); 13 | margin: 32px auto; 14 | border-radius: 10px; 15 | } 16 | 17 | > nav { 18 | position: absolute; 19 | display: flow-root; 20 | width: var(--menu-width); 21 | height: 100%; 22 | background-color: var(--app-background-secondary); 23 | overflow-x: hidden; 24 | overflow-y: auto; 25 | } 26 | 27 | > nav h4 { 28 | padding-left: 16px; 29 | font-weight: normal; 30 | text-transform: uppercase; 31 | } 32 | 33 | > nav ul { 34 | position: relative; 35 | } 36 | 37 | > nav li { 38 | position: absolute; 39 | width: 100%; 40 | animation: zoom 200ms forwards; 41 | opacity: 0; 42 | transition: top 200ms; 43 | } 44 | 45 | > nav li > a { 46 | position: relative; 47 | display: flex; 48 | overflow: hidden; 49 | flex-flow: row; 50 | align-items: center; 51 | margin-left: 16px; 52 | border-right: 2px solid transparent; 53 | border-bottom-left-radius: 48px; 54 | border-top-left-radius: 48px; 55 | text-transform: capitalize; 56 | transition: border 500ms; 57 | } 58 | 59 | > nav li > a:hover { 60 | background-color: var(--app-highlight); 61 | } 62 | 63 | > nav li > a::after { 64 | position: absolute; 65 | top: 0; 66 | right: -2px; 67 | bottom: 0; 68 | left: 0; 69 | background-image: radial-gradient( 70 | circle, 71 | var(--app-ripple) 1%, 72 | transparent 1% 73 | ); 74 | background-position: center; 75 | background-repeat: no-repeat; 76 | background-size: 10000%; 77 | content: ''; 78 | opacity: 0; 79 | transition: opacity 700ms, background 300ms; 80 | } 81 | 82 | > nav li > a:active::after { 83 | background-size: 100%; 84 | opacity: 0.5; 85 | transition: none; 86 | } 87 | 88 | > nav li > a.active { 89 | border-color: var(--app-primary); 90 | background-color: var(--app-highlight); 91 | } 92 | 93 | > nav li > a > * { 94 | margin: 8px; 95 | } 96 | 97 | #people-main { 98 | padding-left: var(--menu-width); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /mangle.json: -------------------------------------------------------------------------------- 1 | { 2 | "help": { 3 | "what is this file?": "It controls protected/private property mangling so that minified builds have consistent property names.", 4 | "why are there duplicate minified properties?": "Most properties are only used on one type of objects, so they can have the same name since they will never collide. Doing this reduces size." 5 | }, 6 | "minify": { 7 | "mangle": { 8 | "properties": { 9 | "regex": "^_[^_]", 10 | "reserved": [ 11 | "__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED", 12 | "__REACT_DEVTOOLS_GLOBAL_HOOK__", 13 | "__PREACT_DEVTOOLS__", 14 | "_renderers", 15 | "__source", 16 | "__self" 17 | ] 18 | } 19 | }, 20 | "compress": { 21 | "hoist_vars": true, 22 | "reduce_funcs": false 23 | } 24 | }, 25 | "props": { 26 | "cname": 6, 27 | "props": { 28 | "$_afterPaintQueued": "__a", 29 | "$__hooks": "__H", 30 | "$_list": "__", 31 | "$_pendingEffects": "__h", 32 | "$_value": "__", 33 | "$_original": "__v", 34 | "$_args": "__H", 35 | "$_factory": "__h", 36 | "$_depth": "__b", 37 | "$_nextDom": "__d", 38 | "$_dirty": "__d", 39 | "$_detachOnNextRender": "__b", 40 | "$_force": "__e", 41 | "$_nextState": "__s", 42 | "$_renderCallbacks": "__h", 43 | "$_vnode": "__v", 44 | "$_children": "__k", 45 | "$_pendingSuspensionCount": "__u", 46 | "$_childDidSuspend": "__c", 47 | "$_suspendedComponentWillUnmount": "__c", 48 | "$_suspended": "__e", 49 | "$_dom": "__e", 50 | "$_hydrating": "__h", 51 | "$_component": "__c", 52 | "$__html": "__html", 53 | "$_parent": "__", 54 | "$_pendingError": "__E", 55 | "$_processingException": "__", 56 | "$_globalContext": "__n", 57 | "$_context": "__c", 58 | "$_defaultValue": "__", 59 | "$_id": "__c", 60 | "$_contextRef": "__", 61 | "$_parentDom": "__P", 62 | "$_prevState": "__u", 63 | "$_root": "__", 64 | "$_diff": "__b", 65 | "$_commit": "__c", 66 | "$_render": "__r", 67 | "$_hook": "__h", 68 | "$_catchError": "__e", 69 | "$_unmount": "__u", 70 | "$_owner": "__o", 71 | "$_skipEffects": "__s", 72 | "$_rerenderCount": "__r", 73 | "$_forwarded": "__f" 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /test/_util/dom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Serialize contents 3 | * @typedef {string | number | Array} Contents 4 | * @param {Contents} contents 5 | */ 6 | const serialize = contents => 7 | Array.isArray(contents) ? contents.join('') : contents.toString(); 8 | 9 | /** 10 | * A helper to generate innerHTML validation strings containing spans 11 | * @param {Contents} contents The contents of the span, as a string 12 | */ 13 | export const span = contents => `${serialize(contents)}`; 14 | 15 | /** 16 | * A helper to generate innerHTML validation strings containing divs 17 | * @param {Contents} contents The contents of the div, as a string 18 | */ 19 | export const div = contents => `
    ${serialize(contents)}
    `; 20 | 21 | /** 22 | * A helper to generate innerHTML validation strings containing p 23 | * @param {Contents} contents The contents of the p, as a string 24 | */ 25 | export const p = contents => `

    ${serialize(contents)}

    `; 26 | 27 | /** 28 | * A helper to generate innerHTML validation strings containing sections 29 | * @param {Contents} contents The contents of the section, as a string 30 | */ 31 | export const section = contents => `
    ${serialize(contents)}
    `; 32 | 33 | /** 34 | * A helper to generate innerHTML validation strings containing uls 35 | * @param {Contents} contents The contents of the ul, as a string 36 | */ 37 | export const ul = contents => `
      ${serialize(contents)}
    `; 38 | 39 | /** 40 | * A helper to generate innerHTML validation strings containing ols 41 | * @param {Contents} contents The contents of the ol, as a string 42 | */ 43 | export const ol = contents => `
      ${serialize(contents)}
    `; 44 | 45 | /** 46 | * A helper to generate innerHTML validation strings containing lis 47 | * @param {Contents} contents The contents of the li, as a string 48 | */ 49 | export const li = contents => `
  • ${serialize(contents)}
  • `; 50 | 51 | /** 52 | * A helper to generate innerHTML validation strings containing inputs 53 | */ 54 | export const input = () => ``; 55 | 56 | /** 57 | * A helper to generate innerHTML validation strings containing h1 58 | * @param {Contents} contents The contents of the h1 59 | */ 60 | export const h1 = contents => `

    ${serialize(contents)}

    `; 61 | 62 | /** 63 | * A helper to generate innerHTML validation strings containing h2 64 | * @param {Contents} contents The contents of the h2 65 | */ 66 | export const h2 = contents => `

    ${serialize(contents)}

    `; 67 | -------------------------------------------------------------------------------- /benches/src/util.js: -------------------------------------------------------------------------------- 1 | // import afterFrame from "../node_modules/afterframe/dist/afterframe.module.js"; 2 | import afterFrame from 'afterframe'; 3 | 4 | export { afterFrame }; 5 | 6 | export const measureName = 'duration'; 7 | 8 | let promise = null; 9 | export function afterFrameAsync() { 10 | if (promise === null) { 11 | promise = new Promise(resolve => 12 | afterFrame(time => { 13 | promise = null; 14 | resolve(time); 15 | }) 16 | ); 17 | } 18 | 19 | return promise; 20 | } 21 | 22 | export function measureMemory() { 23 | if ('gc' in window && 'memory' in performance) { 24 | // Report results in MBs 25 | window.gc(); 26 | window.usedJSHeapSize = performance.memory.usedJSHeapSize / 1e6; 27 | } else { 28 | window.usedJSHeapSize = 0; 29 | } 30 | } 31 | 32 | export function getRowIdSel(index) { 33 | return `tbody > tr:nth-child(${index}) > td:first-child`; 34 | } 35 | 36 | export function getRowLinkSel(index) { 37 | return `tbody > tr:nth-child(${index}) > td:nth-child(2) > a`; 38 | } 39 | 40 | /** 41 | * @param {string} selector 42 | * @returns {Element} 43 | */ 44 | export function getBySelector(selector) { 45 | const element = document.querySelector(selector); 46 | if (element == null) { 47 | throw new Error(`Could not find element matching selector: ${selector}`); 48 | } 49 | 50 | return element; 51 | } 52 | 53 | export function testElement(selector) { 54 | const testElement = document.querySelector(selector); 55 | if (testElement == null) { 56 | throw new Error( 57 | 'Test failed. Rendering after one paint was not successful' 58 | ); 59 | } 60 | } 61 | 62 | export function testElementText(selector, expectedText) { 63 | const elm = document.querySelector(selector); 64 | if (elm == null) { 65 | throw new Error('Could not find element matching selector: ' + selector); 66 | } 67 | 68 | if (elm.textContent != expectedText) { 69 | throw new Error( 70 | `Element did not have expected text. Expected: '${expectedText}' Actual: '${elm.textContent}'` 71 | ); 72 | } 73 | } 74 | 75 | export function testElementTextContains(selector, expectedText) { 76 | const elm = getBySelector(selector); 77 | if (!elm.textContent.includes(expectedText)) { 78 | throw new Error( 79 | `Element did not include expected text. Expected to include: '${expectedText}' Actual: '${elm.textContent}'` 80 | ); 81 | } 82 | } 83 | 84 | export function isPreactX(createElement) { 85 | const vnode = createElement('div', {}); 86 | return vnode.constructor === undefined; 87 | } 88 | -------------------------------------------------------------------------------- /benches/src/keyed-children/store.js: -------------------------------------------------------------------------------- 1 | function _random(max) { 2 | return Math.round(Math.random() * 1000) % max; 3 | } 4 | 5 | export class Store { 6 | constructor() { 7 | this.data = []; 8 | this.selected = undefined; 9 | this.id = 1; 10 | } 11 | buildData(count = 1000) { 12 | var adjectives = [ 13 | 'pretty', 14 | 'large', 15 | 'big', 16 | 'small', 17 | 'tall', 18 | 'short', 19 | 'long', 20 | 'handsome', 21 | 'plain', 22 | 'quaint', 23 | 'clean', 24 | 'elegant', 25 | 'easy', 26 | 'angry', 27 | 'crazy', 28 | 'helpful', 29 | 'mushy', 30 | 'odd', 31 | 'unsightly', 32 | 'adorable', 33 | 'important', 34 | 'inexpensive', 35 | 'cheap', 36 | 'expensive', 37 | 'fancy' 38 | ]; 39 | var colours = [ 40 | 'red', 41 | 'yellow', 42 | 'blue', 43 | 'green', 44 | 'pink', 45 | 'brown', 46 | 'purple', 47 | 'brown', 48 | 'white', 49 | 'black', 50 | 'orange' 51 | ]; 52 | var nouns = [ 53 | 'table', 54 | 'chair', 55 | 'house', 56 | 'bbq', 57 | 'desk', 58 | 'car', 59 | 'pony', 60 | 'cookie', 61 | 'sandwich', 62 | 'burger', 63 | 'pizza', 64 | 'mouse', 65 | 'keyboard' 66 | ]; 67 | var data = []; 68 | for (var i = 0; i < count; i++) 69 | data.push({ 70 | id: this.id++, 71 | label: 72 | adjectives[_random(adjectives.length)] + 73 | ' ' + 74 | colours[_random(colours.length)] + 75 | ' ' + 76 | nouns[_random(nouns.length)] 77 | }); 78 | return data; 79 | } 80 | updateData(mod = 10) { 81 | for (let i = 0; i < this.data.length; i += 10) { 82 | this.data[i] = Object.assign({}, this.data[i], { 83 | label: this.data[i].label + ' !!!' 84 | }); 85 | } 86 | } 87 | delete(id) { 88 | var idx = this.data.findIndex(d => d.id === id); 89 | this.data.splice(idx, 1); 90 | } 91 | run() { 92 | this.data = this.buildData(); 93 | this.selected = undefined; 94 | } 95 | add() { 96 | this.data = this.data.concat(this.buildData(1000)); 97 | } 98 | update() { 99 | this.updateData(); 100 | } 101 | select(id) { 102 | this.selected = id; 103 | } 104 | runLots() { 105 | this.data = this.buildData(10000); 106 | this.selected = undefined; 107 | } 108 | clear() { 109 | this.data = []; 110 | this.selected = undefined; 111 | } 112 | swapRows() { 113 | if (this.data.length > 998) { 114 | var a = this.data[1]; 115 | this.data[1] = this.data[998]; 116 | this.data[998] = a; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /demo/people/store.ts: -------------------------------------------------------------------------------- 1 | import { flow, Instance, types } from 'mobx-state-tree'; 2 | 3 | const cmp = (fn: (x: T) => U) => (a: T, b: T): number => 4 | fn(a) > fn(b) ? 1 : -1; 5 | 6 | const User = types.model({ 7 | email: types.string, 8 | gender: types.enumeration(['male', 'female']), 9 | id: types.identifier, 10 | name: types.model({ 11 | first: types.string, 12 | last: types.string 13 | }), 14 | picture: types.model({ 15 | large: types.string 16 | }) 17 | }); 18 | 19 | const Store = types 20 | .model({ 21 | users: types.array(User), 22 | usersOrder: types.enumeration(['name', 'id']) 23 | }) 24 | .views(self => ({ 25 | getSortedUsers() { 26 | if (self.usersOrder === 'name') 27 | return self.users.slice().sort(cmp(x => x.name.first)); 28 | if (self.usersOrder === 'id') 29 | return self.users.slice().sort(cmp(x => x.id)); 30 | throw Error(`Unknown ordering ${self.usersOrder}`); 31 | } 32 | })) 33 | .actions(self => ({ 34 | addUser: flow(function*() { 35 | const data = yield fetch('https://randomuser.me/api?results=1') 36 | .then(res => res.json()) 37 | .then(data => 38 | data.results.map((user: any) => ({ 39 | ...user, 40 | id: user.login.username 41 | })) 42 | ); 43 | self.users.push(...data); 44 | }), 45 | loadUsers: flow(function*() { 46 | const data = yield fetch( 47 | `https://randomuser.me/api?seed=${12321}&results=12` 48 | ) 49 | .then(res => res.json()) 50 | .then(data => 51 | data.results.map((user: any) => ({ 52 | ...user, 53 | id: user.login.username 54 | })) 55 | ); 56 | self.users.replace(data); 57 | }), 58 | deleteUser(id: string) { 59 | const user = self.users.find(u => u.id === id); 60 | if (user != null) self.users.remove(user); 61 | }, 62 | setUsersOrder(order: 'name' | 'id') { 63 | self.usersOrder = order; 64 | } 65 | })); 66 | 67 | export type StoreType = Instance; 68 | export const store = Store.create({ 69 | usersOrder: 'name', 70 | users: [] 71 | }); 72 | 73 | // const { Provider, Consumer } = createContext(undefined as any) 74 | 75 | // export const StoreProvider: FunctionalComponent = props => { 76 | // const store = Store.create({}) 77 | // return 78 | // } 79 | 80 | // export type StoreProps = {store: StoreType} 81 | // export function injectStore(Child: AnyComponent): FunctionalComponent { 82 | // return props => }/> 83 | // } 84 | -------------------------------------------------------------------------------- /hooks/test/browser/errorBoundary.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, render } from 'preact'; 2 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 3 | import { useErrorBoundary } from 'preact/hooks'; 4 | import { setupRerender } from 'preact/test-utils'; 5 | 6 | /** @jsx createElement */ 7 | 8 | describe('errorBoundary', () => { 9 | /** @type {HTMLDivElement} */ 10 | let scratch, rerender; 11 | 12 | beforeEach(() => { 13 | scratch = setupScratch(); 14 | rerender = setupRerender(); 15 | }); 16 | 17 | afterEach(() => { 18 | teardown(scratch); 19 | }); 20 | 21 | it('catches errors', () => { 22 | let resetErr, 23 | success = false; 24 | const Throws = () => { 25 | throw new Error('test'); 26 | }; 27 | 28 | const App = props => { 29 | const [err, reset] = useErrorBoundary(); 30 | resetErr = reset; 31 | return err ?

    Error

    : success ?

    Success

    : ; 32 | }; 33 | 34 | render(, scratch); 35 | rerender(); 36 | expect(scratch.innerHTML).to.equal('

    Error

    '); 37 | 38 | success = true; 39 | resetErr(); 40 | rerender(); 41 | expect(scratch.innerHTML).to.equal('

    Success

    '); 42 | }); 43 | 44 | it('calls the errorBoundary callback', () => { 45 | const spy = sinon.spy(); 46 | const error = new Error('test'); 47 | const Throws = () => { 48 | throw error; 49 | }; 50 | 51 | const App = props => { 52 | const [err] = useErrorBoundary(spy); 53 | return err ?

    Error

    : ; 54 | }; 55 | 56 | render(, scratch); 57 | rerender(); 58 | expect(scratch.innerHTML).to.equal('

    Error

    '); 59 | expect(spy).to.be.calledOnce; 60 | expect(spy).to.be.calledWith(error); 61 | }); 62 | 63 | it('does not leave a stale closure', () => { 64 | const spy = sinon.spy(), 65 | spy2 = sinon.spy(); 66 | let resetErr; 67 | const error = new Error('test'); 68 | const Throws = () => { 69 | throw error; 70 | }; 71 | 72 | const App = props => { 73 | const [err, reset] = useErrorBoundary(props.onError); 74 | resetErr = reset; 75 | return err ?

    Error

    : ; 76 | }; 77 | 78 | render(, scratch); 79 | rerender(); 80 | expect(scratch.innerHTML).to.equal('

    Error

    '); 81 | expect(spy).to.be.calledOnce; 82 | expect(spy).to.be.calledWith(error); 83 | 84 | resetErr(); 85 | render(, scratch); 86 | rerender(); 87 | expect(scratch.innerHTML).to.equal('

    Error

    '); 88 | expect(spy).to.be.calledOnce; 89 | expect(spy2).to.be.calledOnce; 90 | expect(spy2).to.be.calledWith(error); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /debug/src/internal.d.ts: -------------------------------------------------------------------------------- 1 | import { Component, PreactElement, VNode, Options } from '../../src/internal'; 2 | 3 | export { Component, PreactElement, VNode, Options }; 4 | 5 | export interface DevtoolsInjectOptions { 6 | /** 1 = DEV, 0 = production */ 7 | bundleType: 1 | 0; 8 | /** The devtools enable different features for different versions of react */ 9 | version: string; 10 | /** Informative string, currently unused in the devtools */ 11 | rendererPackageName: string; 12 | /** Find the root dom node of a vnode */ 13 | findHostInstanceByFiber(vnode: VNode): HTMLElement | null; 14 | /** Find the closest vnode given a dom node */ 15 | findFiberByHostInstance(instance: HTMLElement): VNode | null; 16 | } 17 | 18 | export interface DevtoolsUpdater { 19 | setState(objOrFn: any): void; 20 | forceUpdate(): void; 21 | setInState(path: Array, value: any): void; 22 | setInProps(path: Array, value: any): void; 23 | setInContext(): void; 24 | } 25 | 26 | export type NodeType = 'Composite' | 'Native' | 'Wrapper' | 'Text'; 27 | 28 | export interface DevtoolData { 29 | nodeType: NodeType; 30 | // Component type 31 | type: any; 32 | name: string; 33 | ref: any; 34 | key: string | number; 35 | updater: DevtoolsUpdater | null; 36 | text: string | number | null; 37 | state: any; 38 | props: any; 39 | children: VNode[] | string | number | null; 40 | publicInstance: PreactElement | Text | Component; 41 | memoizedInteractions: any[]; 42 | 43 | actualDuration: number; 44 | actualStartTime: number; 45 | treeBaseDuration: number; 46 | } 47 | 48 | export type EventType = 49 | | 'unmount' 50 | | 'rootCommitted' 51 | | 'root' 52 | | 'mount' 53 | | 'update' 54 | | 'updateProfileTimes'; 55 | 56 | export interface DevtoolsEvent { 57 | data?: DevtoolData; 58 | internalInstance: VNode; 59 | renderer: string; 60 | type: EventType; 61 | } 62 | 63 | export interface DevtoolsHook { 64 | _renderers: Record; 65 | _roots: Set; 66 | on(ev: string, listener: () => void): void; 67 | emit(ev: string, data?: object): void; 68 | helpers: Record; 69 | getFiberRoots(rendererId: string): Set; 70 | inject(config: DevtoolsInjectOptions): string; 71 | onCommitFiberRoot(rendererId: string, root: VNode): void; 72 | onCommitFiberUnmount(rendererId: string, vnode: VNode): void; 73 | } 74 | 75 | export interface DevtoolsWindow extends Window { 76 | /** 77 | * If the devtools extension is installed it will inject this object into 78 | * the dom. This hook handles all communications between preact and the 79 | * devtools panel. 80 | */ 81 | __REACT_DEVTOOLS_GLOBAL_HOOK__?: DevtoolsHook; 82 | } 83 | -------------------------------------------------------------------------------- /src/render.js: -------------------------------------------------------------------------------- 1 | import { EMPTY_OBJ, EMPTY_ARR } from './constants'; 2 | import { commitRoot, diff } from './diff/index'; 3 | import { createElement, Fragment } from './create-element'; 4 | import options from './options'; 5 | 6 | const IS_HYDRATE = EMPTY_OBJ; 7 | 8 | /** 9 | * Render a Preact virtual node into a DOM element 10 | * @param {import('./index').ComponentChild} vnode The virtual node to render 11 | * @param {import('./internal').PreactElement} parentDom The DOM element to 12 | * render into 13 | * @param {Element | Text} [replaceNode] Optional: Attempt to re-use an 14 | * existing DOM tree rooted at `replaceNode` 15 | */ 16 | export function render(vnode, parentDom, replaceNode) { 17 | if (options._root) options._root(vnode, parentDom); 18 | 19 | // We abuse the `replaceNode` parameter in `hydrate()` to signal if we 20 | // are in hydration mode or not by passing `IS_HYDRATE` instead of a 21 | // DOM element. 22 | let isHydrating = replaceNode === IS_HYDRATE; 23 | 24 | // To be able to support calling `render()` multiple times on the same 25 | // DOM node, we need to obtain a reference to the previous tree. We do 26 | // this by assigning a new `_children` property to DOM nodes which points 27 | // to the last rendered tree. By default this property is not present, which 28 | // means that we are mounting a new tree for the first time. 29 | let oldVNode = isHydrating 30 | ? null 31 | : (replaceNode && replaceNode._children) || parentDom._children; 32 | vnode = createElement(Fragment, null, [vnode]); 33 | 34 | // List of effects that need to be called after diffing. 35 | let commitQueue = []; 36 | diff( 37 | parentDom, 38 | // Determine the new vnode tree and store it on the DOM element on 39 | // our custom `_children` property. 40 | ((isHydrating ? parentDom : replaceNode || parentDom)._children = vnode), 41 | oldVNode || EMPTY_OBJ, 42 | EMPTY_OBJ, 43 | parentDom.ownerSVGElement !== undefined, 44 | replaceNode && !isHydrating 45 | ? [replaceNode] 46 | : oldVNode 47 | ? null 48 | : parentDom.childNodes.length 49 | ? EMPTY_ARR.slice.call(parentDom.childNodes) 50 | : null, 51 | commitQueue, 52 | replaceNode || EMPTY_OBJ, 53 | isHydrating 54 | ); 55 | 56 | // Flush all queued effects 57 | commitRoot(commitQueue, vnode); 58 | } 59 | 60 | /** 61 | * Update an existing DOM element with data from a Preact virtual node 62 | * @param {import('./index').ComponentChild} vnode The virtual node to render 63 | * @param {import('./internal').PreactElement} parentDom The DOM element to 64 | * update 65 | */ 66 | export function hydrate(vnode, parentDom) { 67 | render(vnode, parentDom, IS_HYDRATE); 68 | } 69 | -------------------------------------------------------------------------------- /test/ts/custom-elements.tsx: -------------------------------------------------------------------------------- 1 | import { createElement, Component, createContext } from '../../'; 2 | 3 | declare module '../../' { 4 | namespace createElement.JSX { 5 | interface IntrinsicElements { 6 | // Custom element can use JSX EventHandler definitions 7 | 'clickable-ce': { 8 | optionalAttr?: string; 9 | onClick?: MouseEventHandler; 10 | }; 11 | 12 | // Custom Element that extends HTML attributes 13 | 'color-picker': HTMLAttributes & { 14 | // Required attribute 15 | space: 'rgb' | 'hsl' | 'hsv'; 16 | // Optional attribute 17 | alpha?: boolean; 18 | }; 19 | 20 | // Custom Element with custom interface definition 21 | 'custom-whatever': WhateveElAttributes; 22 | } 23 | } 24 | } 25 | 26 | // Whatever Element definition 27 | 28 | interface WhateverElement { 29 | instanceProp: string; 30 | } 31 | 32 | interface WhateverElementEvent { 33 | eventProp: number; 34 | } 35 | 36 | // preact.JSX.HTMLAttributes also appears to work here but for consistency, 37 | // let's use createElement.JSX 38 | interface WhateveElAttributes extends createElement.JSX.HTMLAttributes { 39 | someattribute?: string; 40 | onsomeevent?: (this: WhateverElement, ev: WhateverElementEvent) => void; 41 | } 42 | 43 | // Ensure context still works 44 | const { Provider, Consumer } = createContext({ contextValue: '' }); 45 | 46 | // Sample component that uses custom elements 47 | 48 | class SimpleComponent extends Component { 49 | componentProp = 'componentProp'; 50 | render() { 51 | // Render inside div to ensure standard JSX elements still work 52 | return ( 53 | 54 |
    55 | { 57 | // `this` should be instance of SimpleComponent since this is an 58 | // arrow function 59 | console.log(this.componentProp); 60 | 61 | // Validate `currentTarget` is HTMLElement 62 | console.log('clicked ', e.currentTarget.style.display); 63 | }} 64 | > 65 | 66 | 74 | 75 | {/* Ensure context still works */} 76 | 77 | {({ contextValue }) => contextValue.toLowerCase()} 78 | 79 |
    80 |
    81 | ); 82 | } 83 | } 84 | 85 | const component = ; 86 | -------------------------------------------------------------------------------- /test/browser/cloneElement.test.js: -------------------------------------------------------------------------------- 1 | import { createElement, cloneElement, createRef } from 'preact'; 2 | 3 | /** @jsx createElement */ 4 | 5 | describe('cloneElement', () => { 6 | it('should clone components', () => { 7 | function Comp() {} 8 | const instance = hello; 9 | const clone = cloneElement(instance); 10 | 11 | expect(clone.prototype).to.equal(instance.prototype); 12 | expect(clone.type).to.equal(instance.type); 13 | expect(clone.props).not.to.equal(instance.props); // Should be a different object... 14 | expect(clone.props).to.deep.equal(instance.props); // with the same properties 15 | }); 16 | 17 | it('should merge new props', () => { 18 | function Foo() {} 19 | const instance = ; 20 | const clone = cloneElement(instance, { prop1: -1, newProp: -2 }); 21 | 22 | expect(clone.prototype).to.equal(instance.prototype); 23 | expect(clone.type).to.equal(instance.type); 24 | expect(clone.props).not.to.equal(instance.props); 25 | expect(clone.props.prop1).to.equal(-1); 26 | expect(clone.props.prop2).to.equal(2); 27 | expect(clone.props.newProp).to.equal(-2); 28 | }); 29 | 30 | it('should override children if specified', () => { 31 | function Foo() {} 32 | const instance = hello; 33 | const clone = cloneElement(instance, null, 'world', '!'); 34 | 35 | expect(clone.prototype).to.equal(instance.prototype); 36 | expect(clone.type).to.equal(instance.type); 37 | expect(clone.props).not.to.equal(instance.props); 38 | expect(clone.props.children).to.deep.equal(['world', '!']); 39 | }); 40 | 41 | it('should override key if specified', () => { 42 | function Foo() {} 43 | const instance = hello; 44 | 45 | let clone = cloneElement(instance); 46 | expect(clone.key).to.equal('1'); 47 | 48 | clone = cloneElement(instance, { key: '2' }); 49 | expect(clone.key).to.equal('2'); 50 | }); 51 | 52 | it('should override ref if specified', () => { 53 | function a() {} 54 | function b() {} 55 | function Foo() {} 56 | const instance = hello; 57 | 58 | let clone = cloneElement(instance); 59 | expect(clone.ref).to.equal(a); 60 | 61 | clone = cloneElement(instance, { ref: b }); 62 | expect(clone.ref).to.equal(b); 63 | }); 64 | 65 | it('should normalize props (ref)', () => { 66 | const div =
    hello
    ; 67 | const clone = cloneElement(div, { ref: createRef() }); 68 | expect(clone.props.ref).to.equal(undefined); 69 | }); 70 | 71 | it('should normalize props (key)', () => { 72 | const div =
    hello
    ; 73 | const clone = cloneElement(div, { key: 'myKey' }); 74 | expect(clone.props.key).to.equal(undefined); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /demo/old.js.bak: -------------------------------------------------------------------------------- 1 | 2 | // function createRoot(title) { 3 | // let div = document.createElement('div'); 4 | // let h2 = document.createElement('h2'); 5 | // h2.textContent = title; 6 | // div.appendChild(h2); 7 | // document.body.appendChild(div); 8 | // return div; 9 | // } 10 | 11 | 12 | /* 13 | function logCall(obj, method, name) { 14 | let old = obj[method]; 15 | obj[method] = function(...args) { 16 | console.log(`<${this.localName}>.`+(name || `${method}(${args})`)); 17 | return old.apply(this, args); 18 | }; 19 | } 20 | 21 | logCall(HTMLElement.prototype, 'appendChild'); 22 | logCall(HTMLElement.prototype, 'removeChild'); 23 | logCall(HTMLElement.prototype, 'insertBefore'); 24 | logCall(HTMLElement.prototype, 'replaceChild'); 25 | logCall(HTMLElement.prototype, 'setAttribute'); 26 | logCall(HTMLElement.prototype, 'removeAttribute'); 27 | let d = Object.getOwnPropertyDescriptor(Node.prototype, 'nodeValue'); 28 | Object.defineProperty(Text.prototype, 'nodeValue', { 29 | get() { 30 | let value = d.get.call(this); 31 | console.log('get Text#nodeValue: ', value); 32 | return value; 33 | }, 34 | set(v) { 35 | console.log('set Text#nodeValue', v); 36 | return d.set.call(this, v); 37 | } 38 | }); 39 | 40 | 41 | render(( 42 |
    43 |

    This is a test.

    44 | 45 | 46 |
    47 | ), createRoot('Stateful component update demo:')); 48 | 49 | 50 | class Foo extends Component { 51 | componentDidMount() { 52 | console.log('mounted'); 53 | this.timer = setInterval( () => { 54 | this.setState({ time: Date.now() }); 55 | }, 5000); 56 | } 57 | componentWillUnmount() { 58 | clearInterval(this.timer); 59 | } 60 | render(props, state, context) { 61 | // console.log('rendering', props, state, context); 62 | return 63 | } 64 | } 65 | 66 | 67 | render(( 68 |
    69 |

    This is a test.

    70 | 71 | 72 |
    73 | ), createRoot('Stateful component update demo:')); 74 | 75 | 76 | let items = []; 77 | let count = 0; 78 | let three = createRoot('Top-level render demo:'); 79 | 80 | setInterval( () => { 81 | if (count++ %20 < 10 ) { 82 | items.push(
  • item #{items.length}
  • ); 87 | } 88 | else { 89 | items.shift(); 90 | } 91 | 92 | render(( 93 |
    94 |

    This is a test.

    95 | 96 |
      {items}
    97 |
    98 | ), three); 99 | }, 5000); 100 | 101 | // Mount the top-level component to the DOM: 102 | render(
    , document.body); 103 | */ 104 | -------------------------------------------------------------------------------- /compat/test/browser/svg.test.js: -------------------------------------------------------------------------------- 1 | import React, { createElement } from 'preact/compat'; 2 | import { 3 | setupScratch, 4 | teardown, 5 | serializeHtml, 6 | sortAttributes 7 | } from '../../../test/_util/helpers'; 8 | 9 | describe('svg', () => { 10 | /** @type {HTMLDivElement} */ 11 | let scratch; 12 | 13 | beforeEach(() => { 14 | scratch = setupScratch(); 15 | }); 16 | 17 | afterEach(() => { 18 | teardown(scratch); 19 | }); 20 | 21 | it('should render SVG to string', () => { 22 | let svg = ( 23 | 24 | 29 | 30 | ); 31 | // string -> parse 32 | expect(svg).to.eql(svg); 33 | }); 34 | 35 | it('should render SVG to DOM #1', () => { 36 | const Demo = () => ( 37 | 38 | 43 | 44 | ); 45 | React.render(, scratch); 46 | 47 | expect(serializeHtml(scratch)).to.equal( 48 | sortAttributes( 49 | '' 50 | ) 51 | ); 52 | }); 53 | 54 | it('should render SVG to DOM #2', () => { 55 | React.render( 56 | 57 | foo 58 | 59 | , 60 | scratch 61 | ); 62 | 63 | expect(serializeHtml(scratch)).to.equal( 64 | sortAttributes( 65 | 'foo' 66 | ) 67 | ); 68 | }); 69 | 70 | it('should render correct SVG attribute names to the DOM', () => { 71 | React.render( 72 | , 85 | scratch 86 | ); 87 | 88 | expect(serializeHtml(scratch)).to.eql( 89 | sortAttributes( 90 | '' 91 | ) 92 | ); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /demo/people/styles/button.scss: -------------------------------------------------------------------------------- 1 | #people-app { 2 | button { 3 | position: relative; 4 | overflow: hidden; 5 | min-width: 36px; 6 | height: 36px; 7 | padding: 0 16px; 8 | border: none; 9 | background-color: transparent; 10 | border-radius: 4px; 11 | color: var(--app-text); 12 | font-family: 'Montserrat', sans-serif; 13 | font-size: 14px; 14 | font-weight: 600; 15 | letter-spacing: 0.08em; 16 | text-transform: uppercase; 17 | transition: background 300ms, color 200ms; 18 | white-space: nowrap; 19 | } 20 | 21 | button::before { 22 | position: absolute; 23 | top: 0; 24 | right: 0; 25 | bottom: 0; 26 | left: 0; 27 | background-color: var(--app-ripple); 28 | content: ''; 29 | opacity: 0; 30 | transition: opacity 200ms; 31 | } 32 | 33 | button:hover:not(:disabled)::before { 34 | opacity: 0.3; 35 | transition: opacity 100ms; 36 | } 37 | 38 | button:active:not(:disabled)::before { 39 | opacity: 0.7; 40 | transition: none; 41 | } 42 | 43 | button::after { 44 | position: absolute; 45 | top: 0; 46 | right: 0; 47 | bottom: 0; 48 | left: 0; 49 | background-image: radial-gradient( 50 | circle, 51 | var(--app-ripple) 1%, 52 | transparent 1% 53 | ); 54 | background-position: center; 55 | background-repeat: no-repeat; 56 | background-size: 20000%; 57 | content: ''; 58 | opacity: 0; 59 | transition: opacity 700ms, background 400ms; 60 | } 61 | 62 | button:active:not(:disabled)::after { 63 | background-size: 100%; 64 | opacity: 1; 65 | transition: none; 66 | } 67 | 68 | button.primary { 69 | background-color: var(--app-primary); 70 | box-shadow: 0 2px 6px var(--app-shadow); 71 | } 72 | 73 | button.secondary { 74 | background-color: var(--app-secondary); 75 | box-shadow: 0 2px 6px var(--app-shadow); 76 | } 77 | 78 | button:disabled { 79 | color: var(--app-text-secondary); 80 | } 81 | 82 | button.busy { 83 | animation: stripes 500ms linear infinite; 84 | background-image: repeating-linear-gradient( 85 | 45deg, 86 | var(--app-shadow) 0%, 87 | var(--app-shadow) 25%, 88 | transparent 25%, 89 | transparent 50%, 90 | var(--app-shadow) 50%, 91 | var(--app-shadow) 75%, 92 | transparent 75%, 93 | transparent 100% 94 | ); 95 | color: var(--app-text); 96 | /* letter-spacing: -.7em; */ 97 | } 98 | 99 | button:disabled:not(.primary):not(.secondary).busy, 100 | button:disabled.primary:not(.busy), 101 | button:disabled.secondary:not(.busy) { 102 | background-color: var(--app-background-disabled); 103 | } 104 | 105 | @keyframes stripes { 106 | from { 107 | background-position-x: 0; 108 | background-size: 16px 16px; 109 | } 110 | to { 111 | background-position-x: 16px; 112 | background-size: 16px 16px; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /demo/reorder.js: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from 'preact'; 2 | 3 | function createItems(count = 10) { 4 | let items = []; 5 | for (let i = 0; i < count; i++) { 6 | items.push({ 7 | label: `Item #${i + 1}`, 8 | key: i + 1 9 | }); 10 | } 11 | return items; 12 | } 13 | 14 | function random() { 15 | return Math.random() < 0.5 ? 1 : -1; 16 | } 17 | 18 | export default class Reorder extends Component { 19 | state = { 20 | items: createItems(), 21 | count: 1, 22 | useKeys: false 23 | }; 24 | 25 | shuffle = () => { 26 | this.setState({ items: this.state.items.slice().sort(random) }); 27 | }; 28 | 29 | swapTwo = () => { 30 | let items = this.state.items.slice(), 31 | first = Math.floor(Math.random() * items.length), 32 | second; 33 | do { 34 | second = Math.floor(Math.random() * items.length); 35 | } while (second === first); 36 | let other = items[first]; 37 | items[first] = items[second]; 38 | items[second] = other; 39 | this.setState({ items }); 40 | }; 41 | 42 | reverse = () => { 43 | this.setState({ items: this.state.items.slice().reverse() }); 44 | }; 45 | 46 | setCount = e => { 47 | this.setState({ count: Math.round(e.target.value) }); 48 | }; 49 | 50 | rotate = () => { 51 | let { items, count } = this.state; 52 | items = items.slice(count).concat(items.slice(0, count)); 53 | this.setState({ items }); 54 | }; 55 | 56 | rotateBackward = () => { 57 | let { items, count } = this.state, 58 | len = items.length; 59 | items = items.slice(len - count, len).concat(items.slice(0, len - count)); 60 | this.setState({ items }); 61 | }; 62 | 63 | toggleKeys = () => { 64 | this.setState({ useKeys: !this.state.useKeys }); 65 | }; 66 | 67 | renderItem = item => ( 68 |
  • {item.label}
  • 69 | ); 70 | 71 | render({}, { items, count, useKeys }) { 72 | return ( 73 |
    74 |
    75 | 76 | 77 | 78 | 79 | 80 | 88 | 99 |
    100 |
      {items.map(this.renderItem)}
    101 |
    102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /test/browser/lifecycles/componentWillUpdate.test.js: -------------------------------------------------------------------------------- 1 | import { setupRerender } from 'preact/test-utils'; 2 | import { createElement, render, Component } from 'preact'; 3 | import { setupScratch, teardown } from '../../_util/helpers'; 4 | 5 | /** @jsx createElement */ 6 | 7 | describe('Lifecycle methods', () => { 8 | /** @type {HTMLDivElement} */ 9 | let scratch; 10 | 11 | /** @type {() => void} */ 12 | let rerender; 13 | 14 | beforeEach(() => { 15 | scratch = setupScratch(); 16 | rerender = setupRerender(); 17 | }); 18 | 19 | afterEach(() => { 20 | teardown(scratch); 21 | }); 22 | 23 | describe('#componentWillUpdate', () => { 24 | it('should NOT be called on initial render', () => { 25 | class ReceivePropsComponent extends Component { 26 | componentWillUpdate() {} 27 | render() { 28 | return
    ; 29 | } 30 | } 31 | sinon.spy(ReceivePropsComponent.prototype, 'componentWillUpdate'); 32 | render(, scratch); 33 | expect(ReceivePropsComponent.prototype.componentWillUpdate).not.to.have 34 | .been.called; 35 | }); 36 | 37 | it('should be called when rerender with new props from parent', () => { 38 | let doRender; 39 | class Outer extends Component { 40 | constructor(p, c) { 41 | super(p, c); 42 | this.state = { i: 0 }; 43 | } 44 | componentDidMount() { 45 | doRender = () => this.setState({ i: this.state.i + 1 }); 46 | } 47 | render(props, { i }) { 48 | return ; 49 | } 50 | } 51 | class Inner extends Component { 52 | componentWillUpdate(nextProps, nextState) { 53 | expect(nextProps).to.be.deep.equal({ i: 1 }); 54 | expect(nextState).to.be.deep.equal({}); 55 | } 56 | render() { 57 | return
    ; 58 | } 59 | } 60 | sinon.spy(Inner.prototype, 'componentWillUpdate'); 61 | sinon.spy(Outer.prototype, 'componentDidMount'); 62 | 63 | // Initial render 64 | render(, scratch); 65 | expect(Inner.prototype.componentWillUpdate).not.to.have.been.called; 66 | 67 | // Rerender inner with new props 68 | doRender(); 69 | rerender(); 70 | expect(Inner.prototype.componentWillUpdate).to.have.been.called; 71 | }); 72 | 73 | it('should be called on new state', () => { 74 | let doRender; 75 | class ReceivePropsComponent extends Component { 76 | componentWillUpdate() {} 77 | componentDidMount() { 78 | doRender = () => this.setState({ i: this.state.i + 1 }); 79 | } 80 | render() { 81 | return
    ; 82 | } 83 | } 84 | sinon.spy(ReceivePropsComponent.prototype, 'componentWillUpdate'); 85 | render(, scratch); 86 | expect(ReceivePropsComponent.prototype.componentWillUpdate).not.to.have 87 | .been.called; 88 | 89 | doRender(); 90 | rerender(); 91 | expect(ReceivePropsComponent.prototype.componentWillUpdate).to.have.been 92 | .called; 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /compat/test/browser/cloneElement.test.js: -------------------------------------------------------------------------------- 1 | import { createElement as preactH } from 'preact'; 2 | import React, { createElement, render, cloneElement } from 'preact/compat'; 3 | import { setupScratch, teardown } from '../../../test/_util/helpers'; 4 | 5 | describe('compat cloneElement', () => { 6 | /** @type {HTMLDivElement} */ 7 | let scratch; 8 | 9 | beforeEach(() => { 10 | scratch = setupScratch(); 11 | }); 12 | 13 | afterEach(() => { 14 | teardown(scratch); 15 | }); 16 | 17 | it('should clone elements', () => { 18 | let element = ( 19 | 20 | ab 21 | 22 | ); 23 | expect(cloneElement(element)).to.eql(element); 24 | }); 25 | 26 | it('should support props.children', () => { 27 | let element = b} />; 28 | let clone = cloneElement(element); 29 | expect(clone).to.eql(element); 30 | expect(cloneElement(clone).props.children).to.eql(element.props.children); 31 | }); 32 | 33 | it('children take precedence over props.children', () => { 34 | let element = ( 35 | c}> 36 |
    b
    37 |
    38 | ); 39 | let clone = cloneElement(element); 40 | expect(clone).to.eql(element); 41 | expect(clone.props.children.type).to.eql('div'); 42 | }); 43 | 44 | it('should support children in prop argument', () => { 45 | let element = ; 46 | let children = [b]; 47 | let clone = cloneElement(element, { children }); 48 | expect(clone.props.children).to.eql(children); 49 | }); 50 | 51 | it('single child argument takes precedence over props.children', () => { 52 | let element = ; 53 | let childrenA = [b]; 54 | let childrenB = [
    c
    ]; 55 | let clone = cloneElement(element, { children: childrenA }, ...childrenB); 56 | expect(clone.props.children).to.eql(childrenB[0]); 57 | }); 58 | 59 | it('multiple children arguments take precedence over props.children', () => { 60 | let element = ; 61 | let childrenA = [b]; 62 | let childrenB = [
    c
    , 'd']; 63 | let clone = cloneElement(element, { children: childrenA }, ...childrenB); 64 | expect(clone.props.children).to.eql(childrenB); 65 | }); 66 | 67 | it('children argument takes precedence over props.children even if falsey', () => { 68 | let element = ; 69 | let childrenA = [b]; 70 | let clone = cloneElement(element, { children: childrenA }, undefined); 71 | expect(clone.children).to.eql(undefined); 72 | }); 73 | 74 | it('should skip cloning on invalid element', () => { 75 | let element = { foo: 42 }; 76 | let clone = cloneElement(element); 77 | expect(clone).to.eql(element); 78 | }); 79 | 80 | it('should work with jsx constructor from core', () => { 81 | function Foo(props) { 82 | return
    {props.value}
    ; 83 | } 84 | 85 | let clone = cloneElement(preactH(Foo), { value: 'foo' }); 86 | render(clone, scratch); 87 | expect(scratch.textContent).to.equal('foo'); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/benchmarks/text.test.js: -------------------------------------------------------------------------------- 1 | /*global coverage, ENABLE_PERFORMANCE */ 2 | /*eslint no-console:0*/ 3 | /** @jsx createElement */ 4 | 5 | import { setupScratch, teardown } from '../_util/helpers'; 6 | import bench from '../_util/bench'; 7 | import preact8 from '../fixtures/preact'; 8 | import * as preactX from '../../dist/preact.module'; 9 | const MULTIPLIER = ENABLE_PERFORMANCE ? (coverage ? 5 : 1) : 999999; 10 | 11 | describe('benchmarks', function() { 12 | let scratch; 13 | 14 | this.timeout(100000); 15 | 16 | before(function() { 17 | if (!ENABLE_PERFORMANCE) this.skip(); 18 | if (coverage) { 19 | console.warn( 20 | 'WARNING: Code coverage is enabled, which dramatically reduces performance. Do not pay attention to these numbers.' 21 | ); 22 | } 23 | }); 24 | 25 | beforeEach(() => { 26 | scratch = setupScratch(); 27 | }); 28 | 29 | afterEach(() => { 30 | teardown(scratch); 31 | }); 32 | 33 | it('in-place text update', done => { 34 | function createTest({ createElement, render }) { 35 | const parent = document.createElement('div'); 36 | scratch.appendChild(parent); 37 | 38 | function component(randomValue) { 39 | return ( 40 |
    41 |

    Test {randomValue}

    42 |

    ==={randomValue}===

    43 |
    44 | ); 45 | } 46 | 47 | return value => { 48 | const t = value % 100; 49 | render(component(t), parent); 50 | }; 51 | } 52 | 53 | function createVanillaTest() { 54 | const parent = document.createElement('div'); 55 | let div, h1, h2, text1, text2; 56 | parent.appendChild((div = document.createElement('div'))); 57 | div.appendChild((h2 = document.createElement('h2'))); 58 | h2.appendChild(document.createTextNode('Vanilla ')); 59 | h2.appendChild((text1 = document.createTextNode('0'))); 60 | div.appendChild((h1 = document.createElement('h1'))); 61 | h1.appendChild(document.createTextNode('===')); 62 | h1.appendChild((text2 = document.createTextNode('0'))); 63 | h1.appendChild(document.createTextNode('===')); 64 | scratch.appendChild(parent); 65 | 66 | return value => { 67 | const t = value % 100; 68 | text1.data = '' + t; 69 | text2.data = '' + t; 70 | }; 71 | } 72 | 73 | const preact8Test = createTest(preact8); 74 | const preactXTest = createTest(preactX); 75 | const vanillaTest = createVanillaTest(); 76 | 77 | for (let i = 100; i--; ) { 78 | preact8Test(i); 79 | preactXTest(i); 80 | vanillaTest(i); 81 | } 82 | 83 | bench( 84 | { 85 | vanilla: vanillaTest, 86 | preact8: preact8Test, 87 | preactX: preactXTest 88 | }, 89 | ({ text, results }) => { 90 | const THRESHOLD = 10 * MULTIPLIER; 91 | // const slowdown = Math.sqrt(results.preactX.hz * results.vanilla.hz); 92 | const slowdown = results.vanilla.hz / results.preactX.hz; 93 | console.log( 94 | `in-place text update is ${slowdown.toFixed(2)}x slower:` + text 95 | ); 96 | expect(slowdown).to.be.below(THRESHOLD); 97 | done(); 98 | } 99 | ); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /demo/style.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | background: #eee; 5 | font: 400 16px/1.3 'Helvetica Neue',helvetica,sans-serif; 6 | text-rendering: optimizeSpeed; 7 | color: #444; 8 | } 9 | 10 | .app { 11 | display: block; 12 | flex-direction: column; 13 | height: 100%; 14 | 15 | > header { 16 | flex: 0; 17 | background: #f9f9f9; 18 | box-shadow: inset 0 -.5px 0 0 rgba(0,0,0,0.2), 0 .5px 0 0 rgba(255,255,255,0.6); 19 | 20 | nav { 21 | display: inline-block; 22 | padding: 4px 7px; 23 | 24 | a { 25 | display: inline-block; 26 | margin: 2px; 27 | padding: 4px 10px; 28 | background-color: rgba(255,255,255,0); 29 | border-radius: 1em; 30 | color: #6b1d8f; 31 | text-decoration: none; 32 | // transition: all 250ms ease; 33 | transition: all 250ms cubic-bezier(.2,0,.4,2); 34 | &:hover { 35 | background-color: rgba(255,255,255,1); 36 | box-shadow: 0 0 0 2px #6b1d8f; 37 | } 38 | &.active { 39 | background-color: #6b1d8f; 40 | color: white; 41 | } 42 | } 43 | } 44 | } 45 | 46 | > main { 47 | flex: 1; 48 | padding: 10px; 49 | } 50 | } 51 | 52 | 53 | h1 { 54 | margin: 0; 55 | color: #6b1d8f; 56 | font-weight: 300; 57 | font-size: 250%; 58 | } 59 | 60 | 61 | input, textarea { 62 | box-sizing: border-box; 63 | margin: 1px; 64 | padding: .25em .5em; 65 | background: #fff; 66 | border: 1px solid #999; 67 | border-radius: 3px; 68 | font: inherit; 69 | color: #000; 70 | outline: none; 71 | 72 | &:focus { 73 | border-color: #6b1d8f; 74 | } 75 | } 76 | 77 | 78 | button, input[type="submit"], input[type="reset"], input[type="button"] { 79 | box-sizing: border-box; 80 | margin: 1px; 81 | padding: .25em .8em; 82 | background: #6b1d8f; 83 | border: 1px solid #6b1d8f; 84 | // border: none; 85 | border-radius: 1.5em; 86 | font: inherit; 87 | color: white; 88 | outline: none; 89 | cursor: pointer; 90 | } 91 | 92 | 93 | .cursor { 94 | position: absolute; 95 | left: 0; 96 | top: 0; 97 | width: 8px; 98 | height: 8px; 99 | margin: -5px 0 0 -5px; 100 | border: 2px solid #F00; 101 | border-radius: 50%; 102 | transform-origin: 50% 50%; 103 | pointer-events: none; 104 | overflow: hidden; 105 | font-size: 9px; 106 | line-height: 25px; 107 | text-indent: 15px; 108 | white-space: nowrap; 109 | 110 | &:not(.label) { 111 | contain: strict; 112 | } 113 | 114 | &.label { 115 | overflow: visible; 116 | } 117 | 118 | // &.big { 119 | // transform: scale(2); 120 | // // width: 24px; 121 | // // height: 24px; 122 | // // margin: -13px 0 0 -13px; 123 | // } 124 | 125 | .label { 126 | position: absolute; 127 | left: 0; 128 | top: 0; 129 | //transform: translateZ(0); 130 | // z-index: 10; 131 | } 132 | } 133 | 134 | 135 | .animation-picker { 136 | position: fixed; 137 | display: inline-block; 138 | right: 0; 139 | top: 0; 140 | padding: 10px; 141 | background: #000; 142 | color: #BBB; 143 | z-index: 1000; 144 | 145 | select { 146 | font-size: 100%; 147 | margin-left: 5px; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /benches/src/many_updates.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Patching HTML 7 | 16 | 17 | 18 |
    19 | 109 | 110 | 111 | --------------------------------------------------------------------------------