diff options
author | Florian Dold <florian.dold@gmail.com> | 2016-11-08 15:07:07 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2016-11-08 15:19:39 +0100 |
commit | afb9fba64be1f15a3ce3ed31214a704e73e5e8bb (patch) | |
tree | 6f69712a8c976178c05144483ff0c8e9b09445c8 /thirdparty/preact | |
parent | b37e7762bb5492cbd6788863232e7d2634ab5e5c (diff) | |
parent | 6e5fb04d3f3f9a6cd43ac20896d73321dd079f96 (diff) |
Update preact version
Diffstat (limited to 'thirdparty/preact')
25 files changed, 979 insertions, 153 deletions
diff --git a/thirdparty/preact/.gitignore b/thirdparty/preact/.gitignore index 17acf3f20..9fe87e641 100644 --- a/thirdparty/preact/.gitignore +++ b/thirdparty/preact/.gitignore @@ -4,5 +4,7 @@ /dist /_dev /coverage -aliases.js -aliases.js.map + +# Additional bundles +/*.js +/*.js.map diff --git a/thirdparty/preact/.travis.yml b/thirdparty/preact/.travis.yml index 5953c1a84..8a5ff0a0d 100644 --- a/thirdparty/preact/.travis.yml +++ b/thirdparty/preact/.travis.yml @@ -9,13 +9,19 @@ cache: directories: - node_modules +# Make chrome browser available for testing +before_install: + - export CHROME_BIN=chromium-browser + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + install: - npm install script: - npm run build - npm run test - - SAUCELABS=true COVERAGE=false FLAKEY=false PERFORMANCE=false npm run test:karma + - BROWSER=true COVERAGE=false FLAKEY=false PERFORMANCE=false npm run test:karma # Necessary to compile native modules for io.js v3 or Node.js v4 env: diff --git a/thirdparty/preact/README.md b/thirdparty/preact/README.md index 582c55216..38f0f1b3a 100644 --- a/thirdparty/preact/README.md +++ b/thirdparty/preact/README.md @@ -2,13 +2,14 @@ <img alt="Preact" title="Preact" src="https://cdn.rawgit.com/developit/b4416d5c92b743dbaec1e68bc4c27cda/raw/3235dc508f7eb834ebf48418aea212a05df13db1/preact-logo-trans.svg" width="550"> </a> -**Preact is a fast, `3kb` alternative to React, with the same ES2015 API.** +**Preact is a fast, `3kB` alternative to React, with the same ES2015 API.** Preact retains a large amount of compatibility with React, but only the modern ([ES6 Classes] and [stateless functional components](https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html#stateless-functional-components)) interfaces. As one would expect coming from React, Components are simple building blocks for composing a User Interface. ### :information_desk_person: Full documentation is available at the [Preact Website ➞](https://preactjs.com) +[![CDNJS](https://img.shields.io/cdnjs/v/preact.svg)](https://cdnjs.com/libraries/preact) [![npm](https://img.shields.io/npm/v/preact.svg)](http://npm.im/preact) [![travis](https://travis-ci.org/developit/preact.svg?branch=master)](https://travis-ci.org/developit/preact) [![gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/developit/preact) @@ -24,7 +25,7 @@ As one would expect coming from React, Components are simple building blocks for - [**ESBench**](http://esbench.com) is built using Preact. - [**Nectarine.rocks**](http://nectarine.rocks) _([Github Project](https://github.com/developit/nectarine))_ :peach: - [**Documentation Viewer**](https://documentation-viewer.firebaseapp.com) _([Github Project](https://github.com/developit/documentation-viewer))_ -- [**TodoMVC**](http://developit.github.io/preact-todomvc/) _([Github Project](https://github.com/developit/preact-todomvc))_ +- [**TodoMVC**](https://preact-todomvc.surge.sh) _([Github Project](https://github.com/developit/preact-todomvc))_ - [**Hacker News Minimal**](https://developit.github.io/hn_minimal/) _([Github Project](https://github.com/developit/hn_minimal))_ - [**Preact Boilerplate**](https://preact-boilerplate.surge.sh) _([Github Project](https://github.com/developit/preact-boilerplate))_ :zap: - [**Preact Redux Example**](https://github.com/developit/preact-redux-example) :star: @@ -41,17 +42,25 @@ As one would expect coming from React, Components are simple building blocks for ## Libraries & Add-ons -- :earth_americas: [**preact-router**](https://git.io/preact-router): URL routing for your components. +- :raised_hands: [**preact-compat**](https://git.io/preact-compat): use any React library with Preact *([full example](http://git.io/preact-compat-example))* +- :repeat: [**preact-cycle**](https://git.io/preact-cycle): Functional-reactive paradigm for Preact - :page_facing_up: [**preact-render-to-string**](https://git.io/preact-render-to-string): Universal rendering. -- :raised_hands: [**preact-compat**](https://git.io/preact-compat): use any React library with Preact. *([full example](http://git.io/preact-compat-example))* -- :rocket: [**preact-photon**](https://git.io/preact-photon): build beautiful desktop UI with [photon](http://photonkit.com). -- :microscope: [**preact-jsx-chai**](https://git.io/preact-jsx-chai): JSX assertion testing _(no DOM, right in Node)_ +- :earth_americas: [**preact-router**](https://git.io/preact-router): URL routing for your components - :bookmark_tabs: [**preact-markup**](https://git.io/preact-markup): Render HTML & Custom Elements as JSX & Components -- :pencil: [**preact-richtextarea**](https://git.io/preact-richtextarea): Simple HTML editor component -- :repeat: [**preact-cycle**](https://git.io/preact-cycle): Functional-reactive paradigm for Preact. - :satellite: [**preact-portal**](https://git.io/preact-portal): Render Preact components into (a) SPACE :milky_way: -- :construction: [**preact-classless-component**](https://github.com/ld0rman/preact-classless-component): A utility method to create components without the `class` keyword +- :pencil: [**preact-richtextarea**](https://git.io/preact-richtextarea): Simple HTML editor component +- :bookmark: [**preact-token-input**](https://github.com/developit/preact-token-input): Text field that tokenizes input, for things like tags +- :card_index: [**preact-virtual-list**](https://github.com/developit/preact-virtual-list): Easily render lists with millions of rows ([demo](https://jsfiddle.net/developit/qqan9pdo/)) +- :triangular_ruler: [**preact-layout**](https://download.github.io/preact-layout/): Small and simple layout library +- :thought_balloon: [**preact-socrates**](https://github.com/matthewmueller/preact-socrates): Preact plugin for [Socrates](http://github.com/matthewmueller/socrates) +- :rowboat: [**preact-flyd**](https://github.com/xialvjun/preact-flyd): Use [flyd](https://github.com/paldepind/flyd) FRP streams in Preact + JSX +- :speech_balloon: [**preact-i18nline**](https://github.com/download/preact-i18nline): Integrates the ecosystem around [i18n-js](https://github.com/everydayhero/i18n-js) with Preact via [i18nline](https://github.com/download/i18nline). +- :white_square_button: [**preact-mdl**](https://git.io/preact-mdl): Use [MDL](https://getmdl.io) as Preact components +- :rocket: [**preact-photon**](https://git.io/preact-photon): build beautiful desktop UI with [photon](http://photonkit.com) +- :microscope: [**preact-jsx-chai**](https://git.io/preact-jsx-chai): JSX assertion testing _(no DOM, right in Node)_ +- :tophat: [**preact-classless-component**](https://github.com/ld0rman/preact-classless-component): create preact components without the class keyword - :hammer: [**preact-hyperscript**](https://github.com/queckezz/preact-hyperscript): Hyperscript-like syntax for creating elements +- :white_check_mark: [**shallow-compare**](https://github.com/tkh44/shallow-compare): simplified `shouldComponentUpdate` helper. ## Getting Started @@ -328,6 +337,24 @@ class MixedComponent extends Component { } ``` +## Developer Tools + +You can inspect and modify the state of your Preact UI components at runtime using the +[React Developer Tools](https://github.com/facebook/react-devtools) browser extension. + +1. Install the [React Developer Tools](https://github.com/facebook/react-devtools) extension +2. Import the "preact/devtools" module in your app +3. Reload and go to the 'React' tab in the browser's development tools + + +```js +import { h, Component, render } from 'preact'; + +// Enable devtools. You can reduce the size of your app by only including this +// module in development builds. eg. In Webpack, wrap this with an `if (module.hot) {...}` +// check. +require('preact/devtools'); +``` ## License diff --git a/thirdparty/preact/config/rollup.config.devtools.js b/thirdparty/preact/config/rollup.config.devtools.js new file mode 100644 index 000000000..1fb90b238 --- /dev/null +++ b/thirdparty/preact/config/rollup.config.devtools.js @@ -0,0 +1,20 @@ +import nodeResolve from 'rollup-plugin-node-resolve'; +import babel from 'rollup-plugin-babel'; + +export default { + entry: 'devtools/index.js', + external: ['preact'], + format: 'umd', + globals: { + preact: 'preact' + }, + moduleName: 'preactDevTools', + plugins: [ + babel({ + sourceMap: true, + loose: 'all', + blacklist: ['es6.tailCall'], + exclude: 'node_modules/**' + }) + ] +} diff --git a/thirdparty/preact/devtools/devtools.js b/thirdparty/preact/devtools/devtools.js new file mode 100644 index 000000000..4bcbeae1e --- /dev/null +++ b/thirdparty/preact/devtools/devtools.js @@ -0,0 +1,427 @@ +/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */ + +import { options, Component } from 'preact'; + +// Internal helpers from preact +import { ATTR_KEY } from '../src/constants'; +import { isFunctionalComponent } from '../src/vdom/functional-component'; + +/** + * Return a ReactElement-compatible object for the current state of a preact + * component. + */ +function createReactElement(component) { + return { + type: component.constructor, + key: component.key, + ref: null, // Unsupported + props: component.props + }; +} + +/** + * Create a ReactDOMComponent-compatible object for a given DOM node rendered + * by preact. + * + * This implements the subset of the ReactDOMComponent interface that + * React DevTools requires in order to display DOM nodes in the inspector with + * the correct type and properties. + * + * @param {Node} node + */ +function createReactDOMComponent(node) { + const childNodes = node.nodeType === Node.ELEMENT_NODE ? + Array.from(node.childNodes) : []; + + const isText = node.nodeType === Node.TEXT_NODE; + + return { + // --- ReactDOMComponent interface + _currentElement: isText ? node.textContent : { + type: node.nodeName.toLowerCase(), + props: node[ATTR_KEY] + }, + _renderedChildren: childNodes.map(child => { + if (child._component) { + return updateReactComponent(child._component); + } + return updateReactComponent(child); + }), + _stringText: isText ? node.textContent : null, + + // --- Additional properties used by preact devtools + + // A flag indicating whether the devtools have been notified about the + // existence of this component instance yet. + // This is used to send the appropriate notifications when DOM components + // are added or updated between composite component updates. + _inDevTools: false, + node + }; +} + +/** + * Return the name of a component created by a `ReactElement`-like object. + * + * @param {ReactElement} element + */ +function typeName(element) { + if (typeof element.type === 'function') { + return element.type.displayName || element.type.name; + } + return element.type; +} + +/** + * Return a ReactCompositeComponent-compatible object for a given preact + * component instance. + * + * This implements the subset of the ReactCompositeComponent interface that + * the DevTools requires in order to walk the component tree and inspect the + * component's properties. + * + * See https://github.com/facebook/react-devtools/blob/e31ec5825342eda570acfc9bcb43a44258fceb28/backend/getData.js + */ +function createReactCompositeComponent(component) { + const _currentElement = createReactElement(component); + const node = component.base; + + let instance = { + // --- ReactDOMComponent properties + getName() { + return typeName(_currentElement); + }, + _currentElement: createReactElement(component), + props: component.props, + state: component.state, + forceUpdate: component.forceUpdate.bind(component), + setState: component.setState.bind(component), + + // --- Additional properties used by preact devtools + node + }; + + // React DevTools exposes the `_instance` field of the selected item in the + // component tree as `$r` in the console. `_instance` must refer to a + // React Component (or compatible) class instance with `props` and `state` + // fields and `setState()`, `forceUpdate()` methods. + instance._instance = component; + + // If the root node returned by this component instance's render function + // was itself a composite component, there will be a `_component` property + // containing the child component instance. + if (component._component) { + instance._renderedComponent = updateReactComponent(component._component); + } else { + // Otherwise, if the render() function returned an HTML/SVG element, + // create a ReactDOMComponent-like object for the DOM node itself. + instance._renderedComponent = updateReactComponent(node); + } + + return instance; +} + +/** + * Map of Component|Node to ReactDOMComponent|ReactCompositeComponent-like + * object. + * + * The same React*Component instance must be used when notifying devtools + * about the initial mount of a component and subsequent updates. + */ +let instanceMap = new Map(); + +/** + * Update (and create if necessary) the ReactDOMComponent|ReactCompositeComponent-like + * instance for a given preact component instance or DOM Node. + * + * @param {Component|Node} componentOrNode + */ +function updateReactComponent(componentOrNode) { + const newInstance = componentOrNode instanceof Node ? + createReactDOMComponent(componentOrNode) : + createReactCompositeComponent(componentOrNode); + if (instanceMap.has(componentOrNode)) { + let inst = instanceMap.get(componentOrNode); + Object.assign(inst, newInstance); + return inst; + } + instanceMap.set(componentOrNode, newInstance); + return newInstance; +} + +function nextRootKey(roots) { + return '.' + Object.keys(roots).length; +} + +/** + * Find all root component instances rendered by preact in `node`'s children + * and add them to the `roots` map. + * + * @param {DOMElement} node + * @param {[key: string] => ReactDOMComponent|ReactCompositeComponent} + */ +function findRoots(node, roots) { + Array.from(node.childNodes).forEach(child => { + if (child._component) { + roots[nextRootKey(roots)] = updateReactComponent(child._component); + } else { + findRoots(child, roots); + } + }); +} + +/** + * Map of functional component name -> wrapper class. + */ +let functionalComponentWrappers = new Map(); + +/** + * Wrap a functional component with a stateful component. + * + * preact does not record any information about the original hierarchy of + * functional components in the rendered DOM nodes. Wrapping functional components + * with a trivial wrapper allows us to recover information about the original + * component structure from the DOM. + * + * @param {VNode} vnode + */ +function wrapFunctionalComponent(vnode) { + const originalRender = vnode.nodeName; + const name = vnode.nodeName.name || '(Function.name missing)'; + const wrappers = functionalComponentWrappers; + if (!wrappers.has(originalRender)) { + let wrapper = class extends Component { + render(props, state, context) { + return originalRender(props, context); + } + }; + + // Expose the original component name. React Dev Tools will use + // this property if it exists or fall back to Function.name + // otherwise. + wrapper.displayName = name; + + wrappers.set(originalRender, wrapper); + } + vnode.nodeName = wrappers.get(originalRender); +} + +/** + * Create a bridge for exposing preact's component tree to React DevTools. + * + * It creates implementations of the interfaces that ReactDOM passes to + * devtools to enable it to query the component tree and hook into component + * updates. + * + * See https://github.com/facebook/react/blob/59ff7749eda0cd858d5ee568315bcba1be75a1ca/src/renderers/dom/ReactDOM.js + * for how ReactDOM exports its internals for use by the devtools and + * the `attachRenderer()` function in + * https://github.com/facebook/react-devtools/blob/e31ec5825342eda570acfc9bcb43a44258fceb28/backend/attachRenderer.js + * for how the devtools consumes the resulting objects. + */ +function createDevToolsBridge() { + // The devtools has different paths for interacting with the renderers from + // React Native, legacy React DOM and current React DOM. + // + // Here we emulate the interface for the current React DOM (v15+) lib. + + // ReactDOMComponentTree-like object + const ComponentTree = { + getNodeFromInstance(instance) { + return instance.node; + }, + getClosestInstanceFromNode(node) { + while (node && !node._component) { + node = node.parentNode; + } + return node ? updateReactComponent(node._component) : null; + } + }; + + // Map of root ID (the ID is unimportant) to component instance. + let roots = {}; + findRoots(document.body, roots); + + // ReactMount-like object + // + // Used by devtools to discover the list of root component instances and get + // notified when new root components are rendered. + const Mount = { + _instancesByReactRootID: roots, + + // Stub - React DevTools expects to find this method and replace it + // with a wrapper in order to observe new root components being added + _renderNewRootComponent(/* instance, ... */) { } + }; + + // ReactReconciler-like object + const Reconciler = { + // Stubs - React DevTools expects to find these methods and replace them + // with wrappers in order to observe components being mounted, updated and + // unmounted + mountComponent(/* instance, ... */) { }, + performUpdateIfNecessary(/* instance, ... */) { }, + receiveComponent(/* instance, ... */) { }, + unmountComponent(/* instance, ... */) { } + }; + + /** Notify devtools that a new component instance has been mounted into the DOM. */ + const componentAdded = component => { + const instance = updateReactComponent(component); + if (isRootComponent(component)) { + instance._rootID = nextRootKey(roots); + roots[instance._rootID] = instance; + Mount._renderNewRootComponent(instance); + } + visitNonCompositeChildren(instance, childInst => { + childInst._inDevTools = true; + Reconciler.mountComponent(childInst); + }); + Reconciler.mountComponent(instance); + }; + + /** Notify devtools that a component has been updated with new props/state. */ + const componentUpdated = component => { + const prevRenderedChildren = []; + visitNonCompositeChildren(instanceMap.get(component), childInst => { + prevRenderedChildren.push(childInst); + }); + + // Notify devtools about updates to this component and any non-composite + // children + const instance = updateReactComponent(component); + Reconciler.receiveComponent(instance); + visitNonCompositeChildren(instance, childInst => { + if (!childInst._inDevTools) { + // New DOM child component + childInst._inDevTools = true; + Reconciler.mountComponent(childInst); + } else { + // Updated DOM child component + Reconciler.receiveComponent(childInst); + } + }); + + // For any non-composite children that were removed by the latest render, + // remove the corresponding ReactDOMComponent-like instances and notify + // the devtools + prevRenderedChildren.forEach(childInst => { + if (!document.body.contains(childInst.node)) { + instanceMap.delete(childInst.node); + Reconciler.unmountComponent(childInst); + } + }); + }; + + /** Notify devtools that a component has been unmounted from the DOM. */ + const componentRemoved = component => { + const instance = updateReactComponent(component); + visitNonCompositeChildren(childInst => { + instanceMap.delete(childInst.node); + Reconciler.unmountComponent(childInst); + }); + Reconciler.unmountComponent(instance); + instanceMap.delete(component); + if (instance._rootID) { + delete roots[instance._rootID]; + } + }; + + return { + componentAdded, + componentUpdated, + componentRemoved, + + // Interfaces passed to devtools via __REACT_DEVTOOLS_GLOBAL_HOOK__.inject() + ComponentTree, + Mount, + Reconciler + }; +} + +/** + * Return `true` if a preact component is a top level component rendered by + * `render()` into a container Element. + */ +function isRootComponent(component) { + return !component.base.parentElement || !component.base.parentElement[ATTR_KEY]; +} + +/** + * Visit all child instances of a ReactCompositeComponent-like object that are + * not composite components (ie. they represent DOM elements or text) + * + * @param {Component} component + * @param {(Component) => void} visitor + */ +function visitNonCompositeChildren(component, visitor) { + if (component._renderedComponent) { + if (!component._renderedComponent._component) { + visitor(component._renderedComponent); + visitNonCompositeChildren(component._renderedComponent, visitor); + } + } else if (component._renderedChildren) { + component._renderedChildren.forEach(child => { + visitor(child); + if (!child._component) visitNonCompositeChildren(child, visitor); + }); + } +} + +/** + * Create a bridge between the preact component tree and React's dev tools + * and register it. + * + * After this function is called, the React Dev Tools should be able to detect + * "React" on the page and show the component tree. + * + * This function hooks into preact VNode creation in order to expose functional + * components correctly, so it should be called before the root component(s) + * are rendered. + * + * Returns a cleanup function which unregisters the hooks. + */ +export function initDevTools() { + if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') { + // React DevTools are not installed + return; + } + + // Hook into preact element creation in order to wrap functional components + // with stateful ones in order to make them visible in the devtools + const nextVNode = options.vnode; + options.vnode = (vnode) => { + if (isFunctionalComponent(vnode)) wrapFunctionalComponent(vnode); + if (nextVNode) return nextVNode(vnode); + }; + + // Notify devtools when preact components are mounted, updated or unmounted + const bridge = createDevToolsBridge(); + + const nextAfterMount = options.afterMount; + options.afterMount = component => { + bridge.componentAdded(component); + if (nextAfterMount) nextAfterMount(component); + }; + + const nextAfterUpdate = options.afterUpdate; + options.afterUpdate = component => { + bridge.componentUpdated(component); + if (nextAfterUpdate) nextAfterUpdate(component); + }; + + const nextBeforeUnmount = options.beforeUnmount; + options.beforeUnmount = component => { + bridge.componentRemoved(component); + if (nextBeforeUnmount) nextBeforeUnmount(component); + }; + + // Notify devtools about this instance of "React" + __REACT_DEVTOOLS_GLOBAL_HOOK__.inject(bridge); + + return () => { + options.afterMount = nextAfterMount; + options.afterUpdate = nextAfterUpdate; + options.beforeUnmount = nextBeforeUnmount; + }; +} diff --git a/thirdparty/preact/devtools/index.js b/thirdparty/preact/devtools/index.js new file mode 100644 index 000000000..7c361c8ee --- /dev/null +++ b/thirdparty/preact/devtools/index.js @@ -0,0 +1,4 @@ +import { initDevTools } from './devtools'; + +initDevTools(); + diff --git a/thirdparty/preact/package.json b/thirdparty/preact/package.json index 293597fae..eba36d0fe 100644 --- a/thirdparty/preact/package.json +++ b/thirdparty/preact/package.json @@ -1,7 +1,7 @@ { "name": "preact", "amdName": "preact", - "version": "6.2.1", + "version": "6.4.0", "description": "Tiny & fast Component-based virtual DOM framework.", "main": "dist/preact.js", "jsnext:main": "src/preact.js", @@ -9,25 +9,27 @@ "dev:main": "dist/preact.dev.js", "minified:main": "dist/preact.min.js", "scripts": { - "clean": "rimraf dist/ $npm_package_aliases_main ${npm_package_aliases_main}.map", - "copy-flow-definition": "cp src/preact.js.flow dist/preact.js.flow", - "copy-typescript-definition": "cp src/preact.d.ts dist/preact.d.ts", + "clean": "rimraf dist/ aliases.js aliases.js.map devtools.js devtools.js.map", + "copy-flow-definition": "copyfiles src/preact.js.flow dist/preact.js.flow", + "copy-typescript-definition": "copyfiles src/preact.d.ts dist/preact.d.ts", "build": "npm-run-all --silent clean transpile copy-flow-definition copy-typescript-definition strip optimize minify size", - "transpile:main": "rollup -c config/rollup.config.js -m ${npm_package_dev_main}.map -f umd -n $npm_package_amdName $npm_package_jsnext_main -o $npm_package_dev_main", - "transpile:aliases": "rollup -c config/rollup.config.aliases.js -m ${npm_package_aliases_main}.map -f umd -n $npm_package_amdName $npm_package_jsnext_main -o $npm_package_aliases_main", - "transpile": "npm-run-all transpile:main transpile:aliases", - "optimize": "uglifyjs $npm_package_dev_main -c conditionals=false,sequences=false,loops=false,join_vars=false,collapse_vars=false --pure-funcs=Object.defineProperty -b width=120,quote_style=3 -o $npm_package_main -p relative --in-source-map ${npm_package_dev_main}.map --source-map ${npm_package_main}.map", - "minify": "uglifyjs $npm_package_main -c collapse_vars,evaluate,screw_ie8,unsafe,loops=false,keep_fargs=false,pure_getters,unused,dead_code -m -o $npm_package_minified_main -p relative --in-source-map ${npm_package_main}.map --source-map ${npm_package_minified_main}.map", + "transpile:main": "rollup -c config/rollup.config.js -m dist/preact.dev.js.map -f umd -n preact src/preact.js -o dist/preact.dev.js", + "transpile:devtools": "rollup -c config/rollup.config.devtools.js -o devtools.js -m devtools.js.map", + "transpile:aliases": "rollup -c config/rollup.config.aliases.js -m aliases.js.map -f umd -n preact src/preact.js -o aliases.js", + "transpile": "npm-run-all transpile:main transpile:aliases transpile:devtools", + "optimize": "uglifyjs dist/preact.dev.js -c conditionals=false,sequences=false,loops=false,join_vars=false,collapse_vars=false --pure-funcs=Object.defineProperty -b width=120,quote_style=3 -o dist/preact.js -p relative --in-source-map dist/preact.dev.js.map --source-map dist/preact.js.map", + "minify": "uglifyjs dist/preact.js -c collapse_vars,evaluate,screw_ie8,unsafe,loops=false,keep_fargs=false,pure_getters,unused,dead_code -m -o dist/preact.min.js -p relative --in-source-map dist/preact.js.map --source-map dist/preact.min.js.map", "strip": "jscodeshift --run-in-band -s -t config/codemod-strip-tdz.js dist/preact.dev.js && jscodeshift --run-in-band -s -t config/codemod-const.js dist/preact.dev.js", - "size": "size=$(gzip-size $npm_package_minified_main) && echo \"gzip size: $size / $(pretty-bytes $size)\"", + "size": "node -e \"process.stdout.write('gzip size: ')\" && gzip-size dist/preact.min.js", "test": "npm-run-all lint --parallel test:mocha test:karma", "test:mocha": "mocha --recursive --compilers js:babel/register test/shared test/node", "test:karma": "karma start test/karma.conf.js --single-run", "test:mocha:watch": "npm run test:mocha -- --watch", "test:karma:watch": "npm run test:karma -- no-single-run", - "lint": "eslint src test", + "lint": "eslint devtools src test", "prepublish": "npm run build", - "release": "npm run build && npm test && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish" + "smart-release": "npm run build && npm test && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish", + "release": "cross-env npm run smart-release" }, "eslintConfig": { "extends": "./config/eslint-config.js" @@ -38,10 +40,13 @@ "url": "https://github.com/developit/preact.git" }, "files": [ + "devtools", "src", "dist", "aliases.js", "aliases.js.map", + "devtools.js", + "devtools.js.map", "typings.json" ], "author": "Jason Miller <jason@developit.ca>", @@ -57,6 +62,9 @@ "babel-loader": "^5.3.2", "babel-runtime": "^5.8.24", "chai": "^3.4.1", + "copyfiles": "^1.0.0", + "core-js": "^2.4.1", + "cross-env": "^3.1.3", "diff": "^3.0.0", "eslint": "^3.0.0", "eslint-plugin-react": "^6.0.0", @@ -67,18 +75,18 @@ "karma-babel-preprocessor": "^5.2.2", "karma-chai": "^0.1.0", "karma-chai-sinon": "^0.1.5", + "karma-chrome-launcher": "^2.0.0", "karma-coverage": "^1.0.0", "karma-mocha": "^1.1.1", "karma-mocha-reporter": "^2.0.4", "karma-phantomjs-launcher": "^1.0.1", - "karma-sauce-launcher": "^1.0.0", + "karma-sauce-launcher": "^1.1.0", "karma-source-map-support": "^1.1.0", "karma-sourcemap-loader": "^0.3.6", "karma-webpack": "^1.7.0", "mocha": "^3.0.1", "npm-run-all": "^3.0.0", "phantomjs-prebuilt": "^2.1.7", - "pretty-bytes-cli": "^2.0.0", "rimraf": "^2.5.3", "rollup": "^0.34.1", "rollup-plugin-babel": "^1.0.0", diff --git a/thirdparty/preact/src/dom/index.js b/thirdparty/preact/src/dom/index.js index 248a3cdc5..b72d056af 100644 --- a/thirdparty/preact/src/dom/index.js +++ b/thirdparty/preact/src/dom/index.js @@ -1,4 +1,4 @@ -import { ATTR_KEY, NON_DIMENSION_PROPS, NON_BUBBLING_EVENTS } from '../constants'; +import { NON_DIMENSION_PROPS, NON_BUBBLING_EVENTS } from '../constants'; import options from '../options'; import { toLowerCase, isString, isFunction, hashToClassName } from '../util'; @@ -20,8 +20,7 @@ export function removeNode(node) { * @param {any} previousValue The last value that was set for this name/node pair * @private */ -export function setAccessor(node, name, value, old, isSvg) { - node[ATTR_KEY][name] = value; +export function setAccessor(node, name, old, value, isSvg) { if (name==='className') name = 'class'; @@ -29,8 +28,8 @@ export function setAccessor(node, name, value, old, isSvg) { value = hashToClassName(value); } - if (name==='key' || name==='children' || name==='innerHTML') { - // skip these + if (name==='key') { + // ignore } else if (name==='class' && !isSvg) { node.className = value || ''; diff --git a/thirdparty/preact/src/h.js b/thirdparty/preact/src/h.js index e57ce4bde..c137bec84 100644 --- a/thirdparty/preact/src/h.js +++ b/thirdparty/preact/src/h.js @@ -2,8 +2,7 @@ import { VNode } from './vnode'; import options from './options'; -let stack = []; - +const stack = []; /** JSX/hyperscript reviver @@ -16,7 +15,8 @@ let stack = []; * render(<span>foo</span>, document.body); */ export function h(nodeName, attributes) { - let children, lastSimple, child, simple, i; + let children = [], + lastSimple, child, simple, i; for (i=arguments.length; i-- > 2; ) { stack.push(arguments[i]); } @@ -35,8 +35,7 @@ export function h(nodeName, attributes) { children[children.length-1] += child; } else { - if (children) children.push(child); - else children = [child]; + children.push(child); lastSimple = simple; } } diff --git a/thirdparty/preact/src/linked-state.js b/thirdparty/preact/src/linked-state.js index ed72bd8bc..b6959df73 100644 --- a/thirdparty/preact/src/linked-state.js +++ b/thirdparty/preact/src/linked-state.js @@ -8,21 +8,17 @@ import { isString, delve } from './util'; * @private */ export function createLinkedState(component, key, eventPath) { - let path = key.split('.'), - p0 = path[0]; + let path = key.split('.'); return function(e) { - let t = e && e.currentTarget || this, - s = component.state, - obj = s, - v = isString(eventPath) ? delve(e, eventPath) : t.nodeName ? ((t.nodeName+t.type).match(/^input(che|rad)/i) ? t.checked : t.value) : e, - i; - if (path.length>1) { - for (i=0; i<path.length-1; i++) { - obj = obj[path[i]] || (obj[path[i]] = {}); - } - obj[path[i]] = v; - v = s[p0]; + let t = e && e.target || this, + state = {}, + obj = state, + v = isString(eventPath) ? delve(e, eventPath) : t.nodeName ? (t.type.match(/^che|rad/) ? t.checked : t.value) : e, + i = 0; + for ( ; i<path.length-1; i++) { + obj = obj[path[i]] || (obj[path[i]] = !i && component.state[path[i]] || {}); } - component.setState({ [p0]: v }); + obj[path[i]] = v; + component.setState(state); }; } diff --git a/thirdparty/preact/src/options.js b/thirdparty/preact/src/options.js index 35b7418fc..49869604e 100644 --- a/thirdparty/preact/src/options.js +++ b/thirdparty/preact/src/options.js @@ -15,4 +15,13 @@ export default { * @param {VNode} vnode A newly-created VNode to normalize/process */ //vnode(vnode) { } + + /** Hook invoked after a component is mounted. */ + // afterMount(component) { } + + /** Hook invoked after the DOM is updated with a component's latest render. */ + // afterUpdate(component) { } + + /** Hook invoked immediately before a component is unmounted. */ + // beforeUnmount(component) { } }; diff --git a/thirdparty/preact/src/preact.d.ts b/thirdparty/preact/src/preact.d.ts index 2dd8299a9..784844152 100644 --- a/thirdparty/preact/src/preact.d.ts +++ b/thirdparty/preact/src/preact.d.ts @@ -4,8 +4,14 @@ declare namespace preact { key?:string; } + interface DangerouslySetInnerHTML { + __html: string; + } + interface PreactHTMLAttributes { + dangerouslySetInnerHTML?:DangerouslySetInnerHTML; key?:string; + ref?:(el?: Element) => void; } interface VNode { @@ -51,8 +57,8 @@ declare namespace preact { abstract render(props:PropsType & ComponentProps, state:any):JSX.Element; } - function h<PropsType>(node:ComponentConstructor<PropsType, any>, params:PropsType, ...children:(JSX.Element|string)[]):JSX.Element; - function h(node:string, params:JSX.HTMLAttributes&JSX.SVGAttributes, ...children:(JSX.Element|string)[]):JSX.Element; + function h<PropsType>(node:ComponentConstructor<PropsType, any>, params:PropsType, ...children:(JSX.Element|JSX.Element[]|string)[]):JSX.Element; + function h(node:string, params:JSX.HTMLAttributes&JSX.SVGAttributes&{[propName: string]: any}, ...children:(JSX.Element|JSX.Element[]|string)[]):JSX.Element; function render(node:JSX.Element, parent:Element, merge?:boolean):Element; @@ -72,6 +78,11 @@ declare module "preact" { export = preact; } +declare module "preact/devtools" { + // Empty. This module initializes the React Developer Tools integration + // when imported. +} + declare namespace JSX { interface Element extends preact.VNode { @@ -277,8 +288,8 @@ declare namespace JSX { charSet?:string; challenge?:string; checked?:boolean; - class?:string; - className?:string; + class?:string | { [key:string]: boolean }; + className?:string | { [key:string]: boolean }; cols?:number; colSpan?:number; content?:string; @@ -551,4 +562,4 @@ declare namespace JSX { tspan:SVGAttributes; use:SVGAttributes; } -} +}
\ No newline at end of file diff --git a/thirdparty/preact/src/vdom/component.js b/thirdparty/preact/src/vdom/component.js index bb2e4fa5d..64e7ff81f 100644 --- a/thirdparty/preact/src/vdom/component.js +++ b/thirdparty/preact/src/vdom/component.js @@ -154,11 +154,11 @@ export function renderComponent(component, opts, mountAll, isChild) { let baseParent = initialBase.parentNode; if (baseParent && base!==baseParent) { baseParent.replaceChild(base, initialBase); - } - if (!cbase && !toUnmount && component._parentComponent) { - initialBase._component = null; - recollectNodeTree(initialBase); + if (!toUnmount) { + initialBase._component = null; + recollectNodeTree(initialBase); + } } } @@ -170,7 +170,9 @@ export function renderComponent(component, opts, mountAll, isChild) { if (base && !isChild) { let componentRef = component, t = component; - while ((t=t._parentComponent)) { componentRef = t; } + while ((t=t._parentComponent)) { + (componentRef = t).base = base; + } base._component = componentRef; base._componentConstructor = componentRef.constructor; } @@ -179,8 +181,11 @@ export function renderComponent(component, opts, mountAll, isChild) { if (!isUpdate || mountAll) { mounts.unshift(component); } - else if (!skip && component.componentDidUpdate) { - component.componentDidUpdate(previousProps, previousState, previousContext); + else if (!skip) { + if (component.componentDidUpdate) { + component.componentDidUpdate(previousProps, previousState, previousContext); + } + if (options.afterUpdate) options.afterUpdate(component); } let cb = component._renderCallbacks, fn; @@ -218,7 +223,11 @@ export function buildComponentFromVNode(dom, vnode, context, mountAll) { } c = createComponent(vnode.nodeName, props, context); - if (dom && !c.nextBase) c.nextBase = dom; + if (dom && !c.nextBase) { + c.nextBase = dom; + // passing dom/oldDom as nextBase will recycle it if unused, so bypass recycling on L241: + oldDom = null; + } setComponentProps(c, props, SYNC_RENDER, context, mountAll); dom = c.base; @@ -239,6 +248,8 @@ export function buildComponentFromVNode(dom, vnode, context, mountAll) { * @private */ export function unmountComponent(component, remove) { + if (options.beforeUnmount) options.beforeUnmount(component); + // console.log(`${remove?'Removing':'Unmounting'} component: ${component.constructor.name}`); let base = component.base; diff --git a/thirdparty/preact/src/vdom/diff.js b/thirdparty/preact/src/vdom/diff.js index 691434e98..794a79aaa 100644 --- a/thirdparty/preact/src/vdom/diff.js +++ b/thirdparty/preact/src/vdom/diff.js @@ -6,6 +6,7 @@ import { buildComponentFromVNode } from './component'; import { setAccessor } from '../dom/index'; import { createNode, collectNode } from '../dom/recycler'; import { unmountComponent } from './component'; +import options from '../options'; /** Diff recursion count, used to track the end of the diff cycle. */ @@ -20,6 +21,7 @@ let isSvgMode = false; export function flushMounts() { let c; while ((c=mounts.pop())) { + if (options.afterMount) options.afterMount(c); if (c.componentDidMount) c.componentDidMount(); } } @@ -52,7 +54,9 @@ function idiff(dom, vnode, context, mountAll) { if (isString(vnode)) { if (dom) { if (dom instanceof Text && dom.parentNode) { - dom.nodeValue = vnode; + if (dom.nodeValue!=vnode) { + dom.nodeValue = vnode; + } return dom; } recollectNodeTree(dom); @@ -66,7 +70,8 @@ function idiff(dom, vnode, context, mountAll) { let out = dom, nodeName = vnode.nodeName, - prevSvgMode = isSvgMode; + prevSvgMode = isSvgMode, + vchildren = vnode.children; if (!isString(nodeName)) { nodeName = String(nodeName); @@ -86,11 +91,13 @@ function idiff(dom, vnode, context, mountAll) { } // fast-path for elements containing a single TextNode: - if (vnode.children && vnode.children.length===1 && typeof vnode.children[0]==='string' && out.childNodes.length===1 && out.firstChild instanceof Text) { - out.firstChild.nodeValue = vnode.children[0]; + if (vchildren && vchildren.length===1 && typeof vchildren[0]==='string' && out.childNodes.length===1 && out.firstChild instanceof Text) { + if (out.firstChild.nodeValue!=vchildren[0]) { + out.firstChild.nodeValue = vchildren[0]; + } } - else if (vnode.children || out.firstChild) { - innerDiffNode(out, vnode.children, context, mountAll); + else if (vchildren && vchildren.length || out.firstChild) { + innerDiffNode(out, vchildren, context, mountAll); } let props = out[ATTR_KEY]; @@ -232,15 +239,15 @@ export function recollectNodeTree(node, unmountOnly) { function diffAttributes(dom, attrs, old) { for (let name in old) { if (!(attrs && name in attrs) && old[name]!=null) { - setAccessor(dom, name, null, old[name], isSvgMode); + setAccessor(dom, name, old[name], old[name] = undefined, isSvgMode); } } // new & updated if (attrs) { for (let name in attrs) { - if (!(name in old) || attrs[name]!==(name==='value' || name==='checked' ? dom[name] : old[name])) { - setAccessor(dom, name, attrs[name], old[name], isSvgMode); + if (name!=='children' && name!=='innerHTML' && (!(name in old) || attrs[name]!==(name==='value' || name==='checked' ? dom[name] : old[name]))) { + setAccessor(dom, name, old[name], old[name] = attrs[name], isSvgMode); } } } diff --git a/thirdparty/preact/src/vdom/index.js b/thirdparty/preact/src/vdom/index.js index 50d4ca2b9..f59fbae21 100644 --- a/thirdparty/preact/src/vdom/index.js +++ b/thirdparty/preact/src/vdom/index.js @@ -33,9 +33,10 @@ export function isNamedNode(node, nodeName) { * @returns {Object} props */ export function getNodeProps(vnode) { - let defaultProps = vnode.nodeName.defaultProps, - props = clone(vnode.attributes); + let props = clone(vnode.attributes); + props.children = vnode.children; + let defaultProps = vnode.nodeName.defaultProps; if (defaultProps) { for (let i in defaultProps) { if (props[i]===undefined) { @@ -44,7 +45,5 @@ export function getNodeProps(vnode) { } } - if (vnode.children) props.children = vnode.children; - return props; } diff --git a/thirdparty/preact/test/browser/components.js b/thirdparty/preact/test/browser/components.js index b4649a719..9ef43cb1c 100644 --- a/thirdparty/preact/test/browser/components.js +++ b/thirdparty/preact/test/browser/components.js @@ -70,7 +70,7 @@ describe('Components', () => { expect(C3) .to.have.been.calledOnce - .and.to.have.been.calledWith(PROPS) + .and.to.have.been.calledWithMatch(PROPS) .and.to.have.returned(sinon.match({ nodeName: 'div', attributes: PROPS @@ -197,7 +197,7 @@ describe('Components', () => { expect(Outer) .to.have.been.calledOnce - .and.to.have.been.calledWith(PROPS) + .and.to.have.been.calledWithMatch(PROPS) .and.to.have.returned(sinon.match({ nodeName: Inner, attributes: PROPS @@ -205,7 +205,7 @@ describe('Components', () => { expect(Inner) .to.have.been.calledOnce - .and.to.have.been.calledWith(PROPS) + .and.to.have.been.calledWithMatch(PROPS) .and.to.have.returned(sinon.match({ nodeName: 'div', attributes: PROPS, @@ -247,7 +247,7 @@ describe('Components', () => { expect(Inner).to.have.been.calledTwice; expect(Inner.secondCall) - .to.have.been.calledWith({ foo:'bar', i:2 }) + .to.have.been.calledWithMatch({ foo:'bar', i:2 }) .and.to.have.returned(sinon.match({ attributes: { j: 2, @@ -269,7 +269,7 @@ describe('Components', () => { expect(Inner).to.have.been.calledThrice; expect(Inner.thirdCall) - .to.have.been.calledWith({ foo:'bar', i:3 }) + .to.have.been.calledWithMatch({ foo:'bar', i:3 }) .and.to.have.returned(sinon.match({ attributes: { j: 3, @@ -344,7 +344,7 @@ describe('Components', () => { expect(Inner.prototype.render).to.have.been.calledTwice; expect(Inner.prototype.render.secondCall) - .to.have.been.calledWith({ foo:'bar', i:2 }) + .to.have.been.calledWithMatch({ foo:'bar', i:2 }) .and.to.have.returned(sinon.match({ attributes: { j: 2, @@ -372,7 +372,7 @@ describe('Components', () => { expect(Inner.prototype.render).to.have.been.calledThrice; expect(Inner.prototype.render.thirdCall) - .to.have.been.calledWith({ foo:'bar', i:3 }) + .to.have.been.calledWithMatch({ foo:'bar', i:3 }) .and.to.have.returned(sinon.match({ attributes: { j: 3, @@ -435,7 +435,7 @@ describe('Components', () => { expect(Inner.prototype.componentDidMount).to.have.been.calledOnce; expect(Inner.prototype.componentWillMount).to.have.been.calledBefore(Inner.prototype.componentDidMount); - root = render(<asdf />, scratch, root); + render(<asdf />, scratch, root); expect(Inner.prototype.componentWillUnmount).to.have.been.calledOnce; expect(Inner.prototype.componentDidUnmount).to.have.been.calledOnce; @@ -689,8 +689,7 @@ describe('Components', () => { expect(C1.prototype.componentWillMount, 'unmount innermost w/ intermediary div, C1').not.to.have.been.called; expect(C2.prototype.componentDidUnmount, 'unmount innermost w/ intermediary div, C2 ummount').not.to.have.been.called; - // @TODO this was just incorrect? - // expect(C2.prototype.componentWillMount, 'unmount innermost w/ intermediary div, C2').not.to.have.been.called; + expect(C2.prototype.componentWillMount, 'unmount innermost w/ intermediary div, C2').not.to.have.been.called; expect(C3.prototype.componentDidUnmount, 'unmount innermost w/ intermediary div, C3').to.have.been.calledOnce; reset(); diff --git a/thirdparty/preact/test/browser/context.js b/thirdparty/preact/test/browser/context.js index e62a948a4..ed5f81471 100644 --- a/thirdparty/preact/test/browser/context.js +++ b/thirdparty/preact/test/browser/context.js @@ -1,6 +1,8 @@ import { h, render, Component } from '../../src/preact'; /** @jsx h */ +const CHILDREN_MATCHER = sinon.match( v => v==null || Array.isArray(v) && !v.length , '[empty children]'); + describe('context', () => { let scratch; @@ -57,18 +59,19 @@ describe('context', () => { expect(Outer.prototype.getChildContext).to.have.been.calledOnce; // initial render does not invoke anything but render(): - expect(Inner.prototype.render).to.have.been.calledWith({}, {}, CONTEXT); + expect(Inner.prototype.render).to.have.been.calledWith({ children:CHILDREN_MATCHER }, {}, CONTEXT); CONTEXT.foo = 'bar'; render(<Outer {...PROPS} />, scratch, scratch.lastChild); expect(Outer.prototype.getChildContext).to.have.been.calledTwice; - expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(PROPS, {}, CONTEXT); - expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(PROPS, CONTEXT); - expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(PROPS, {}); - expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({}, {}); - expect(Inner.prototype.render).to.have.been.calledWith(PROPS, {}, CONTEXT); + let props = { children: CHILDREN_MATCHER, ...PROPS }; + expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(props, {}, CONTEXT); + expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(props, CONTEXT); + expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(props, {}); + expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({ children:CHILDREN_MATCHER }, {}); + expect(Inner.prototype.render).to.have.been.calledWith(props, {}, CONTEXT); /* Future: @@ -115,18 +118,19 @@ describe('context', () => { expect(Outer.prototype.getChildContext).to.have.been.calledOnce; // initial render does not invoke anything but render(): - expect(Inner.prototype.render).to.have.been.calledWith({}, {}, CONTEXT); + expect(Inner.prototype.render).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {}, CONTEXT); CONTEXT.foo = 'bar'; render(<Outer {...PROPS} />, scratch, scratch.lastChild); expect(Outer.prototype.getChildContext).to.have.been.calledTwice; - expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(PROPS, {}, CONTEXT); - expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(PROPS, CONTEXT); - expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(PROPS, {}); - expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({}, {}); - expect(Inner.prototype.render).to.have.been.calledWith(PROPS, {}, CONTEXT); + let props = { children: CHILDREN_MATCHER, ...PROPS }; + expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(props, {}, CONTEXT); + expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(props, CONTEXT); + expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(props, {}); + expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {}); + expect(Inner.prototype.render).to.have.been.calledWith(props, {}, CONTEXT); // make sure render() could make use of context.a expect(Inner.prototype.render).to.have.returned(sinon.match({ children:['a'] })); @@ -164,7 +168,7 @@ describe('context', () => { render(<Outer />, scratch); - expect(Inner.prototype.render).to.have.been.calledWith({}, {}, { outerContext }); - expect(InnerMost.prototype.render).to.have.been.calledWith({}, {}, { outerContext, innerContext }); + expect(Inner.prototype.render).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {}, { outerContext }); + expect(InnerMost.prototype.render).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {}, { outerContext, innerContext }); }); }); diff --git a/thirdparty/preact/test/browser/devtools.js b/thirdparty/preact/test/browser/devtools.js new file mode 100644 index 000000000..12c0e3369 --- /dev/null +++ b/thirdparty/preact/test/browser/devtools.js @@ -0,0 +1,234 @@ +import { h, Component, render } from '../../src/preact'; +import { initDevTools } from '../../devtools/devtools'; +import { unmountComponent } from '../../src/vdom/component'; + +class StatefulComponent extends Component { + constructor(props) { + super(props); + + this.state = {count: 0}; + } + + render() { + return h('span', {}, String(this.state.count)); + } +} + +function FunctionalComponent() { + return h('span', {class: 'functional'}, 'Functional'); +} + +function Label({label}) { + return label; +} + +class MultiChild extends Component { + constructor(props) { + super(props); + this.state = {count: props.initialCount}; + } + + render() { + return h('div', {}, Array(this.state.count).fill('child')); + } +} + +let describe_ = describe; +if (!('name' in Function.prototype)) { + // Skip these tests under Internet Explorer + describe_ = describe.skip; +} + +describe_('React Developer Tools integration', () => { + let cleanup; + let container; + let renderer; + + // Maps of DOM node to React*Component-like objects. + // For composite components, there will be two instances for each node, one + // for the composite component (instanceMap) and one for the root child DOM + // component rendered by that component (domInstanceMap) + let instanceMap = new Map(); + let domInstanceMap = new Map(); + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + + const onMount = instance => { + if (instance._renderedChildren) { + domInstanceMap.set(instance.node, instance); + } else { + instanceMap.set(instance.node, instance); + } + }; + + const onUnmount = instance => { + instanceMap.delete(instance.node); + domInstanceMap.delete(instance.node); + }; + + global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { + inject: sinon.spy(_renderer => { + renderer = _renderer; + renderer.Mount._renderNewRootComponent = sinon.stub(); + renderer.Reconciler.mountComponent = sinon.spy(onMount); + renderer.Reconciler.unmountComponent = sinon.spy(onUnmount); + renderer.Reconciler.receiveComponent = sinon.stub(); + }) + }; + cleanup = initDevTools(); + }); + + afterEach(() => { + container.remove(); + cleanup(); + }); + + it('registers preact as a renderer with the React DevTools hook', () => { + expect(global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject).to.be.called; + }); + + // Basic component addition/update/removal tests + it('notifies dev tools about new components', () => { + render(h(StatefulComponent), container); + expect(renderer.Reconciler.mountComponent).to.be.called; + }); + + it('notifies dev tools about component updates', () => { + const node = render(h(StatefulComponent), container); + node._component.forceUpdate(); + expect(renderer.Reconciler.receiveComponent).to.be.called; + }); + + it('notifies dev tools when components are removed', () => { + const node = render(h(StatefulComponent), container); + unmountComponent(node._component, true); + expect(renderer.Reconciler.unmountComponent).to.be.called; + }); + + // Test properties of DOM components exposed to devtools via + // ReactDOMComponent-like instances + it('exposes the tag name of DOM components', () => { + const node = render(h(StatefulComponent), container); + const domInstance = domInstanceMap.get(node); + expect(domInstance._currentElement.type).to.equal('span'); + }); + + it('exposes DOM component props', () => { + const node = render(h(FunctionalComponent), container); + const domInstance = domInstanceMap.get(node); + expect(domInstance._currentElement.props.class).to.equal('functional'); + }); + + it('exposes text component contents', () => { + const node = render(h(Label, {label: 'Text content'}), container); + const textInstance = domInstanceMap.get(node); + expect(textInstance._stringText).to.equal('Text content'); + }); + + // Test properties of composite components exposed to devtools via + // ReactCompositeComponent-like instances + it('exposes the name of composite component classes', () => { + const node = render(h(StatefulComponent), container); + expect(instanceMap.get(node).getName()).to.equal('StatefulComponent'); + }); + + it('exposes composite component props', () => { + const node = render(h(Label, {label: 'Text content'}), container); + const instance = instanceMap.get(node); + expect(instance._currentElement.props.label).to.equal('Text content'); + }); + + it('exposes composite component state', () => { + const node = render(h(StatefulComponent), container); + + node._component.setState({count: 42}); + node._component.forceUpdate(); + + expect(instanceMap.get(node).state).to.deep.equal({count: 42}); + }); + + // Test setting state via devtools + it('updates component when setting state from devtools', () => { + const node = render(h(StatefulComponent), container); + + instanceMap.get(node).setState({count: 10}); + instanceMap.get(node).forceUpdate(); + + expect(node.textContent).to.equal('10'); + }); + + // Test that the original instance is exposed via `_instance` so it can + // be accessed conveniently via `$r` in devtools + + // Functional component handling tests + it('wraps functional components with stateful ones', () => { + const vnode = h(FunctionalComponent); + expect(vnode.nodeName.prototype).to.have.property('render'); + }); + + it('exposes the name of functional components', () => { + const node = render(h(FunctionalComponent), container); + const instance = instanceMap.get(node); + expect(instance.getName()).to.equal('FunctionalComponent'); + }); + + it('exposes a fallback name if the component has no useful name', () => { + const node = render(h(() => h('div')), container); + const instance = instanceMap.get(node); + expect(instance.getName()).to.equal('(Function.name missing)'); + }); + + // Test handling of DOM children + it('notifies dev tools about DOM children', () => { + const node = render(h(StatefulComponent), container); + const domInstance = domInstanceMap.get(node); + expect(renderer.Reconciler.mountComponent).to.have.been.calledWith(domInstance); + }); + + it('notifies dev tools when a component update adds DOM children', () => { + const node = render(h(MultiChild, {initialCount: 2}), container); + + node._component.setState({count: 4}); + node._component.forceUpdate(); + + expect(renderer.Reconciler.mountComponent).to.have.been.called.twice; + }); + + it('notifies dev tools when a component update modifies DOM children', () => { + const node = render(h(StatefulComponent), container); + + instanceMap.get(node).setState({count: 10}); + instanceMap.get(node).forceUpdate(); + + const textInstance = domInstanceMap.get(node.childNodes[0]); + expect(textInstance._stringText).to.equal('10'); + }); + + it('notifies dev tools when a component update removes DOM children', () => { + const node = render(h(MultiChild, {initialCount: 1}), container); + + node._component.setState({count: 0}); + node._component.forceUpdate(); + + expect(renderer.Reconciler.unmountComponent).to.be.called; + }); + + // Root component info + it('exposes root components on the _instancesByReactRootID map', () => { + render(h(StatefulComponent), container); + expect(Object.keys(renderer.Mount._instancesByReactRootID).length).to.equal(1); + }); + + it('notifies dev tools when new root components are mounted', () => { + render(h(StatefulComponent), container); + expect(renderer.Mount._renderNewRootComponent).to.be.called; + }); + + it('removes root components when they are unmounted', () => { + const node = render(h(StatefulComponent), container); + unmountComponent(node._component, true); + expect(Object.keys(renderer.Mount._instancesByReactRootID).length).to.equal(0); + }); +}); diff --git a/thirdparty/preact/test/browser/lifecycle.js b/thirdparty/preact/test/browser/lifecycle.js index d6204ca8f..4deb92163 100644 --- a/thirdparty/preact/test/browser/lifecycle.js +++ b/thirdparty/preact/test/browser/lifecycle.js @@ -3,6 +3,8 @@ import { h, render, rerender, Component } from '../../src/preact'; let spyAll = obj => Object.keys(obj).forEach( key => sinon.spy(obj,key) ); +const EMPTY_CHILDREN = []; + describe('Lifecycle methods', () => { let scratch; @@ -50,7 +52,7 @@ describe('Lifecycle methods', () => { } class Inner extends Component { componentWillUpdate(nextProps, nextState) { - expect(nextProps).to.be.deep.equal({i: 1}); + expect(nextProps).to.be.deep.equal({ children:EMPTY_CHILDREN, i: 1 }); expect(nextState).to.be.deep.equal({}); } render() { diff --git a/thirdparty/preact/test/browser/linked-state.js b/thirdparty/preact/test/browser/linked-state.js index 1ca84cdc6..03db2a7b8 100644 --- a/thirdparty/preact/test/browser/linked-state.js +++ b/thirdparty/preact/test/browser/linked-state.js @@ -26,7 +26,10 @@ describe('linked-state', () => { element.type= 'text'; element.value = 'newValue'; - linkFunction({ currentTarget: element }); + linkFunction({ + currentTarget: element, + target: element + }); expect(TestComponent.prototype.setState).to.have.been.calledOnce; expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': 'newValue'}); @@ -42,7 +45,10 @@ describe('linked-state', () => { checkboxElement.type= 'checkbox'; checkboxElement.checked = true; - linkFunction({ currentTarget: checkboxElement }); + linkFunction({ + currentTarget: checkboxElement, + target: checkboxElement + }); expect(TestComponent.prototype.setState).to.have.been.calledOnce; expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': true}); @@ -53,7 +59,10 @@ describe('linked-state', () => { radioElement.type= 'radio'; radioElement.checked = true; - linkFunction({ currentTarget: radioElement }); + linkFunction({ + currentTarget: radioElement, + target: radioElement + }); expect(TestComponent.prototype.setState).to.have.been.calledOnce; expect(TestComponent.prototype.setState).to.have.been.calledWith({'testStateKey': true}); @@ -66,7 +75,10 @@ describe('linked-state', () => { element.type= 'text'; element.value = 'newValue'; - linkFunction({ currentTarget: element }); + linkFunction({ + currentTarget: element, + target: element + }); expect(TestComponent.prototype.setState).to.have.been.calledOnce; expect(TestComponent.prototype.setState).to.have.been.calledWith({nested: {state: {key: 'newValue'}}}); diff --git a/thirdparty/preact/test/browser/refs.js b/thirdparty/preact/test/browser/refs.js index 89678b76e..337a9717b 100644 --- a/thirdparty/preact/test/browser/refs.js +++ b/thirdparty/preact/test/browser/refs.js @@ -200,8 +200,8 @@ describe('refs', () => { </div> ), scratch); - expect(Foo.prototype.render).to.have.been.calledWithExactly({ a:'a' }, { }, { }); - expect(Bar).to.have.been.calledWithExactly({ b:'b', ref:bar }, { }); + expect(Foo.prototype.render).to.have.been.calledWithMatch({ ref:sinon.match.falsy, a:'a' }, { }, { }); + expect(Bar).to.have.been.calledWithMatch({ b:'b', ref:bar }, { }); }); // Test for #232 @@ -284,4 +284,22 @@ describe('refs', () => { expect(inst.handleMount.firstCall).to.have.been.calledWith(null); expect(inst.handleMount.secondCall).to.have.been.calledWith(scratch.querySelector('#div')); }); + + + it('should add refs to components representing DOM nodes with no attributes if they have been pre-rendered', () => { + // Simulate pre-render + let parent = document.createElement('div'); + let child = document.createElement('div'); + parent.appendChild(child); + scratch.appendChild(parent); // scratch contains: <div><div></div></div> + + let ref = spy('ref'); + + function Wrapper() { + return <div></div>; + } + + render(<div><Wrapper ref={ref} /></div>, scratch, scratch.firstChild); + expect(ref).to.have.been.calledOnce.and.calledWith(scratch.firstChild.firstChild); + }); }); diff --git a/thirdparty/preact/test/browser/spec.js b/thirdparty/preact/test/browser/spec.js index eb48151f0..d33cdb93f 100644 --- a/thirdparty/preact/test/browser/spec.js +++ b/thirdparty/preact/test/browser/spec.js @@ -1,6 +1,8 @@ import { h, render, rerender, Component } from '../../src/preact'; /** @jsx h */ +const EMPTY_CHILDREN = []; + describe('Component spec', () => { let scratch; @@ -24,6 +26,7 @@ describe('Component spec', () => { constructor(props, context) { super(props, context); expect(props).to.be.deep.equal({ + children: EMPTY_CHILDREN, fieldA: 1, fieldB: 2, fieldC: 1, fieldD: 2 }); @@ -81,14 +84,14 @@ describe('Component spec', () => { fieldC: 1, fieldD: 2 }; - expect(proto.ctor).to.have.been.calledWith(PROPS1); - expect(proto.render).to.have.been.calledWith(PROPS1); + expect(proto.ctor).to.have.been.calledWithMatch(PROPS1); + expect(proto.render).to.have.been.calledWithMatch(PROPS1); rerender(); // expect(proto.ctor).to.have.been.calledWith(PROPS2); - expect(proto.componentWillReceiveProps).to.have.been.calledWith(PROPS2); - expect(proto.render).to.have.been.calledWith(PROPS2); + expect(proto.componentWillReceiveProps).to.have.been.calledWithMatch(PROPS2); + expect(proto.render).to.have.been.calledWithMatch(PROPS2); }); // @TODO: migrate this to preact-compat diff --git a/thirdparty/preact/test/karma.conf.js b/thirdparty/preact/test/karma.conf.js index 6ed5397fb..3236e944b 100644 --- a/thirdparty/preact/test/karma.conf.js +++ b/thirdparty/preact/test/karma.conf.js @@ -1,45 +1,66 @@ /*eslint no-var:0, object-shorthand:0 */ var coverage = String(process.env.COVERAGE)!=='false', - sauceLabs = String(process.env.SAUCELABS).match(/^(1|true)$/gi) && !String(process.env.TRAVIS_PULL_REQUEST).match(/^(1|true)$/gi), - performance = !coverage && !sauceLabs && String(process.env.PERFORMANCE)!=='false', + ci = String(process.env.CI).match(/^(1|true)$/gi), + pullRequest = !String(process.env.TRAVIS_PULL_REQUEST).match(/^(0|false|undefined)$/gi), + realBrowser = String(process.env.BROWSER).match(/^(1|true)$/gi), + sauceLabs = realBrowser && ci && !pullRequest, + performance = !coverage && !realBrowser && String(process.env.PERFORMANCE)!=='false', webpack = require('webpack'); var sauceLabsLaunchers = { sl_chrome: { base: 'SauceLabs', - browserName: 'chrome' + browserName: 'chrome', + platform: 'Windows 10' }, sl_firefox: { base: 'SauceLabs', - browserName: 'firefox' + browserName: 'firefox', + platform: 'Windows 10' }, - sl_ios_safari: { + sl_safari: { base: 'SauceLabs', - browserName: 'iphone', - platform: 'OS X 10.9', - version: '7.1' + browserName: 'safari', + platform: 'OS X 10.11' + }, + sl_edge: { + base: 'SauceLabs', + browserName: 'MicrosoftEdge', + platform: 'Windows 10' }, sl_ie_11: { base: 'SauceLabs', browserName: 'internet explorer', - version: '11' + version: '11.103', + platform: 'Windows 10' }, sl_ie_10: { base: 'SauceLabs', browserName: 'internet explorer', - version: '10' + version: '10.0', + platform: 'Windows 7' }, sl_ie_9: { base: 'SauceLabs', browserName: 'internet explorer', - version: '9' + version: '9.0', + platform: 'Windows 7' } }; +var travisLaunchers = { + chrome_travis: { + base: 'Chrome', + flags: ['--no-sandbox'] + } +}; + +var localBrowsers = realBrowser ? Object.keys(travisLaunchers) : ['PhantomJS']; + module.exports = function(config) { config.set({ - browsers: sauceLabs ? Object.keys(sauceLabsLaunchers) : ['PhantomJS'], + browsers: sauceLabs ? Object.keys(sauceLabsLaunchers) : localBrowsers, frameworks: ['source-map-support', 'mocha', 'chai-sinon'], @@ -69,14 +90,18 @@ module.exports = function(config) { browserNoActivityTimeout: 5 * 60 * 1000, + // Use only two browsers concurrently, works better with open source Sauce Labs remote testing + concurrency: 2, + // sauceLabs: { // tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER || ('local'+require('./package.json').version), // startConnect: false // }, - customLaunchers: sauceLabsLaunchers, + customLaunchers: sauceLabs ? sauceLabsLaunchers : travisLaunchers, files: [ + { pattern: 'polyfills.js', watched: false }, { pattern: '{browser,shared}/**.js', watched: false } ], @@ -107,6 +132,10 @@ module.exports = function(config) { } : []) }, resolve: { + // The React DevTools integration requires preact as a module + // rather than referencing source files inside the module + // directly + alias: { preact: '../src/preact' }, modulesDirectories: [__dirname, 'node_modules'] }, plugins: [ diff --git a/thirdparty/preact/test/polyfills.js b/thirdparty/preact/test/polyfills.js new file mode 100644 index 000000000..51f256e6e --- /dev/null +++ b/thirdparty/preact/test/polyfills.js @@ -0,0 +1,5 @@ +// ES2015 APIs used by developer tools integration +import 'core-js/es6/map'; +import 'core-js/fn/array/fill'; +import 'core-js/fn/array/from'; +import 'core-js/fn/object/assign'; diff --git a/thirdparty/preact/test/shared/h.js b/thirdparty/preact/test/shared/h.js index b0cf7f0e8..ae692e3e5 100644 --- a/thirdparty/preact/test/shared/h.js +++ b/thirdparty/preact/test/shared/h.js @@ -6,7 +6,12 @@ import { expect } from 'chai'; /** @jsx h */ -let flatten = obj => JSON.parse(JSON.stringify(obj)); +const buildVNode = (nodeName, attributes, children=[]) => ({ + nodeName, + children, + attributes, + key: attributes && attributes.key +}); describe('h(jsx)', () => { it('should return a VNode', () => { @@ -16,7 +21,7 @@ describe('h(jsx)', () => { expect(r).to.be.an.instanceof(VNode); expect(r).to.have.property('nodeName', 'foo'); expect(r).to.have.property('attributes', undefined); - expect(r).to.have.property('children', undefined); + expect(r).to.have.property('children').that.eql([]); }); it('should perserve raw attributes', () => { @@ -38,8 +43,8 @@ describe('h(jsx)', () => { expect(r).to.be.an('object') .with.property('children') .that.deep.equals([ - new VNode('bar'), - new VNode('baz') + buildVNode('bar'), + buildVNode('baz') ]); }); @@ -51,15 +56,13 @@ describe('h(jsx)', () => { h('baz', null, h('test')) ); - r = flatten(r); - expect(r).to.be.an('object') .with.property('children') .that.deep.equals([ - { nodeName:'bar' }, - { nodeName:'baz', children:[ - { nodeName:'test' } - ]} + buildVNode('bar'), + buildVNode('baz', undefined, [ + buildVNode('test') + ]) ]); }); @@ -73,15 +76,13 @@ describe('h(jsx)', () => { ] ); - r = flatten(r); - expect(r).to.be.an('object') .with.property('children') .that.deep.equals([ - { nodeName:'bar' }, - { nodeName:'baz', children:[ - { nodeName:'test' } - ]} + buildVNode('bar'), + buildVNode('baz', undefined, [ + buildVNode('test') + ]) ]); }); @@ -95,15 +96,13 @@ describe('h(jsx)', () => { ] ); - r = flatten(r); - expect(r).to.be.an('object') .with.property('children') .that.deep.equals([ - { nodeName:'bar' }, - { nodeName:'baz', children:[ - { nodeName:'test' } - ]} + buildVNode('bar'), + buildVNode('baz', undefined, [ + buildVNode('test') + ]) ]); }); @@ -164,16 +163,14 @@ describe('h(jsx)', () => { 'six' ); - r = flatten(r); - expect(r).to.be.an('object') .with.property('children') .that.deep.equals([ 'onetwo', - { nodeName:'bar' }, + buildVNode('bar'), 'three', - { nodeName:'baz' }, - { nodeName:'baz' }, + buildVNode('baz'), + buildVNode('baz'), 'fourfivesix' ]); }); @@ -190,8 +187,6 @@ describe('h(jsx)', () => { null ); - r = flatten(r); - expect(r).to.be.an('object') .with.property('children') .that.deep.equals([ |