,
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 |
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 |
Toggle
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 | {
34 | display = !display;
35 | store.dispatch({ type: 1, display });
36 | }}
37 | >
38 | Toggle visibility
39 |
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 |
42 | Remove contact
43 |
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 |
Re-render
22 |
Swap 2 & 8
23 |
24 |
25 | Use Keys
26 |
27 |
28 |
29 | Counts
30 |
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(, scratch);
18 |
19 | expect(scratch.firstElementChild.value).to.equal('foo');
20 | });
21 |
22 | it('should alias defaultValue to children', () => {
23 | render(, scratch);
24 |
25 | expect(scratch.firstElementChild.value).to.equal('foo');
26 | });
27 |
28 | it('should support resetting the value', () => {
29 | let set;
30 | const App = () => {
31 | const [state, setState] = useState('');
32 | set = setState;
33 | return ;
34 | };
35 |
36 | render( , scratch);
37 | expect(scratch.innerHTML).to.equal('');
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 value', () => {
18 | function App() {
19 | return (
20 |
21 | A
22 | B
23 | C
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 |
36 | A
37 |
38 | B
39 |
40 | C
41 |
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 |
53 | A
54 |
55 | B
56 |
57 |
58 | C
59 |
60 |
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 |
11 | {
15 | childFirst = evt.target.checked;
16 | }}
17 | />
18 | Set child state before parent state.
19 |
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 |
20 |
21 | Sort by{" "}
22 | {
25 | store.setUsersOrder(ev.target.value)
26 | }}
27 | >
28 | Name
29 | ID
30 |
31 |
32 |
33 | {store.getSortedUsers().map((user, i) => (
34 |
42 |
43 |
44 | {user.name.first} {user.name.last}
45 |
46 |
47 | ))}
48 |
49 |
50 |
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 |
Force re-render
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 |
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 |
56 |
57 | }>
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
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 |
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 |
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 |
55 | {this.state.count}
56 |
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('0 ');
70 |
71 | buttonRef.current.dispatchEvent(createEvent('click'));
72 | rerender();
73 | expect(scratch.innerHTML).to.equal('1 ');
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 | Rerun
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 => ``;
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 => ``;
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 test: {state.time}
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 |
{Date.now()}
96 |
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 |
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 |
--------------------------------------------------------------------------------