Squashed 'thirdparty/preact/' changes from b2d90cc..ba094e2

ba094e2 Run only local tests for pull requests (#390)
e9fc3c2 Fix CI build (#386)
70a5ca3 This adds a link to preact-i18nline. (#382)
5dffd85 Merge branch 'pr-fix-build-for-windows' of https://github.com/Download/preact
f14edf7 kilobits => kilobytes (#383)
c193547 Test for #292
284e4aa 6.4.0
24eab2f Prevent accidental duplicate recycling of elements when swapping the base element of a component. Fixes #373.
76c5ef7 fix lint error
8008886 When swapping the base of a composed child component, update its parent's base reference. Fixes #349.
fd4f21f Add an equality check prior to setting `.nodeValue` on text nodes, since Firefox (and maybe others) don't do this internally by default.  Fixes #368 - thanks @zbinlin!
1555e2b Add CDNJS version badge in readme (#365)
79c8bae Disable React Developer Tools integration tests under IE (#362)
84f4eeb Refactor `linkState()` a bit to drop around 40 bytes. Coincidentally, that's the exact size of the hooks just added for DevTools... 👌
22bbfcb Little tweaks 👯
f8b326e Document how to use the React DevTools with Preact (#354)
1f4a8eb Correct "preact/devtools" type definitions (#355)
68f22eb Add React Developer Tools integration (#339)
2a7a027 Add ref and allow objects in className (#316)
4a59cca fix readme todomvc link (#345)
37ca4e0 Fixes build for Windows #343
cf93387 6.3.0
ff05818 Make `VNode.children` *always* be an Array, even when there are no children.
9b095f4 Added link to preact-layout (#342)

git-subtree-dir: thirdparty/preact
git-subtree-split: ba094e27b602cb16aded7dcad95f71e44b7b0476
This commit is contained in:
Florian Dold 2016-11-08 15:07:07 +01:00
parent 30b577138d
commit 6e5fb04d3f
25 changed files with 979 additions and 153 deletions

6
.gitignore vendored
View File

@ -4,5 +4,7 @@
/dist
/_dev
/coverage
aliases.js
aliases.js.map
# Additional bundles
/*.js
/*.js.map

View File

@ -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:

View File

@ -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

View File

@ -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/**'
})
]
}

427
devtools/devtools.js Normal file
View File

@ -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;
};
}

4
devtools/index.js Normal file
View File

@ -0,0 +1,4 @@
import { initDevTools } from './devtools';
initDevTools();

View File

@ -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",

View File

@ -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 || '';

View File

@ -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;
}
}

View File

@ -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]] = {});
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]] || {});
}
obj[path[i]] = v;
v = s[p0];
}
component.setState({ [p0]: v });
component.setState(state);
};
}

View File

@ -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) { }
};

19
src/preact.d.ts vendored
View File

@ -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;

View File

@ -154,13 +154,13 @@ export function renderComponent(component, opts, mountAll, isChild) {
let baseParent = initialBase.parentNode;
if (baseParent && base!==baseParent) {
baseParent.replaceChild(base, initialBase);
}
if (!cbase && !toUnmount && component._parentComponent) {
if (!toUnmount) {
initialBase._component = null;
recollectNodeTree(initialBase);
}
}
}
if (toUnmount) {
unmountComponent(toUnmount, base!==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,9 +181,12 @@ export function renderComponent(component, opts, mountAll, isChild) {
if (!isUpdate || mountAll) {
mounts.unshift(component);
}
else if (!skip && component.componentDidUpdate) {
else if (!skip) {
if (component.componentDidUpdate) {
component.componentDidUpdate(previousProps, previousState, previousContext);
}
if (options.afterUpdate) options.afterUpdate(component);
}
let cb = component._renderCallbacks, fn;
if (cb) while ( (fn = cb.pop()) ) fn.call(component);
@ -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;

View File

@ -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) {
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);
}
}
}

View File

@ -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;
}

View File

@ -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();

View File

@ -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 });
});
});

234
test/browser/devtools.js Normal file
View File

@ -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);
});
});

View File

@ -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() {

View File

@ -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'}}});

View File

@ -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);
});
});

View File

@ -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

View File

@ -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: [

5
test/polyfills.js Normal file
View File

@ -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';

View File

@ -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([