diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..deef3397 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +// This is solely to avoid https://github.com/danger/danger-js/issues/261 +// and should be fine to remove before much longer +{} diff --git a/.eslintignore b/.eslintignore index 61d2a0b7..62117a94 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,7 +1,7 @@ -.vscode -/coverage -/docs/lib -/examples +.vscode/ +coverage/ +docs/lib/ +examples/ /mithril.js /mithril.min.js -/node_modules +node_modules/ diff --git a/.eslintrc.js b/.eslintrc.js index c14d0119..a38c5e1b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,7 +4,7 @@ module.exports = { "commonjs": true, "es6": true, "node": true - }, + }, "extends": "eslint:recommended", "rules": { "accessor-pairs": "error", diff --git a/.gitattributes b/.gitattributes index 6de5c647..7296866f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ * text=auto /mithril.js binary /mithril.min.js binary +/package-lock.json binary +/yarn.lock binary diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..7714f3da --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,9 @@ +.travis.yml @tivac +package.json @tivac +.npmignore @tivac +.eslintrc.js @tivac +.eslintignore @tivac +README.md @tivac +docs/ @tivac +performance/ @tivac +render/ @pygy diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..be4f8168 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,32 @@ + + +## Expected Behavior + + + +## Current Behavior + + + +## Possible Solution + + + +## Steps to Reproduce (for bugs) + + +1. +2. +3. +4. + +## Context + + + +## Your Environment + +* Version used: +* Browser Name and version: +* Operating System and version (desktop or mobile): +* Link to your project: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..f85840f2 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ + + +## Description + + +## Motivation and Context + + + +## How Has This Been Tested? + + + + +## Types of changes + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) +- [ ] Documentation change + +## Checklist: + + +- [ ] My code follows the code style of this project. +- [ ] My change requires a change to the documentation. +- [ ] I have updated the documentation accordingly. +- [ ] I have read the **CONTRIBUTING** document. +- [ ] I have added tests to cover my changes. +- [ ] All new and existing tests passed. +- [ ] I have updated `docs/change-log.md` diff --git a/.npmignore b/.npmignore index bf023d92..d81ad888 100644 --- a/.npmignore +++ b/.npmignore @@ -6,3 +6,4 @@ .gitignore .travis.yml CONTRIBUTING.md +yarn.lock diff --git a/.travis.yml b/.travis.yml index cc4576b5..d4470ebd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,10 +13,15 @@ cache: # Custom install step so the travis-only stuff doesn't need to be in package.json install: - npm install -- npm install @alrra/travis-scripts@^3.0.1 gh-pages@^0.12.0 +# This is to prevent lint-staged/prettier from running on the bundles +- npm rm husky -# Bundle before running tests so the bundle is always up-to-date -before_script: npm run build +# Run danger, build bundles (so they're always up to date) +before_script: +- npx danger run +- npm run build-browser +# Pass -save so it'll update the readme as well +- npm run build-min -- -save # Run tests, lint, and then check for perf regressions script: @@ -44,7 +49,7 @@ after_success: # this doesn't have the built-in branch protection like commit-changes if [ "$TRAVIS_EVENT_TYPE" == "push" ] && \ [ "$TRAVIS_BRANCH" == "master" ] && \ - [ "$TRAVIS_REPO_SLUG" == "lhorie/mithril.js" ] + [ "$TRAVIS_REPO_SLUG" == "MithrilJS/mithril.js" ] then # Generate docs npm run gendocs @@ -59,7 +64,7 @@ after_success: $(npm bin)/gh-pages \ --dist ./dist \ --add \ - --repo "git@github.com:lhorie/mithril.js.git" \ + --repo "git@github.com:MithrilJS/mithril.js.git" \ --message "Generated docs for commit $TRAVIS_COMMIT [skip ci]" else echo "Not submitting documentation updates" @@ -83,8 +88,7 @@ deploy: skip_cleanup: true on: tags: true - repo: lhorie/mithril.js - branch: master + repo: MithrilJS/mithril.js - provider: npm skip_cleanup: true @@ -93,5 +97,4 @@ deploy: secure: ADElvD1oxn9GfEG7dDOggX96b36A/cGEybovAc0221CCKzv5kWCavMrtxneiJYI6N/n24abSlbM90vMfU84FEzH0Ev28dGVokRP4ad6VRkISszKlYVEP8Lds4QxfKh78jZlUxmxM0B3vmQ1kYJbTBqp3ICtaJ5ptEQHWhrLtxnc= on: tags: true - repo: lhorie/mithril.js - branch: master + repo: MithrilJS/mithril.js diff --git a/README.md b/README.md index 08636be2..296c4c32 100644 --- a/README.md +++ b/README.md @@ -1,267 +1,61 @@ -# Introduction +mithril.js [![NPM Version](https://img.shields.io/npm/v/mithril.svg)](https://www.npmjs.com/package/mithril) [![NPM License](https://img.shields.io/npm/l/mithril.svg)](https://www.npmjs.com/package/mithril) [![NPM Downloads](https://img.shields.io/npm/dm/mithril.svg)](https://www.npmjs.com/package/mithril) +========== + +

+ + Build Status + + + Gitter + +

- [What is Mithril?](#what-is-mithril) -- [Getting started](#getting-started) -- [Hello world](#hello-world) -- [DOM elements](#dom-elements) -- [Components](#components) -- [Routing](#routing) -- [XHR](#xhr) +- [Installation](#installation) +- [Documentation](#documentation) +- [Getting Help](#getting-help) +- [Contributing](#contributing) + +## What is Mithril? + +A modern client-side Javascript framework for building Single Page Applications. It's small (8.30 KB gzipped), fast and provides routing and XHR utilities out of the box. + +Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍. + +Browsers all the way back to IE9 are supported, no polyfills required 👌. + +## Installation + +### CDN + +```html + +``` + +### npm + +```bash +$ npm install mithril +``` + +The ["Getting started" guide](https://mithril.js.org/#getting-started) is a good place to start learning how to use mithril. + +## Documentation + +Documentation lives on [mithril.js.org](https://mithril.js.org). + +You may be interested in the [API Docs](https://mithril.js.org/api.html), a [Simple Application](https://mithril.js.org/simple-application.html), or perhaps some [Examples](https://mithril.js.org/examples.html). + +## Getting Help + +Mithril has an active & welcoming community on [Gitter](https://gitter.im/mithriljs/mithril.js), or feel free to ask questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/mithril.js) using the `mithril.js` tag. + +## Contributing + +There's a [Contributing FAQ](https://mithril.js.org/contributing.html) on the mithril site that hopefully helps, but if not definitely hop into the [Gitter Room](https://gitter.im/mithriljs/mithril.js) and ask away! --- -### What is Mithril? +Thanks for reading! -Mithril is a modern client-side Javascript framework for building Single Page Applications. -It's small (< 8kb gzip), fast and provides routing and XHR utilities out of the box. - -
-
-
Download size
- Mithril (8kb) -
- Vue + Vue-Router + Vuex + fetch (40kb) -
- React + React-Router + Redux + fetch (64kb) -
- Angular (135kb) -
-
-
-
Performance
- Mithril (6.4ms) -
- Vue (9.8ms) -
- React (12.1ms) -
- Angular (11.5ms) -
-
-
- -Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess. - -If you are an experienced developer and want to know how Mithril compares to other frameworks, see the [framework comparison](http://mithril.js.org/framework-comparison.html) page. - -Mithril supports browsers all the way back to IE9, no polyfills required. - ---- - -### Getting started - -The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. It'll cover the majority of the API surface (including routing and XHR) but it'll only take 10 minutes. - -Let's create an HTML file to follow along: - -```markup - - - - -``` - ---- - -### Hello world - -Let's start as small as we can: render some text on screen. Copy the code below into your file (and by copy, I mean type it out - you'll learn better) - -```javascript -var root = document.body - -m.render(root, "Hello world") -``` - -Now, let's change the text to something else. Add this line of code under the previous one: - -```javascript -m.render(root, "My first app") -``` - -As you can see, you use the same code to both create and update HTML. Mithril automatically figures out the most efficient way of updating the text, rather than blindly recreating it from scratch. - ---- - -### DOM elements - -Let's wrap our text in an `

` tag. - -```javascript -m.render(root, m("h1", "My first app")) -``` - -The `m()` function can be used to describe any HTML structure you want. So if you need to add a class to the `

`: - -```javascript -m("h1", {class: "title"}, "My first app") -``` - -If you want to have multiple elements: - -```javascript -[ - m("h1", {class: "title"}, "My first app"), - m("button", "A button"), -] -``` - -And so on: - -```javascript -m("main", [ - m("h1", {class: "title"}, "My first app"), - m("button", "A button"), -]) -``` - -Note: If you prefer `` syntax, [it's possible to use it via a Babel plugin](http://mithril.js.org/jsx.html). - -```jsx -// HTML syntax via Babel's JSX plugin -
-

My first app

- -
-``` - ---- - -### Components - -A Mithril component is just an object with a `view` function. Here's the code above as a component: - -```javascript -var Hello = { - view: function() { - return m("main", [ - m("h1", {class: "title"}, "My first app"), - m("button", "A button"), - ]) - } -} -``` - -To activate the component, we use `m.mount`. - -```javascript -m.mount(root, Hello) -``` - -As you would expect, doing so creates this markup: - -```markup -
-

My first app

- -
-``` - -The `m.mount` function is similar to `m.render`, but instead of rendering some HTML only once, it activates Mithril's auto-redrawing system. To understand what that means, let's add some events: - -```javascript -var count = 0 // added a variable - -var Hello = { - view: function() { - return m("main", [ - m("h1", {class: "title"}, "My first app"), - // changed the next line - m("button", {onclick: function() {count++}}, count + " clicks"), - ]) - } -} - -m.mount(root, Hello) -``` - -We defined an `onclick` event on the button, which increments a variable `count` (which was declared at the top). We are now also rendering the value of that variable in the button label. - -You can now update the label of the button by clicking the button. Since we used `m.mount`, you don't need to manually call `m.render` to apply the changes in the `count` variable to the HTML; Mithril does it for you. - -If you're wondering about performance, it turns out Mithril is very fast at rendering updates, because it only touches the parts of the DOM it absolutely needs to. So in our example above, when you click the button, the text in it is the only part of the DOM Mithril actually updates. - ---- - -### Routing - -Routing just means going from one screen to another in an application with several screens. - -Let's add a splash page that appears before our click counter. First we create a component for it: - -```javascript -var Splash = { - view: function() { - return m("a", {href: "#!/hello"}, "Enter!") - } -} -``` - -As you can see, this component simply renders a link to `#!/hello`. The `#!` part is known as a hashbang, and it's a common convention used in Single Page Applications to indicate that the stuff after it (the `/hello` part) is a route path. - -Now that we going to have more than one screen, we use `m.route` instead of `m.mount`. - -```javascript -m.route(root, "/splash", { - "/splash": Splash, - "/hello": Hello, -}) -``` - -The `m.route` function still has the same auto-redrawing functionality that `m.mount` does, and it also enables URL awareness; in other words, it lets Mithril know what to do when it sees a `#!` in the URL. - -The `"/splash"` right after `root` means that's the default route, i.e. if the hashbang in the URL doesn't point to one of the defined routes (`/splash` and `/hello`, in our case), then Mithril redirects to the default route. So if you open the page in a browser and your URL is `http://localhost`, then you get redirected to `http://localhost/#!/splash`. - -Also, as you would expect, clicking on the link on the splash page takes you to the click counter screen we created earlier. Notice that now your URL will point to `http://localhost/#!/hello`. You can navigate back and forth to the splash page using the browser's back and next button. - ---- - -### XHR - -Basically, XHR is just a way to talk to a server. - -Let's change our click counter to make it save data on a server. For the server, we'll use [REM](http://rem-rest-api.herokuapp.com), a mock REST API designed for toy apps like this tutorial. - -First we create a function that calls `m.request`. The `url` specifies an endpoint that represents a resource, the `method` specifies the type of action we're taking (typically the `PUT` method [upserts](https://en.wiktionary.org/wiki/upsert)), `data` is the payload that we're sending to the endpoint and `withCredentials` means to enable cookies (a requirement for the REM API to work) - -```javascript -var count = 0 -var increment = function() { - m.request({ - method: "PUT", - url: "//rem-rest-api.herokuapp.com/api/tutorial/1", - data: {count: count + 1}, - withCredentials: true, - }) - .then(function(data) { - count = parseInt(data.count) - }) -} -``` - -Calling the increment function [upserts](https://en.wiktionary.org/wiki/upsert) an object `{count: 1}` to the `/api/tutorial/1` endpoint. This endpoint returns an object with the same `count` value that was sent to it. Notice that the `count` variable is only updated after the request completes, and it's updated with the response value from the server now. - -Let's replace the event handler in the component to call the `increment` function instead of incrementing the `count` variable directly: - -```javascript -var Hello = { - view: function() { - return m("main", [ - m("h1", {class: "title"}, "My first app"), - m("button", {onclick: increment}, count + " clicks"), - ]) - } -} -``` - -Clicking the button should now update the count. - ---- - -We covered how to create and update HTML, how to create components, routes for a Single Page Application, and interacted with a server via XHR. - -This should be enough to get you started writing the frontend for a real application. Now that you are comfortable with the basics of the Mithril API, [be sure to check out the simple application tutorial](http://mithril.js.org/simple-application.html), which walks you through building a realistic application. +🎁 diff --git a/api/mount.js b/api/mount.js index 2178505a..7203bf7c 100644 --- a/api/mount.js +++ b/api/mount.js @@ -16,6 +16,6 @@ module.exports = function(redrawService) { redrawService.render(root, Vnode(component)) } redrawService.subscribe(root, run) - redrawService.redraw() + run() } } diff --git a/api/redraw.js b/api/redraw.js index 1b22271b..a6549975 100644 --- a/api/redraw.js +++ b/api/redraw.js @@ -4,44 +4,48 @@ var coreRenderer = require("../render/render") function throttle(callback) { //60fps translates to 16.6ms, round it down since setTimeout requires int - var time = 16 + var delay = 16 var last = 0, pending = null var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout return function() { - var now = Date.now() - if (last === 0 || now - last >= time) { - last = now - callback() - } - else if (pending === null) { + var elapsed = Date.now() - last + if (pending === null) { pending = timeout(function() { pending = null callback() last = Date.now() - }, time - (now - last)) + }, delay - elapsed) } } } -module.exports = function($window) { + +module.exports = function($window, throttleMock) { var renderService = coreRenderer($window) renderService.setEventCallback(function(e) { - if (e.redraw !== false) redraw() + if (e.redraw === false) e.redraw = undefined + else redraw() }) var callbacks = [] + var rendering = false + function subscribe(key, callback) { unsubscribe(key) - callbacks.push(key, throttle(callback)) + callbacks.push(key, callback) } function unsubscribe(key) { var index = callbacks.indexOf(key) if (index > -1) callbacks.splice(index, 2) } - function redraw() { - for (var i = 1; i < callbacks.length; i += 2) { - callbacks[i]() - } + function sync() { + if (rendering) throw new Error("Nested m.redraw.sync() call") + rendering = true + for (var i = 1; i < callbacks.length; i+=2) try {callbacks[i]()} catch (e) {if (typeof console !== "undefined") console.error(e)} + rendering = false } + + var redraw = (throttleMock || throttle)(sync) + redraw.sync = sync return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render} } diff --git a/api/router.js b/api/router.js index c182008f..4d893acc 100644 --- a/api/router.js +++ b/api/router.js @@ -11,9 +11,14 @@ module.exports = function($window, redrawService) { var render, component, attrs, currentPath, lastUpdate var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") - var run = function() { + function run() { if (render != null) redrawService.render(root, render(Vnode(component, attrs.key, attrs))) } + var redraw = function() { + run() + redraw = redrawService.redraw + } + redrawService.subscribe(root, run) var bail = function(path) { if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true}) else throw new Error("Could not resolve default route " + defaultRoute) @@ -24,7 +29,7 @@ module.exports = function($window, redrawService) { component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div" attrs = params, currentPath = path, lastUpdate = null render = (routeResolver.render || identity).bind(routeResolver) - run() + redraw() } if (payload.view || typeof payload === "function") update({}, payload) else { @@ -36,16 +41,18 @@ module.exports = function($window, redrawService) { else update(payload, "div") } }, bail) - redrawService.subscribe(root, run) } route.set = function(path, data, options) { - if (lastUpdate != null) options = {replace: true} + if (lastUpdate != null) { + options = options || {} + options.replace = true + } lastUpdate = null routeService.setPath(path, data, options) } route.get = function() {return currentPath} route.prefix = function(prefix) {routeService.prefix = prefix} - route.link = function(vnode) { + var link = function(options, vnode) { vnode.dom.setAttribute("href", routeService.prefix + vnode.attrs.href) vnode.dom.onclick = function(e) { if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return @@ -53,9 +60,13 @@ module.exports = function($window, redrawService) { e.redraw = false var href = this.getAttribute("href") if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length) - route.set(href, undefined, undefined) + route.set(href, undefined, options) } } + route.link = function(args) { + if (args.tag == null) return link.bind(link, args) + return link({}, args) + } route.param = function(key) { if(typeof attrs !== "undefined" && typeof key !== "undefined") return attrs[key] return attrs diff --git a/api/tests/index.html b/api/tests/index.html index 37d313f4..e85bd4b7 100644 --- a/api/tests/index.html +++ b/api/tests/index.html @@ -13,8 +13,8 @@ - - + + @@ -28,7 +28,6 @@ - diff --git a/api/tests/test-mount.js b/api/tests/test-mount.js index db2bee04..7385e43d 100644 --- a/api/tests/test-mount.js +++ b/api/tests/test-mount.js @@ -3,24 +3,27 @@ var o = require("../../ospec/ospec") var components = require("../../test-utils/components") var domMock = require("../../test-utils/domMock") +var throttleMocker = require("../../test-utils/throttleMock") var m = require("../../render/hyperscript") -var coreRenderer = require("../../render/render") var apiRedraw = require("../../api/redraw") var apiMounter = require("../../api/mount") o.spec("mount", function() { - var FRAME_BUDGET = Math.floor(1000 / 60) - var $window, root, redrawService, mount, render + var $window, root, redrawService, mount, render, throttleMock o.beforeEach(function() { $window = domMock() + throttleMock = throttleMocker() root = $window.document.body - - redrawService = apiRedraw($window) + redrawService = apiRedraw($window, throttleMock.throttle) mount = apiMounter(redrawService) - render = coreRenderer($window).render + render = redrawService.render + }) + + o.afterEach(function() { + o(throttleMock.queueLength()).equals(0) }) o("throws on invalid component", function() { @@ -47,7 +50,7 @@ o.spec("mount", function() { o(threw).equals(true) }) - o("renders into `root`", function() { + o("renders into `root` synchronoulsy", function() { mount(root, createComponent({ view : function() { return m("div") @@ -69,7 +72,37 @@ o.spec("mount", function() { o(root.childNodes.length).equals(0) }) - o("redraws on events", function(done) { + o("Mounting a second root doesn't cause the first one to redraw", function() { + var view = o.spy(function() { + return m("div") + }) + + render(root, [ + m("#child0"), + m("#child1") + ]) + + mount(root.childNodes[0], createComponent({ + view : view + })) + + o(root.firstChild.nodeName).equals("DIV") + o(view.callCount).equals(1) + + mount(root.childNodes[1], createComponent({ + view : function() { + return m("div") + } + })) + + o(view.callCount).equals(1) + + throttleMock.fire() + + o(view.callCount).equals(1) + }) + + o("redraws on events", function() { var onupdate = o.spy() var oninit = o.spy() var onclick = o.spy() @@ -97,17 +130,12 @@ o.spec("mount", function() { o(onclick.args[0].type).equals("click") o(onclick.args[0].target).equals(root.firstChild) - // Wrapped to give time for the rate-limited redraw to fire - setTimeout(function() { - o(onupdate.callCount).equals(1) + throttleMock.fire() - done() - }, FRAME_BUDGET) + o(onupdate.callCount).equals(1) }) - o("redraws several mount points on events", function(done, timeout) { - timeout(60) - + o("redraws several mount points on events", function() { var onupdate0 = o.spy() var oninit0 = o.spy() var onclick0 = o.spy() @@ -154,26 +182,26 @@ o.spec("mount", function() { o(onclick0.callCount).equals(1) o(onclick0.this).equals(root.childNodes[0].firstChild) - setTimeout(function() { - o(onupdate0.callCount).equals(1) - o(onupdate1.callCount).equals(1) + throttleMock.fire() - root.childNodes[1].firstChild.dispatchEvent(e) - o(onclick1.callCount).equals(1) - o(onclick1.this).equals(root.childNodes[1].firstChild) + o(onupdate0.callCount).equals(1) + o(onupdate1.callCount).equals(1) - setTimeout(function() { - o(onupdate0.callCount).equals(2) - o(onupdate1.callCount).equals(2) + root.childNodes[1].firstChild.dispatchEvent(e) - done() - }, FRAME_BUDGET) - }, FRAME_BUDGET) + o(onclick1.callCount).equals(1) + o(onclick1.this).equals(root.childNodes[1].firstChild) + throttleMock.fire() + + o(onupdate0.callCount).equals(2) + o(onupdate1.callCount).equals(2) }) - o("event handlers can skip redraw", function(done) { - var onupdate = o.spy() + o("event handlers can skip redraw", function() { + var onupdate = o.spy(function(){ + throw new Error("This shouldn't have been called") + }) var oninit = o.spy() var e = $window.document.createEvent("MouseEvents") @@ -195,15 +223,12 @@ o.spec("mount", function() { o(oninit.callCount).equals(1) - // Wrapped to ensure no redraw fired - setTimeout(function() { - o(onupdate.callCount).equals(0) + throttleMock.fire() - done() - }, FRAME_BUDGET) + o(onupdate.callCount).equals(0) }) - o("redraws when the render function is run", function(done) { + o("redraws when the render function is run", function() { var onupdate = o.spy() var oninit = o.spy() @@ -221,17 +246,12 @@ o.spec("mount", function() { redrawService.redraw() - // Wrapped to give time for the rate-limited redraw to fire - setTimeout(function() { - o(onupdate.callCount).equals(1) + throttleMock.fire() - done() - }, FRAME_BUDGET) + o(onupdate.callCount).equals(1) }) - o("throttles", function(done, timeout) { - timeout(200) - + o("throttles", function() { var i = 0 mount(root, createComponent({view: function() {i++}})) var before = i @@ -243,12 +263,11 @@ o.spec("mount", function() { var after = i - setTimeout(function(){ - o(before).equals(1) // mounts synchronously - o(after).equals(1) // throttles rest - o(i).equals(2) - done() - },40) + throttleMock.fire() + + o(before).equals(1) // mounts synchronously + o(after).equals(1) // throttles rest + o(i).equals(2) }) }) }) diff --git a/api/tests/test-redraw.js b/api/tests/test-redraw.js index f13c2d3f..65831bb0 100644 --- a/api/tests/test-redraw.js +++ b/api/tests/test-redraw.js @@ -2,6 +2,7 @@ var o = require("../../ospec/ospec") var domMock = require("../../test-utils/domMock") +var throttleMocker = require("../../test-utils/throttleMock") var apiRedraw = require("../../api/redraw") o.spec("redrawService", function() { @@ -17,25 +18,39 @@ o.spec("redrawService", function() { redrawService.redraw() }) + o("honours throttleMock", function() { + var throttleMock = throttleMocker() + redrawService = apiRedraw(domMock(), throttleMock.throttle) + var spy = o.spy() + + redrawService.subscribe(root, spy) + + o(spy.callCount).equals(0) + + redrawService.redraw() + + o(spy.callCount).equals(0) + + throttleMock.fire() + + o(spy.callCount).equals(1) + }) + o("should run a single renderer entry", function(done) { var spy = o.spy() redrawService.subscribe(root, spy) o(spy.callCount).equals(0) - - redrawService.redraw() - - o(spy.callCount).equals(1) redrawService.redraw() redrawService.redraw() redrawService.redraw() - o(spy.callCount).equals(1) + o(spy.callCount).equals(0) setTimeout(function() { - o(spy.callCount).equals(2) - + o(spy.callCount).equals(1) + done() }, 20) }) @@ -54,27 +69,29 @@ o.spec("redrawService", function() { redrawService.redraw() - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(1) + o(spy1.callCount).equals(0) + o(spy2.callCount).equals(0) + o(spy3.callCount).equals(0) redrawService.redraw() - o(spy1.callCount).equals(1) - o(spy2.callCount).equals(1) - o(spy3.callCount).equals(1) + o(spy1.callCount).equals(0) + o(spy2.callCount).equals(0) + o(spy3.callCount).equals(0) setTimeout(function() { - o(spy1.callCount).equals(2) - o(spy2.callCount).equals(2) - o(spy3.callCount).equals(2) - + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(spy3.callCount).equals(1) + done() }, 20) }) - o("should stop running after unsubscribe", function() { - var spy = o.spy() + o("should stop running after unsubscribe", function(done) { + var spy = o.spy(function() { + throw new Error("This shouldn't have been called") + }) redrawService.subscribe(root, spy) redrawService.unsubscribe(root, spy) @@ -82,9 +99,33 @@ o.spec("redrawService", function() { redrawService.redraw() o(spy.callCount).equals(0) + setTimeout(function() { + o(spy.callCount).equals(0) + + done() + }, 20) }) - o("does nothing on invalid unsubscribe", function() { + o("should stop running after unsubscribe, even if it occurs after redraw is requested", function(done) { + var spy = o.spy(function() { + throw new Error("This shouldn't have been called") + }) + + redrawService.subscribe(root, spy) + + redrawService.redraw() + + redrawService.unsubscribe(root, spy) + + o(spy.callCount).equals(0) + setTimeout(function() { + o(spy.callCount).equals(0) + + done() + }, 20) + }) + + o("does nothing on invalid unsubscribe", function(done) { var spy = o.spy() redrawService.subscribe(root, spy) @@ -92,6 +133,39 @@ o.spec("redrawService", function() { redrawService.redraw() - o(spy.callCount).equals(1) + setTimeout(function() { + o(spy.callCount).equals(1) + + done() + }, 20) + }) + + o("redraw.sync() redraws all roots synchronously", function() { + var el1 = $document.createElement("div") + var el2 = $document.createElement("div") + var el3 = $document.createElement("div") + var spy1 = o.spy() + var spy2 = o.spy() + var spy3 = o.spy() + + redrawService.subscribe(el1, spy1) + redrawService.subscribe(el2, spy2) + redrawService.subscribe(el3, spy3) + + o(spy1.callCount).equals(0) + o(spy2.callCount).equals(0) + o(spy3.callCount).equals(0) + + redrawService.redraw.sync() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(spy3.callCount).equals(1) + + redrawService.redraw.sync() + + o(spy1.callCount).equals(2) + o(spy2.callCount).equals(2) + o(spy3.callCount).equals(2) }) }) diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 9406de70..f5336625 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -3,6 +3,7 @@ var o = require("../../ospec/ospec") var callAsync = require("../../test-utils/callAsync") var browserMock = require("../../test-utils/browserMock") +var throttleMocker = require("../../test-utils/throttleMock") var m = require("../../render/hyperscript") var callAsync = require("../../test-utils/callAsync") @@ -14,19 +15,23 @@ o.spec("route", function() { void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { - var FRAME_BUDGET = Math.floor(1000 / 60) - var $window, root, redrawService, route + var $window, root, redrawService, route, throttleMock o.beforeEach(function() { $window = browserMock(env) + throttleMock = throttleMocker() root = $window.document.body - redrawService = apiRedraw($window) + redrawService = apiRedraw($window, throttleMock.throttle) route = apiRouter($window, redrawService) route.prefix(prefix) }) + o.afterEach(function() { + o(throttleMock.queueLength()).equals(0) + }) + o("throws on invalid `root` DOM node", function() { var threw = false try { @@ -50,7 +55,7 @@ o.spec("route", function() { o(root.firstChild.nodeName).equals("DIV") }) - o("routed mount points can redraw synchronously (POJO component)", function() { + o("routed mount points only redraw asynchronously (POJO component)", function() { var view = o.spy() $window.location.href = prefix + "/" @@ -60,11 +65,14 @@ o.spec("route", function() { redrawService.redraw() - o(view.callCount).equals(2) + o(view.callCount).equals(1) + throttleMock.fire() + + o(view.callCount).equals(2) }) - o("routed mount points can redraw synchronously (constructible component)", function() { + o("routed mount points only redraw asynchronously (constructible component)", function() { var view = o.spy() var Cmp = function(){} @@ -77,11 +85,14 @@ o.spec("route", function() { redrawService.redraw() - o(view.callCount).equals(2) + o(view.callCount).equals(1) + throttleMock.fire() + + o(view.callCount).equals(2) }) - o("routed mount points can redraw synchronously (closure component)", function() { + o("routed mount points only redraw asynchronously (closure component)", function() { var view = o.spy() function Cmp() {return {view: view}} @@ -93,8 +104,11 @@ o.spec("route", function() { redrawService.redraw() - o(view.callCount).equals(2) + o(view.callCount).equals(1) + throttleMock.fire() + + o(view.callCount).equals(2) }) o("default route doesn't break back button", function(done) { @@ -160,11 +174,12 @@ o.spec("route", function() { o(oninit.callCount).equals(1) redrawService.redraw() + throttleMock.fire() o(onupdate.callCount).equals(1) }) - o("redraws on events", function(done) { + o("redraws on events", function() { var onupdate = o.spy() var oninit = o.spy() var onclick = o.spy() @@ -194,12 +209,9 @@ o.spec("route", function() { o(onclick.args[0].type).equals("click") o(onclick.args[0].target).equals(root.firstChild) - // Wrapped to give time for the rate-limited redraw to fire - callAsync(function() { - o(onupdate.callCount).equals(1) - done() - }) + throttleMock.fire() + o(onupdate.callCount).equals(1) }) o("event handlers can skip redraw", function(done) { @@ -224,9 +236,11 @@ o.spec("route", function() { } }) + o(oninit.callCount).equals(1) + root.firstChild.dispatchEvent(e) - o(oninit.callCount).equals(1) + o(e.redraw).notEquals(false) // Wrapped to ensure no redraw fired callAsync(function() { @@ -267,6 +281,36 @@ o.spec("route", function() { o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test") }) + o("passes options on route.link", function() { + var opts = {} + var e = $window.document.createEvent("MouseEvents") + + e.initEvent("click", true, true) + $window.location.href = prefix + "/" + + route(root, "/", { + "/" : { + view: function() { + return m("a", { + href: "/test", + oncreate: route.link(opts) + }) + } + }, + "/test" : { + view : function() { + return m("div") + } + } + }) + route.set = o.spy(route.set) + + root.firstChild.dispatchEvent(e) + + o(route.set.callCount).equals(1) + o(route.set.args[2]).equals(opts) + }) + o("accepts RouteResolver with onmatch that returns Component", function(done) { var matchCount = 0 var renderCount = 0 @@ -500,7 +544,10 @@ o.spec("route", function() { o(oninit.callCount).equals(1) route.set("/def") callAsync(function() { + throttleMock.fire() + o(oninit.callCount).equals(2) + done() }) }) @@ -536,23 +583,28 @@ o.spec("route", function() { route(root, "/a", { "/a" : { render: function() { - return m("div") + return m("div", m("p")) }, }, "/b" : { render: function() { - return m("div") + return m("div", m("a")) }, }, }) var dom = root.firstChild + var child = dom.firstChild + o(root.firstChild.nodeName).equals("DIV") route.set("/b") callAsync(function() { + throttleMock.fire() + o(root.firstChild).equals(dom) + o(root.firstChild.firstChild).notEquals(child) done() }) @@ -586,6 +638,7 @@ o.spec("route", function() { o(renderCount).equals(1) redrawService.redraw() + throttleMock.fire() o(matchCount).equals(1) o(renderCount).equals(2) @@ -621,6 +674,7 @@ o.spec("route", function() { o(renderCount).equals(1) redrawService.redraw() + throttleMock.fire() o(matchCount).equals(1) o(renderCount).equals(2) @@ -665,7 +719,7 @@ o.spec("route", function() { route(root, "/a", { "/a" : { onmatch: function() { - route.set("/b") + route.set("/b", {}, {state: {a: 5}}) }, render: render }, @@ -684,6 +738,7 @@ o.spec("route", function() { o(view.callCount).equals(1) o(root.childNodes.length).equals(1) o(root.firstChild.nodeName).equals("DIV") + o($window.history.state).deepEquals({a: 5}) done() }) @@ -815,10 +870,14 @@ o.spec("route", function() { }) callAsync(function() { + throttleMock.fire() + route.set("/b") callAsync(function() { callAsync(function() { callAsync(function() { + throttleMock.fire() + o(render.callCount).equals(0) o(component.view.callCount).equals(2) @@ -939,6 +998,7 @@ o.spec("route", function() { o(onmatch.callCount).equals(1) redrawService.redraw() + throttleMock.fire() o(view.callCount).equals(2) o(onmatch.callCount).equals(1) @@ -1017,6 +1077,8 @@ o.spec("route", function() { }) callAsync(function() { + throttleMock.fire() + o(onmatch.callCount).equals(1) o(render.callCount).equals(1) @@ -1024,6 +1086,8 @@ o.spec("route", function() { callAsync(function() { callAsync(function() { + throttleMock.fire() + o(onmatch.callCount).equals(2) o(render.callCount).equals(2) @@ -1074,9 +1138,15 @@ o.spec("route", function() { route.set("/b") callAsync(function() { + throttleMock.fire() + + o(root.firstChild.nodeName).equals("B") + route.set("/a") callAsync(function() { + throttleMock.fire() + o(root.firstChild.nodeName).equals("A") done() @@ -1141,7 +1211,9 @@ o.spec("route", function() { route.set("/b") + // setting the route is asynchronous callAsync(function() { + throttleMock.fire() o(spy.callCount).equals(1) done() @@ -1181,9 +1253,7 @@ o.spec("route", function() { }) }) - o("throttles", function(done, timeout) { - timeout(200) - + o("throttles", function() { var i = 0 $window.location.href = prefix + "/" route(root, "/", { @@ -1197,12 +1267,11 @@ o.spec("route", function() { redrawService.redraw() var after = i - setTimeout(function() { - o(before).equals(1) // routes synchronously - o(after).equals(2) // redraws synchronously - o(i).equals(3) // throttles rest - done() - }, FRAME_BUDGET * 2) + throttleMock.fire() + + o(before).equals(1) // routes synchronously + o(after).equals(1) // redraws asynchronously + o(i).equals(2) }) o("m.route.param is available outside of route handlers", function(done) { diff --git a/bundler/cli.js b/bundler/cli.js index f702ce7e..93ad0adf 100644 --- a/bundler/cli.js +++ b/bundler/cli.js @@ -5,7 +5,7 @@ var fs = require("fs"); var bundle = require("./bundle") var minify = require("./minify") -var aliases = {o: "output", m: "minify", w: "watch", a: "aggressive"} +var aliases = {o: "output", m: "minify", w: "watch", a: "aggressive", s: "save"} var params = {} var args = process.argv.slice(2), command = null for (var i = 0; i < args.length; i++) { @@ -27,8 +27,6 @@ function add(value) { bundle(params.input, params.output, {watch: params.watch}) if (params.minify) { minify(params.output, params.output, {watch: params.watch, advanced: params.aggressive}, function(stats) { - var readme, kb; - function format(n) { return n.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,") } @@ -36,14 +34,16 @@ if (params.minify) { console.log("Original size: " + format(stats.originalGzipSize) + " bytes gzipped (" + format(stats.originalSize) + " bytes uncompressed)") console.log("Compiled size: " + format(stats.compressedGzipSize) + " bytes gzipped (" + format(stats.compressedSize) + " bytes uncompressed)") - readme = fs.readFileSync("./README.md", "utf8") - kb = stats.compressedGzipSize / 1024 + if (params.save) { + var readme = fs.readFileSync("./README.md", "utf8") + var kb = stats.compressedGzipSize / 1000 - fs.writeFileSync("./README.md", - readme.replace( - /()(.+?)()/, - "$1" + (kb % 1 ? kb.toFixed(2) : kb) + " KB$3" + fs.writeFileSync("./README.md", + readme.replace( + /()(.+?)()/, + "$1" + (kb % 1 ? kb.toFixed(2) : kb) + " KB$3" + ) ) - ) + } }) } diff --git a/bundler/minify.js b/bundler/minify.js index 51b2b775..3b1d3cae 100644 --- a/bundler/minify.js +++ b/bundler/minify.js @@ -1,6 +1,6 @@ "use strict" -var http = require("http") +var http = require("https") var querystring = require("querystring") var fs = require("fs") @@ -22,7 +22,6 @@ module.exports = function(input, output, options, done) { var response = "" var req = http.request({ method: "POST", - protocol: "http:", hostname: "closure-compiler.appspot.com", path: "/compile", headers: { @@ -33,8 +32,16 @@ module.exports = function(input, output, options, done) { res.on("data", function(chunk) { response += chunk.toString() }) + res.on("end", function() { - var results = JSON.parse(response) + try { + var results = JSON.parse(response) + } catch(e) { + console.error(response); + + throw e; + } + if (results.errors) { for (var i = 0; i < results.errors.length; i++) console.log(results.errors[i]) } diff --git a/bundler/tests/test-bundler.js b/bundler/tests/test-bundler.js index 15e7f932..ff8bb32e 100644 --- a/bundler/tests/test-bundler.js +++ b/bundler/tests/test-bundler.js @@ -5,7 +5,7 @@ var bundle = require("../bundle") var fs = require("fs") -var ns = "bundler/tests/" +var ns = "./" function read(filepath) { try {return fs.readFileSync(ns + filepath, "utf8")} catch (e) {/* ignore */} } diff --git a/dangerfile.js b/dangerfile.js new file mode 100644 index 00000000..bd9cd30b --- /dev/null +++ b/dangerfile.js @@ -0,0 +1,60 @@ +/* global danger warn fail */ +"use strict"; + +var fs = require("fs"), + path = require("path"), + + locater = require("locater"), + pinpoint = require("pinpoint"), + dedent = require("dedent"); + +// Various views of changed/added files +var jsfiles = danger.git.created_files + .concat(danger.git.modified_files) + .filter((file) => path.extname(file) === ".js"), + + changelog = danger.git.modified_files.find((file) => + file === "docs/change-log.md" + ), + + appfiles = jsfiles.filter((file) => + file.indexOf("tests/") === -1 + ); + +function link(file, anchor, text) { + var repo = danger.github.pr.head.repo.html_url, + ref = danger.github.pr.head.ref; + + return danger.utils.href(`${repo}/blob/${ref}/${file}${anchor || ""}`, file || text); +} + +// All PRs should be targeted against `next` +if(danger.github.pr.base.ref !== "next") { + warn("PRs should be based on `next`, rebase before submitting please"); +} + +// Any non-test JS changes should probably have a change-log entry +if(appfiles.length && !changelog) { + warn(dedent(` + Please add an entry to ${link("docs/change-log.md")}. + `)) +} + +// Call out if `o.only(...)` was left in +jsfiles + .filter((file) => file.indexOf("tests/") > -1) + // Have to exclude test-ospec.js because it specifically has a purposeful "o.only" in it + .filter((file) => file.indexOf("test-ospec") === -1) + .forEach((file) => { + var code = fs.readFileSync(file, "utf8"), + locs = locater.find("o.only", code); + + locs.forEach((loc) => + fail(dedent(` + Please remove the \`o.only\` from ${link(file, `#L${loc.line}`)}. +
+				${pinpoint(code, {line: loc.line, column : loc.cursor})}
+				
+ `)) + ) + }); diff --git a/docs/animation.md b/docs/animation.md index de9d2bea..7e251580 100644 --- a/docs/animation.md +++ b/docs/animation.md @@ -102,4 +102,4 @@ Note that the `onbeforeremove` hook only fires on the element that loses its `pa When creating animations, it's recommended that you only use the `opacity` and `transform` CSS rules, since these can be hardware-accelerated by modern browsers and yield better performance than animating `top`, `left`, `width`, and `height`. -It's also recommended that you avoid the `box-shadow` rule and selectors like `:nth-child`, since these are also resource intensive options. If you want to animate a `box-shadow`, consider [putting the `box-shadow` rule on a pseudo element, and animate that element's opacity instead](http://tobiasahlin.com/blog/how-to-animate-box-shadow/). Other things that can be expensive include large or dynamically scaled images and overlapping elements with different `position` values (e.g. an absolute postioned element over a fixed element). +It's also recommended that you avoid the `box-shadow` rule and selectors like `:nth-child`, since these are also resource intensive options. If you want to animate a `box-shadow`, consider [putting the `box-shadow` rule on a pseudo element, and animate that element's opacity instead](http://tobiasahlin.com/blog/how-to-animate-box-shadow/). Other things that can be expensive include large or dynamically scaled images and overlapping elements with different `position` values (e.g. an absolute positioned element over a fixed element). diff --git a/docs/change-log.md b/docs/change-log.md index 9c493aa4..687849e2 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -1,5 +1,11 @@ # Change log +- [v2.0.0](#v200-wip) +- [v1.1.6](#v116) +- [v1.1.5](#v115) +- [v1.1.4](#v114) +- [v1.1.3](#v113) +- [v1.1.2](#v112) - [v1.1.1](#v111) - [v1.1.0](#v110) - [v1.0.1](#v101) @@ -8,14 +14,137 @@ --- +### v2.0.0 (WIP) + +#### Breaking changes + +- API: `m.redraw()` is always asynchronous ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592)) +- API: `m.mount()` will only render its own root when called, it will not trigger a `redraw()` ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592)) +- API: Assigning to `vnode.state` (as in `vnode.state = ...`) is no longer supported. Instead, an error is thrown if `vnode.state` changes upon the invocation of a lifecycle hook. +- API: `m.request` will no longer reject the Promise on server errors (eg. status >= 400) if the caller supplies an `extract` callback. This gives applications more control over handling server responses. + +#### News + +- API: Introduction of `m.redraw.sync()` ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592)) +- API: Event handlers may also be objects with `handleEvent` methods ([#1939](https://github.com/MithrilJS/mithril.js/issues/1939)). +- API: `m.route.link` accepts an optional `options` object ([#1930](https://github.com/MithrilJS/mithril.js/pull/1930)) +- API: `m.request` supports `timeout` as attr - ([#1966](https://github.com/MithrilJS/mithril.js/pull/1966)) +- Mocks: add limited support for the DOMParser API ([#2097](https://github.com/MithrilJS/mithril.js/pull/2097)) +- API: add support for raw SVG in `m.trust()` string ([#2097](https://github.com/MithrilJS/mithril.js/pull/2097)) +- Internals: remove the DOM nodes recycling pool ([#2122](https://github.com/MithrilJS/mithril.js/pull/2122)) + +#### Bug fixes + +- API: `m.route.set()` causes all mount points to be redrawn ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592)) +- render/attrs: Using style objects in hyperscript calls will now properly diff style properties from one render to another as opposed to re-writing all element style properties every render. +- render/attrs: `xlink:href` attributes are now correctly removed +- render/attrs: fix element value don't change if new value is undefined [#2082](https://github.com/MithrilJS/mithril.js/issues/2082) +(https://github.com/MithrilJS/mithril.js/pull/2130) +- render/core: Render state correctly on select change event [#1916](https://github.com/MithrilJS/mithril.js/issues/1916) ([#1918](https://github.com/MithrilJS/mithril.js/pull/1918) [@robinchew](https://github.com/robinchew), [#2052](https://github.com/MithrilJS/mithril.js/pull/2052)) +- render/core: fix various updateNodes/removeNodes issues when the pool and fragments are involved [#1990](https://github.com/MithrilJS/mithril.js/issues/1990), [#1991](https://github.com/MithrilJS/mithril.js/issues/1991), [#2003](https://github.com/MithrilJS/mithril.js/issues/2003), [#2021](https://github.com/MithrilJS/mithril.js/pull/2021) +- render/core: fix crashes when the keyed vnodes with the same `key` had different `tag` values [#2128](https://github.com/MithrilJS/mithril.js/issues/2128) [@JacksonJN](https://github.com/JacksonJN) ([#2130](https://github.com/MithrilJS/mithril.js/pull/2130)) +- render/core: fix cached nodes behavior in some keyed diff scenarios [#2132](https://github.com/MithrilJS/mithril.js/issues/2132) ([#2130](https://github.com/MithrilJS/mithril.js/pull/2130)) +- render/events: `addEventListener` and `removeEventListener` are always used to manage event subscriptions, preventing external interference. +- render/events: Event listeners allocate less memory, swap at low cost, and are properly diffed now when rendered via `m.mount()`/`m.redraw()`. +- render/events: `Object.prototype` properties can no longer interfere with event listener calls. +- render/events: Event handlers, when set to literally `undefined` (or any non-function), are now correctly removed. +- render/hooks: fixed an ommission that caused `oninit` to be called unnecessarily in some cases [#1992](https://github.com/MithrilJS/mithril.js/issues/1992) +- docs: fix typo ([#2104](https://github.com/MithrilJS/mithril.js/pull/2104) [@mikeyb](https://github.com/mikeyb)) +--- + +### v1.1.7 + +- Promise polyfill implementation separated from polyfilling logic. +- `PromisePolyfill` is now available on the exported/global `m`. + +--- + +### v1.1.6 + +- core: render() function can no longer prevent from changing `document.activeElement` in lifecycle hooks ([#1988](https://github.com/MithrilJS/mithril.js/pull/1988), [@purplecode](https://github.com/purplecode)) +- core: don't call `onremove` on the children of components that return null from the view [#1921](https://github.com/MithrilJS/mithril.js/issues/1921) [@octavore](https://github.com/octavore) ([#1922](https://github.com/MithrilJS/mithril.js/pull/1922)) +- hypertext: correct handling of shared attributes object passed to `m()`. Will copy attributes when it's necessary [#1941](https://github.com/MithrilJS/mithril.js/issues/1941) [@s-ilya](https://github.com/s-ilya) ([#1942](https://github.com/MithrilJS/mithril.js/pull/1942)) + +#### Ospec improvements + +- ospec v1.4.0 + - Added support for async functions and promises in tests ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928), [@StephanHoyer](https://github.com/StephanHoyer)) + - Error handling for async tests with `done` callbacks supports error as first argument ([#1928](https://github.com/MithrilJS/mithril.js/pull/1928)) + - Error messages which include newline characters do not swallow the stack trace [#1495](https://github.com/MithrilJS/mithril.js/issues/1495) ([#1984](https://github.com/MithrilJS/mithril.js/pull/1984), [@RodericDay](https://github.com/RodericDay)) +- ospec v2.0.0 (to be released) + - Added support for custom reporters ([#2009](https://github.com/MithrilJS/mithril.js/pull/2020)) + - Make Ospec more [Flems](https://flems.io)-friendly ([#2034](https://github.com/MithrilJS/mithril.js/pull/2034)) + - Works either as a global or in CommonJS environments + - the o.run() report is always printed asynchronously (it could be synchronous before if none of the tests were async). + - Properly point to the assertion location of async errors [#2036](https://github.com/MithrilJS/mithril.js/issues/2036) + - expose the default reporter as `o.report(results)` + - Don't try to access the stack traces in IE9 + +--- + +### v1.1.5 + +- API: If a user sets the Content-Type header within a request's options, that value will be the entire header value rather than being appended to the default value ([#1924](https://github.com/MithrilJS/mithril.js/pull/1924)) + +--- + +### v1.1.4 + +#### Bug fixes: + +- Fix IE bug where active element is null causing render function to throw error ([#1943](https://github.com/MithrilJS/mithril.js/pull/1943), [@JacksonJN](https://github.com/JacksonJN)) + +#### Ospec improvements: + +- Log using util.inspect to show object content instead of "[object Object]" ([#1661](https://github.com/MithrilJS/mithril.js/issues/1661), [@porsager](https://github.com/porsager)) +--- + +### v1.1.3 + +#### Bug fixes: + +- move out npm dependencies added by mistake + +--- + +### v1.1.2 + +#### Bug fixes: + +- core: Namespace fixes [#1819](https://github.com/MithrilJS/mithril.js/issues/1819), ([#1825](https://github.com/MithrilJS/mithril.js/pull/1825) [@SamuelTilly](https://github.com/SamuelTilly)), [#1820](https://github.com/MithrilJS/mithril.js/issues/1820) ([#1864](https://github.com/MithrilJS/mithril.js/pull/1864)), [#1872](https://github.com/MithrilJS/mithril.js/issues/1872) ([#1873](https://github.com/MithrilJS/mithril.js/pull/1873)) +- core: Fix select option to allow empty string value [#1814](https://github.com/MithrilJS/mithril.js/issues/1814) ([#1828](https://github.com/MithrilJS/mithril.js/pull/1828) [@spacejack](https://github.com/spacejack)) +- core: Reset e.redraw when it was set to `false` [#1850](https://github.com/MithrilJS/mithril.js/issues/1850) ([#1890](https://github.com/MithrilJS/mithril.js/pull/1890)) +- core: differentiate between `{ value: "" }` and `{ value: 0 }` for form elements [#1595 comment](https://github.com/MithrilJS/mithril.js/pull/1595#issuecomment-304071453) ([#1862](https://github.com/MithrilJS/mithril.js/pull/1862)) +- core: Don't reset the cursor of textareas in IE10 when setting an identical `value` [#1870](https://github.com/MithrilJS/mithril.js/issues/1870) ([#1871](https://github.com/MithrilJS/mithril.js/pull/1871)) +- hypertext: Correct handling of `[value=""]` ([#1843](https://github.com/MithrilJS/mithril.js/issues/1843), [@CreaturesInUnitards](https://github.com/CreaturesInUnitards)) +- router: Don't overwrite the options object when redirecting from `onmatch with m.route.set()` [#1857](https://github.com/MithrilJS/mithril.js/issues/1857) ([#1889](https://github.com/MithrilJS/mithril.js/pull/1889)) +- stream: Move the "use strict" directive inside the IIFE [#1831](https://github.com/MithrilJS/mithril.js/issues/1831) ([#1893](https://github.com/MithrilJS/mithril.js/pull/1893)) + +#### Ospec improvements: + +- Shell command: Ignore hidden directories and files ([#1855](https://github.com/MithrilJS/mithril.js/pull/1855) [@pdfernhout)](https://github.com/pdfernhout)) +- Library: Add the possibility to name new test suites ([#1529](https://github.com/MithrilJS/mithril.js/pull/1529)) + +#### Docs / Repo maintenance: + +Our thanks to [@0joshuaolson1](https://github.com/0joshuaolson1), [@ACXgit](https://github.com/ACXgit), [@cavemansspa](https://github.com/cavemansspa), [@CreaturesInUnitards](https://github.com/CreaturesInUnitards), [@dlepaux](https://github.com/dlepaux), [@isaaclyman](https://github.com/isaaclyman), [@kevinkace](https://github.com/kevinkace), [@micellius](https://github.com/micellius), [@spacejack](https://github.com/spacejack) and [@yurivish](https://github.com/yurivish) + +#### Other: + +- Addition of a performance regression test suite ([#1789](https://github.com/MithrilJS/mithril.js/issues/1789)) + +--- + ### v1.1.1 #### Bug fixes -- hyperscript: Allow `0` as the second argument to `m()` - [#1752](https://github.com/lhorie/mithril.js/issues/1752) / [#1753](https://github.com/lhorie/mithril.js/pull/1753) ([@StephanHoyer](https://github.com/StephanHoyer)) -- hyperscript: restore `attrs.class` handling to what it was in v1.0.1 - [#1764](https://github.com/lhorie/mithril.js/issues/1764) / [#1769](https://github.com/lhorie/mithril.js/pull/1769) +- hyperscript: Allow `0` as the second argument to `m()` - [#1752](https://github.com/MithrilJS/mithril.js/issues/1752) / [#1753](https://github.com/MithrilJS/mithril.js/pull/1753) ([@StephanHoyer](https://github.com/StephanHoyer)) +- hyperscript: restore `attrs.class` handling to what it was in v1.0.1 - [#1764](https://github.com/MithrilJS/mithril.js/issues/1764) / [#1769](https://github.com/MithrilJS/mithril.js/pull/1769) - documentation improvements ([@JAForbes](https://github.com/JAForbes), [@smuemd](https://github.com/smuemd), [@hankeypancake](https://github.com/hankeypancake)) +--- + ### v1.1.0 #### News @@ -26,10 +155,10 @@ #### Bug fixes -- fix IE11 input[type] error - [#1610](https://github.com/lhorie/mithril.js/issues/1610) -- apply [#1609](https://github.com/lhorie/mithril.js/issues/1609) to unkeyed children case -- fix abort detection [#1612](https://github.com/lhorie/mithril.js/issues/1612) -- fix input value focus issue when value is loosely equal to old value [#1593](https://github.com/lhorie/mithril.js/issues/1593) +- fix IE11 input[type] error - [#1610](https://github.com/MithrilJS/mithril.js/issues/1610) +- apply [#1609](https://github.com/MithrilJS/mithril.js/issues/1609) to unkeyed children case +- fix abort detection [#1612](https://github.com/MithrilJS/mithril.js/issues/1612) +- fix input value focus issue when value is loosely equal to old value [#1593](https://github.com/MithrilJS/mithril.js/issues/1593) --- @@ -37,12 +166,12 @@ #### News -- performance improvements in IE [#1598](https://github.com/lhorie/mithril.js/pull/1598) +- performance improvements in IE [#1598](https://github.com/MithrilJS/mithril.js/pull/1598) #### Bug fixes -- prevent infinite loop in non-existent default route - [#1579](https://github.com/lhorie/mithril.js/issues/1579) -- call correct lifecycle methods on children of recycled keyed vnodes - [#1609](https://github.com/lhorie/mithril.js/issues/1609) +- prevent infinite loop in non-existent default route - [#1579](https://github.com/MithrilJS/mithril.js/issues/1579) +- call correct lifecycle methods on children of recycled keyed vnodes - [#1609](https://github.com/MithrilJS/mithril.js/issues/1609) --- @@ -155,7 +284,7 @@ m("div", { // Called after the node is updated onupdate : function(vnode) { /*...*/ }, // Called before the node is removed, return a Promise that resolves when - // ready for the node to be removed from the DOM + // ready for the node to be removed from the DOM onbeforeremove : function(vnode) { /*...*/ }, // Called before the node is removed, but after onbeforeremove calls done() onremove : function(vnode) { /*...*/ } @@ -472,9 +601,9 @@ In `v0.2.x` reading route params was entirely handled through `m.route.param()`. ```javascript m.route(document.body, "/booga", { "/:attr" : { - controller : function() { - m.route.param("attr") // "booga" - }, + controller : function() { + m.route.param("attr") // "booga" + }, view : function() { m.route.param("attr") // "booga" } @@ -489,11 +618,11 @@ m.route(document.body, "/booga", { "/:attr" : { oninit : function(vnode) { vnode.attrs.attr // "booga" - m.route.param("attr") // "booga" + m.route.param("attr") // "booga" }, view : function(vnode) { vnode.attrs.attr // "booga" - m.route.param("attr") // "booga" + m.route.param("attr") // "booga" } } }) @@ -531,14 +660,14 @@ It is no longer possible to prevent unmounting via `onunload`'s `e.preventDefaul ```javascript var Component = { - controller: function() { - this.onunload = function(e) { - if (condition) e.preventDefault() - } - }, - view: function() { - return m("a[href=/]", {config: m.route}) - } + controller: function() { + this.onunload = function(e) { + if (condition) e.preventDefault() + } + }, + view: function() { + return m("a[href=/]", {config: m.route}) + } } ``` @@ -546,9 +675,9 @@ var Component = { ```javascript var Component = { - view: function() { - return m("a", {onclick: function() {if (!condition) m.route.set("/")}}) - } + view: function() { + return m("a", {onclick: function() {if (!condition) m.route.set("/")}}) + } } ``` @@ -562,14 +691,14 @@ Components no longer call `this.onunload` when they are being removed. They now ```javascript var Component = { - controller: function() { - this.onunload = function(e) { - // ... - } - }, - view: function() { - // ... - } + controller: function() { + this.onunload = function(e) { + // ... + } + }, + view: function() { + // ... + } } ``` @@ -577,12 +706,12 @@ var Component = { ```javascript var Component = { - onremove : function() { - // ... - } - view: function() { - // ... - } + onremove : function() { + // ... + } + view: function() { + // ... + } } ``` @@ -598,13 +727,13 @@ In addition, requests no longer have `m.startComputation`/`m.endComputation` sem ```javascript var data = m.request({ - method: "GET", - url: "https://api.github.com/", - initialValue: [], + method: "GET", + url: "https://api.github.com/", + initialValue: [], }) setTimeout(function() { - console.log(data()) + console.log(data()) }, 1000) ``` @@ -613,15 +742,15 @@ setTimeout(function() { ```javascript var data = [] m.request({ - method: "GET", - url: "https://api.github.com/", + method: "GET", + url: "https://api.github.com/", }) .then(function (responseBody) { - data = responseBody + data = responseBody }) setTimeout(function() { - console.log(data) // note: not a getter-setter + console.log(data) // note: not a getter-setter }, 1000) ``` @@ -653,11 +782,11 @@ greetAsync() ```javascript var greetAsync = function() { - return new Promise(function(resolve){ - setTimeout(function() { - resolve("hello") - }, 1000) - }) + return new Promise(function(resolve){ + setTimeout(function() { + resolve("hello") + }, 1000) + }) } greetAsync() @@ -679,7 +808,7 @@ m.sync([ m.request({ method: 'GET', url: 'https://api.github.com/users/isiahmeadows' }), ]) .then(function (users) { - console.log("Contributors:", users[0].name, "and", users[1].name) + console.log("Contributors:", users[0].name, "and", users[1].name) }) ``` @@ -691,7 +820,7 @@ Promise.all([ m.request({ method: 'GET', url: 'https://api.github.com/users/isiahmeadows' }), ]) .then(function (users) { - console.log("Contributors:", users[0].name, "and", users[1].name) + console.log("Contributors:", users[0].name, "and", users[1].name) }) ``` @@ -706,7 +835,7 @@ In `v0.2.x`, the `xlink` namespace was the only supported attribute namespace, a ```javascript m("svg", // the `href` attribute is namespaced automatically - m("image[href='image.gif']") + m("image[href='image.gif']") ) ``` @@ -715,7 +844,7 @@ m("svg", ```javascript m("svg", // User-specified namespace on the `href` attribute - m("image[xlink:href='image.gif']") + m("image[xlink:href='image.gif']") ) ``` diff --git a/docs/code-of-conduct.md b/docs/code-of-conduct.md new file mode 100644 index 00000000..a8875af5 --- /dev/null +++ b/docs/code-of-conduct.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [github@patcavit.com](mailto:github@patcavit.com?subject=Mithril%20Code%20of%20Conduct). All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/docs/components.md b/docs/components.md index 229f1293..cbb97cc0 100644 --- a/docs/components.md +++ b/docs/components.md @@ -61,6 +61,9 @@ var ComponentWithHooks = { oncreate: function(vnode) { console.log("DOM created") }, + onbeforeupdate: function(vnode, old) { + return true + }, onupdate: function(vnode) { console.log("DOM updated") }, @@ -74,9 +77,6 @@ var ComponentWithHooks = { onremove: function(vnode) { console.log("removing DOM element") }, - onbeforeupdate: function(vnode, old) { - return true - }, view: function(vnode) { return "hello" } @@ -177,7 +177,7 @@ They can be consumed in the same way regular components can. m.render(document.body, m(closureComponent)) // EXAMPLE: via m.mount -m.mount(document.body, closuresComponent) +m.mount(document.body, closureComponent) // EXAMPLE: via m.route m.route(document.body, "/", { @@ -316,7 +316,7 @@ var Login = { Normally, in the context of a larger application, a login component like the one above exists alongside components for user registration and password recovery. Imagine that we want to be able to prepopulate the email field when navigating from the login screen to the registration or password recovery screens (or vice versa), so that the user doesn't need to re-type their email if they happened to fill the wrong page (or maybe you want to bump the user to the registration form if a username is not found). -Right away, we see that sharing the `username` and `password` fields from this component to another is difficult. This is because the fat component encapsulates its our state, which by definition makes this state difficult to access from outside. +Right away, we see that sharing the `username` and `password` fields from this component to another is difficult. This is because the fat component encapsulates its state, which by definition makes this state difficult to access from outside. It makes more sense to refactor this component and pull the state code out of the component and into the application's data layer. This can be as simple as creating a new module: diff --git a/docs/contributing.md b/docs/contributing.md index 5b4770d3..4e672470 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -4,7 +4,7 @@ ## How do I go about contributing ideas or new features? -Create an [issue thread on Github](https://github.com/lhorie/mithril.js/issues/new) to suggest your idea so the community can discuss it. +Create an [issue thread on GitHub](https://github.com/MithrilJS/mithril.js/issues/new) to suggest your idea so the community can discuss it. If the consensus is that it's a good idea, the fastest way to get it into a release is to send a pull request. Without a PR, the time to implement the feature will depend on the bandwidth of the development team and its list of priorities. @@ -20,12 +20,14 @@ Ideally, the best way to report bugs is to provide a small snippet of code where To send a pull request: -- fork the repo (button at the top right in Github) -- clone the forked repo to your computer (green button in Github) +- fork the repo (button at the top right in GitHub) +- clone the forked repo to your computer (green button in GitHub) +- Switch to the `next` branch (run `git checkout next`) - create a feature branch (run `git checkout -b the-feature-branch-name`) - make your changes -- run the tests (run `npm t`) -- submit a pull request (go to the pull requests tab in Github, click the green button and select your feature branch) +- run the tests (run `npm test`) +- push your changes to your fork +- submit a pull request (go to the pull requests tab in GitHub, click the green button and select your feature branch) diff --git a/docs/css.md b/docs/css.md index 7664d1ca..ab4f3e2f 100644 --- a/docs/css.md +++ b/docs/css.md @@ -87,7 +87,7 @@ Nowadays there are [a lot of CSS-in-JS libraries with various degrees of robustn The main problem with many of these libraries is that even though they require a non-trivial amount of transpiler tooling and configuration, they also require sacrificing code readability in order to work, e.g. `` vs `` (or `m("a.button.danger")` if we're using hyperscript). -Often sacrifices also need to be made at time of debugging, when mapping rendered CSS class names back to their source. Often all you get in browser developer tools is a class like `button_fvp6zc2gdj35evhsl73ffzq_0 danger_fgdl0s2a5fmle5g56rbuax71_0` with useless source maps (or worse, entirely criptic class names). +Often sacrifices also need to be made at time of debugging, when mapping rendered CSS class names back to their source. Often all you get in browser developer tools is a class like `button_fvp6zc2gdj35evhsl73ffzq_0 danger_fgdl0s2a5fmle5g56rbuax71_0` with useless source maps (or worse, entirely cryptic class names). Another common issue is lack of support for less basic CSS features such as `@keyframes` and `@font-face`. diff --git a/docs/es6.md b/docs/es6.md index 3c19613f..0192a6ca 100644 --- a/docs/es6.md +++ b/docs/es6.md @@ -5,7 +5,7 @@ --- -Mithril is written in ES5, and is fully compatible with ES6 as well. ES6 is a recent update to Javascript that introduces new syntax sugar for various common cases. It's not yet fully supported by all major browsers and it's not a requirement for writing application, but it may be pleasing to use depending on your team's preferences. +Mithril is written in ES5, and is fully compatible with ES6 as well. ES6 is a recent update to Javascript that introduces new syntax sugar for various common cases. It's not yet fully supported by all major browsers and it's not a requirement for writing an application, but it may be pleasing to use depending on your team's preferences. In some limited environments, it's possible to use a significant subset of ES6 directly without extra tooling (for example, in internal applications that do not support IE). However, for the vast majority of use cases, a compiler toolchain like [Babel](https://babeljs.io) is required to compile ES6 features down to ES5. @@ -70,10 +70,12 @@ Create a `.babelrc` file: Next, create a file called `webpack.config.js` ```javascript +const path = require('path') + module.exports = { entry: './src/index.js', output: { - path: './bin', + path: path.resolve(__dirname, './bin'), filename: 'app.js', }, module: { diff --git a/docs/examples.md b/docs/examples.md index 140ad3f6..749900cc 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -2,10 +2,10 @@ Here are some examples of Mithril in action -- [Animation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/animation/mosaic.html) -- [DBMonster](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html) -- [Markdown Editor](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/editor/index.html) -- SVG: [Clock](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/svg/clock.html), [Ring](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/svg/ring.html), [Tiger](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/svg/tiger.html) -- [ThreadItJS](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/threaditjs/index.html) -- [TodoMVC](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/todomvc/index.html) +- [Animation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/animation/mosaic.html) +- [DBMonster](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html) +- [Markdown Editor](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/editor/index.html) +- SVG: [Clock](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/svg/clock.html), [Ring](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/svg/ring.html), [Tiger](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/svg/tiger.html) +- [ThreadItJS](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/threaditjs/index.html) +- [TodoMVC](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/todomvc/index.html) diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100644 index 00000000..61807fae Binary files /dev/null and b/docs/favicon.ico differ diff --git a/docs/favicon.png b/docs/favicon.png new file mode 100644 index 00000000..50712630 Binary files /dev/null and b/docs/favicon.png differ diff --git a/docs/framework-comparison.md b/docs/framework-comparison.md index cf8a3483..0969b2c9 100644 --- a/docs/framework-comparison.md +++ b/docs/framework-comparison.md @@ -74,7 +74,7 @@ What these numbers show is that not only does Mithril initializes significantly Update performance can be even more important than first-render performance, since updates can happen many times while a Single Page Application is running. -A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare a [React implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/react/index.html) and a [Mithril implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html). Sample results are shown below: +A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare a [React implementation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/react/index.html) and a [Mithril implementation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html). Sample results are shown below: React | Mithril ------- | ------- @@ -139,7 +139,7 @@ Also, remember that frameworks like Angular and Mithril are designed for non-tri ##### Update performance -A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare an [Angular implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/angular/index.html) and a [Mithril implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below: +A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare an [Angular implementation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/angular/index.html) and a [Mithril implementation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below: Angular | Mithril ------- | ------- @@ -193,7 +193,7 @@ Library load times matter in applications that don't stay open for long periods ##### Update performance -A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare a [Vue implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/vue/index.html) and a [Mithril implementation](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below: +A useful tool to benchmark update performance is a tool developed by the Ember team called DbMonster. It updates a table as fast as it can and measures frames per second (FPS) and Javascript times (min, max and mean). The FPS count can be difficult to evaluate since it also includes browser repaint times and `setTimeout` clamping delay, so the most meaningful number to look at is the mean render time. You can compare a [Vue implementation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/vue/index.html) and a [Mithril implementation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html). Both implementations are naive (i.e. no optimizations). Sample results are shown below: Vue | Mithril ------ | ------- diff --git a/docs/generate.js b/docs/generate.js index 63628a72..5650ae9d 100644 --- a/docs/generate.js +++ b/docs/generate.js @@ -12,9 +12,6 @@ try {fs.mkdirSync("./dist/archive/v" + version)} catch (e) {/* ignore */} var guides = fs.readFileSync("docs/nav-guides.md", "utf-8") var methods = fs.readFileSync("docs/nav-methods.md", "utf-8") -var index = fs.readFileSync("docs/index.md", "utf-8") -fs.writeFileSync("README.md", index.replace(/(\]\()(.+?)\.md(\))/g, "$1http://mithril.js.org/$2.html$3"), "utf-8") - generate("docs") function generate(pathname) { @@ -66,8 +63,9 @@ function generate(pathname) { fs.writeFileSync("./dist/" + outputFilename.replace(/^docs\//, ""), html, "utf-8") } else if (!pathname.match(/lint|generate/)) { - fs.writeFileSync("./dist/archive/v" + version + "/" + pathname.replace(/^docs\//, ""), fs.readFileSync(pathname, "utf-8"), "utf-8") - fs.writeFileSync("./dist/" + pathname.replace(/^docs\//, ""), fs.readFileSync(pathname, "utf-8"), "utf-8") + var encoding = (/\.(ico|png)$/i).test(path.extname(pathname)) ? "binary" : "utf-8"; + fs.writeFileSync("./dist/archive/v" + version + "/" + pathname.replace(/^docs\//, ""), fs.readFileSync(pathname, encoding), encoding) + fs.writeFileSync("./dist/" + pathname.replace(/^docs\//, ""), fs.readFileSync(pathname, encoding), encoding) } } } diff --git a/docs/index.md b/docs/index.md index 22bb9021..64018eff 100644 --- a/docs/index.md +++ b/docs/index.md @@ -225,7 +225,7 @@ var Splash = { As you can see, this component simply renders a link to `#!/hello`. The `#!` part is known as a hashbang, and it's a common convention used in Single Page Applications to indicate that the stuff after it (the `/hello` part) is a route path. -Now that we going to have more than one screen, we use `m.route` instead of `m.mount`. +Now that we're going to have more than one screen, we use `m.route` instead of `m.mount`. ```javascript m.route(root, "/splash", { diff --git a/docs/installation.md b/docs/installation.md index f0c527bc..5689bc25 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -2,6 +2,8 @@ - [CDN](#cdn) - [NPM](#npm) +- [Quick start with Webpack](#quick-start-with-webpack) +- [TypeScript](#typescript) ### CDN @@ -15,30 +17,56 @@ If you're new to Javascript or just want a very simple setup to get your feet we ### NPM -#### Quick start with Webpack - ```bash -# 1) install -npm install mithril --save - -npm install webpack --save - -# 2) add this line into the scripts section in package.json -# "scripts": { -# "start": "webpack src/index.js bin/app.js --watch" -# } - -# 3) create an `src/index.js` file - -# 4) create an `index.html` file containing `` - -# 5) run bundler -npm start - -# 6) open `index.html` in the (default) browser -open index.html +$ npm install mithril --save ``` +--- + +### Quick start with Webpack + +1. Initialize the directory as an npm package +```bash +$ npm init --yes +``` + +2. install required tools +```bash +$ npm install mithril --save +$ npm install webpack webpack-cli --save-dev +``` + +3. Add a "start" entry to the scripts section in `package.json`. +```js +{ + // ... + "scripts": { + "start": "webpack src/index.js --output bin/app.js -d --watch" + } +} +``` + +4. Create `src/index.js` file. +```js +import m from "mithril"; +m.render(document.body, "hello world"); +``` + +5. create `index.html` +```html + + + + +``` + +6. run bundler +```bash +$ npm start +``` + +7. open `index.html` in a browser + #### Step by step For production-level projects, the recommended way of installing Mithril is to use NPM. @@ -78,7 +106,7 @@ Most browser today do not natively support modularization systems (CommonJS or E A popular way for creating a bundle is to setup an NPM script for [Webpack](https://webpack.js.org/). To install Webpack, run this from the command line: ```bash -npm install webpack --save-dev +npm install webpack webpack-cli --save-dev ``` Open the `package.json` that you created earlier, and add an entry to the `scripts` section: @@ -87,7 +115,7 @@ Open the `package.json` that you created earlier, and add an entry to the `scrip { "name": "my-project", "scripts": { - "start": "webpack src/index.js bin/app.js -d --watch" + "start": "webpack src/index.js --output bin/app.js -d --watch" } } ``` @@ -149,8 +177,8 @@ If you open bin/app.js, you'll notice that the Webpack bundle is not minified, s { "name": "my-project", "scripts": { - "start": "webpack src/index.js bin/app.js -d --watch", - "build": "webpack src/index.js bin/app.js -p", + "start": "webpack src/index.js --output bin/app.js -d --watch", + "build": "webpack src/index.js --output bin/app.js -p", } } ``` @@ -219,7 +247,7 @@ If you don't have the ability to run a bundler script due to company security po Hello world - + @@ -231,3 +259,15 @@ If you don't have the ability to run a bundler script due to company security po // if a CommonJS environment is not detected, Mithril will be created in the global scope m.render(document.body, "hello world") ``` + +--- + +### TypeScript + +TypeScript type definitions are available from DefinitelyTyped. They can be installed with: + +```bash +$ npm install @types/mithril --save-dev +``` + +For example usage, to file issues or to discuss TypeScript related topics visit: https://github.com/MithrilJS/mithril.d.ts diff --git a/docs/integrating-libs.md b/docs/integrating-libs.md new file mode 100644 index 00000000..cf0753de --- /dev/null +++ b/docs/integrating-libs.md @@ -0,0 +1,57 @@ +# 3rd Party Integration + +Integration with third party libraries or vanilla javascript code can be achieved via [lifecycle methods](lifecycle-methods.md). + +## Example + +```javascript +var FullCalendar = { + + oncreate: function (vnode) { + console.log('FullCalendar::oncreate') + $(vnode.dom).fullCalendar({ + // put your initial options and callbacks here + }) + + Object.assign(vnode.attrs.parentState, {fullCalendarEl: vnode.dom}) + }, + + // Consider that the lib will modify this parent element in the DOM (e.g. add dependent class attribute and values). + // As long as you return the same view results here, mithril will not + // overwrite the actual DOM because it's always comparing old and new VDOM + // before applying DOM updates. + view: function (vnode) { + return m('div') + }, + + onbeforeremove: function (vnode) { + // Run any destroy / cleanup methods here. + //E.g. $(vnode.state.fullCalendarEl).fullCalendar('destroy') + } +} + +m.mount(document.body, { + view: function (vnode) { + return [ + m('h1', 'Calendar'), + m(FullCalendar, {parentState: vnode.state}), + m('button', {onclick: prev}, 'Mithril Button -'), + m('button', {onclick: next}, 'Mithril Button +') + + ] + + function next() { + $(vnode.state.fullCalendarEl).fullCalendar('next') + } + + function prev() { + $(vnode.state.fullCalendarEl).fullCalendar('prev') + } + + } + +}) + +``` + +Running example [flems: FullCalendar](https://flems.io/#0=N4IgZglgNgpgziAXAbVAOwIYFsZJAOgAsAXLKEAGhAGMB7NYmBvAHigjQGsACAJxigBeADog4xAJ6w4hGDGKjuhfmBEgSxAA5xEAel3UAJmgBWcfNSi0ArobBQM-C7Sy6MJjAA9d7AEZxdMGsoKGoMWDRDR10AZnwAdnwABkDg0PCmKN58LA4LODhRAD4QAF8KdGxcRAIzShp6RmYagDdHbgAxNIBhDMj2wW5gYTQR4WJ6an4MRkRuILRqYgh6bgAKFrRaQxgASiGxhWI6NDhaWHwrAHM1gHIukN6oft5EREnpxlvdw-GAEg2Wx2+EMLl2+CCjz6WTWw1GR3G+m4mmsxG4EhsvG4HAgy3C3FommW9Dg3AwkW4YRCvgw1E4pNk-F+xFKP1G8PGAHlfCYYEt8BgChArmhAdsYALiMReOZNI4mMQAMrEGYwChDSFQJ6ZRwAUSgc024pBLlZh3KY3hLQgMAA7nMFksVmh1kadvs4eNxvxiNZeC6sHdDBAWt9zRRLeN6L4YGBaPx+FhaC0YA7rItiS6xe6DhziEiAErpsloCTcHbiXi0Mu6SmwcnWTTcHDEQjbBkwJzM-QAt0S8SqiE9aF6qDgzXal5B+DS6th+GlEaL9lYHI2BhrUHUaw4Bj4XzbCTqz3Ea12tMZ52uoF7XNe6XyP0u5DM8aB26EACMt3Vt0nWW+CM8zfNYHi1EdeGPOV+AYZVVUNG98AHRhWSA+8QNuXxUQmNAfzvBEjkmdg6TmTR+BaV8WV-ABZXFlGgbgACFsNWABaQDKPfLCpXoPCT3QnDLAgEjuDQGBPAUYCqO4W5aNbXgGOYniXQAannZkAF1IyOR1M1E8TiDWD1KN7RDkIlCcIP1cdhwiGFbjEiT1KOZdmV0q8yJgFojPw+9TONcyhyhOzRxs4KdV4O5PNDNl71chdLVZMoKhATAcDwfIECoE4mmIPAyg0qh2C4BAUEqdKalyeToHqP1yBqDRtD0XR000TgrmcVwqvoqAAAFP3wAaAFZdG6hSoHwOoqEkTRqhAOpynKuak13PKqDqvBGp0fRWvazrRpcBVeoAJkGgBOfBjoO1bJqykAZrmhaUrSx6AEdrE7CRat4er1ClJqdrQNqOroVwTHez7eriU7P10YNxF0cGPt4CRbvqB68Cepa8E1KkIu+36tua3aQZcVIQjxl4oYSZI4YgBHcYgtHpokWbMYQUoNNKIA) diff --git a/docs/jsx.md b/docs/jsx.md index 9ce6163c..8c715072 100644 --- a/docs/jsx.md +++ b/docs/jsx.md @@ -39,7 +39,7 @@ When using JSX, it's possible to interpolate Javascript expressions within JSX t var greeting = "Hello" var url = "http://google.com" var link = {greeting + "!"} -// yields Hello +// yields Hello! ``` Components can be used by using a convention of uppercasing the first letter of the component name: @@ -98,7 +98,7 @@ npm install babel-core babel-loader babel-preset-es2015 babel-plugin-transform-r Create a `.babelrc` file: -``` +```json { "presets": ["es2015"], "plugins": [ @@ -112,14 +112,16 @@ Create a `.babelrc` file: Next, create a file called `webpack.config.js` ```javascript +const path = require('path') + module.exports = { entry: './src/index.js', output: { - path: './bin', + path: path.resolve(__dirname, './bin'), filename: 'app.js', }, module: { - loaders: [{ + rules: [{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' @@ -128,6 +130,8 @@ module.exports = { } ``` +For those familiar with Webpack already, please note that adding the Babel options to the `babel-loader` section of your `webpack.config.js` will throw an error, so you need to include them in the separate `.babelrc` file. + This configuration assumes the source code file for the application entry point is in `src/index.js`, and this will output the bundle to `bin/app.js`. To run the bundler, setup an npm script. Open `package.json` and add this entry under `"scripts"`: @@ -187,7 +191,7 @@ JSX is useful for teams where HTML is primarily written by someone without Javas Hyperscript is the compiled representation of JSX. It's designed to be readable and can also be used as-is, instead of JSX (as is done in most of the documentation). Hyperscript tends to be terser than JSX for a couple of reasons: - it does not require repeating the tag name in closing tags (e.g. `m("div")` vs `
`) -- static attributes can be written using CSS selector syntax (i.e. `m("a.button")` vs `
` +- static attributes can be written using CSS selector syntax (i.e. `m("a.button")` vs ``) In addition, since hyperscript is plain Javascript, it's often more natural to indent than JSX: diff --git a/docs/keys.md b/docs/keys.md index 0ad0b0c5..377e65ba 100644 --- a/docs/keys.md +++ b/docs/keys.md @@ -180,7 +180,7 @@ m("div", [ #### Avoid passing model data directly to components if the model uses `key` as a data property -The `key` property may appear in your data model in a way that conflicts with Mithril's key logic. For example, a component may represent an entity whose `key` property is expected to change over time. This can lead to components receiving the wrong data, re-initialise, or change positions unexpectedly. If your data model uses the `key` property, make sure to wrap the data such that Mithril doesn't misinterpret it as a rendering instruction: +The `key` property may appear in your data model in a way that conflicts with Mithril's key logic. For example, a component may represent an entity whose `key` property is expected to change over time. This can lead to components receiving the wrong data, re-initialize, or change positions unexpectedly. If your data model uses the `key` property, make sure to wrap the data such that Mithril doesn't misinterpret it as a rendering instruction: ```javascript // Data model diff --git a/docs/layout.html b/docs/layout.html index 02e07a0b..fa06f4f1 100644 --- a/docs/layout.html +++ b/docs/layout.html @@ -4,6 +4,7 @@ Mithril.js + @@ -14,8 +15,8 @@ diff --git a/docs/learning-mithril.md b/docs/learning-mithril.md new file mode 100644 index 00000000..049b11b9 --- /dev/null +++ b/docs/learning-mithril.md @@ -0,0 +1,5 @@ +# Learning Resources + +Links to Mithril learning content: + +- [Mithril 0-60](https://scrimba.com/playlist/playlist-34) diff --git a/docs/mount.md b/docs/mount.md index 9e0957a6..19a0110a 100644 --- a/docs/mount.md +++ b/docs/mount.md @@ -35,12 +35,12 @@ m.mount(element, {view: function () {return m(Component, attrs)}}) ### Signature -`m.mount(element, component)` +`m.mount(element, Component)` Argument | Type | Required | Description ----------- | -------------------- | -------- | --- `element` | `Element` | Yes | A DOM element that will be the parent node to the subtree -`component` | `Component|null` | Yes | The [component](components.md) to be rendered. `null` unmounts the tree and cleans up internal state. +`Component` | `Component|null` | Yes | The [component](components.md) to be rendered. `null` unmounts the tree and cleans up internal state. **returns** | | | Returns nothing [How to read signatures](signatures.md) @@ -49,7 +49,9 @@ Argument | Type | Required | Description ### How it works -Similar to [`m.render()`](render.md), the `m.mount()` method takes a component and mounts a corresponding DOM tree into `element`. If `element` already has a DOM tree mounted via a previous `m.mount()` call, the component is diffed against the previous vnode tree and the existing DOM tree is modified only where needed to reflect the changes. Unchanged DOM nodes are not touched at all. +`m.mount(element, Component)`, when called renders the component into the element and subscribe the `(element, Component)` pair to the redraw subsystem. That tree will be re-rendered when [manual](redraw.md) or [automatic](autoredraw.md) redraws are triggered. + +On redraw, the new vDOM tree is compared (or "diffed") with the old one, and the existing DOM tree is modified only where needed to reflect the changes. Unchanged DOM nodes are not touched at all. #### Replace a component @@ -73,7 +75,7 @@ In contrast, traversing a javascript data structure has a much more predictable ### Differences from m.render -A component rendered via `m.mount` automatically auto-redraws in response to view events, `m.redraw()` calls or `m.request()` calls. Vnodes rendered via `m.render()` do not. +A component rendered via `m.mount` [automatically redraws](autoredraw.md) in response to view events, `m.redraw()` calls or `m.request()` calls. Vnodes rendered via `m.render()` do not. `m.mount()` is suitable for application developers integrating Mithril widgets into existing codebases where routing is handled by another library or framework, while still enjoying Mithril's auto-redrawing facilities. diff --git a/docs/nav-guides.md b/docs/nav-guides.md index 3a8c8313..0a954c4d 100644 --- a/docs/nav-guides.md +++ b/docs/nav-guides.md @@ -1,7 +1,9 @@ -- Tutorials - - [Installation](installation.md) +- Getting Started - [Introduction](index.md) + - [Installation](installation.md) - [Tutorial](simple-application.md) + - [Learning Resources](learning-mithril.md) + - [Getting Help](support.md) - Resources - [JSX](jsx.md) - [ES6](es6.md) @@ -9,6 +11,7 @@ - [Animation](animation.md) - [Testing](testing.md) - [Examples](examples.md) + - [3rd Party Integration](integrating-libs.md) - Key concepts - [Vnodes](vnodes.md) - [Components](components.md) @@ -16,9 +19,10 @@ - [Keys](keys.md) - [Autoredraw system](autoredraw.md) - Social - - [Mithril Jobs](https://github.com/lhorie/mithril.js/wiki/JOBS) + - [Mithril Jobs](https://github.com/MithrilJS/mithril.js/wiki/JOBS) - [How to contribute](contributing.md) - [Credits](credits.md) + - [Code of Conduct](code-of-conduct.md) - Misc - [Framework comparison](framework-comparison.md) - [Change log/Migration](change-log.md) diff --git a/docs/nav-methods.md b/docs/nav-methods.md index 06809b50..06ae1f42 100644 --- a/docs/nav-methods.md +++ b/docs/nav-methods.md @@ -16,4 +16,4 @@ - Optional - [Stream](stream.md) - Tooling - - [Ospec](https://github.com/lhorie/mithril.js/blob/rewrite/ospec) + - [Ospec](https://github.com/MithrilJS/mithril.js/blob/master/ospec) diff --git a/docs/promise.md b/docs/promise.md index 119d3a51..dc93f92d 100644 --- a/docs/promise.md +++ b/docs/promise.md @@ -26,6 +26,8 @@ A [ES6 Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/G A Promise is a mechanism for working with asynchronous computations. +Mithril provides a polyfill when the environment does not support Promises. The polyfill can also be referenced specifically via `m.PromisePolyfill`. + --- ### Signature @@ -302,7 +304,7 @@ This example also illustrates another benefit of smaller functions: we reused th ### Why not callbacks -Callbacks are another mechanism for working with asynchrounous computations, and are indeed more adequate to use if an asynchronous computation may occur more than one time (for example, an `onscroll` event handler). +Callbacks are another mechanism for working with asynchronous computations, and are indeed more adequate to use if an asynchronous computation may occur more than one time (for example, an `onscroll` event handler). However, for asynchronous computations that only occur once in response to an action, promises can be refactored more effectively, reducing code smells known as pyramids of doom (deeply nested series of callbacks with unmanaged state being used across several closure levels). diff --git a/docs/redraw.md b/docs/redraw.md index 85b397dc..0d079592 100644 --- a/docs/redraw.md +++ b/docs/redraw.md @@ -2,6 +2,8 @@ - [Description](#description) - [Signature](#signature) + - [Static members](#static-members) + -[m.redraw.sync()](#mredrawsync) - [How it works](#how-it-works) --- @@ -10,12 +12,10 @@ Updates the DOM after a change in the application data layer. -You DON'T need to call it if data is modified within the execution context of an event handler defined in a Mithril view, or after request completion when using `m.request`/`m.jsonp`. +You DON'T need to call it if data is modified within the execution context of an event handler defined in a Mithril view, or after request completion when using `m.request`/`m.jsonp`. The [autoredraw](autoredraw.md) system, which is built on top of `m.redraw()` will take care of it. You DO need to call it in `setTimeout`/`setInterval`/`requestAnimationFrame` callbacks, or callbacks from 3rd party libraries. -Typically, `m.redraw` triggers an asynchronous redraws, but it may trigger synchronously if Mithril detects it's possible to improve performance by doing so (i.e. if no redraw was requested within the last animation frame). You should write code assuming that it always redraws asynchronously. - --- ### Signature @@ -26,6 +26,16 @@ Argument | Type | Required | Description ----------- | -------------------- | -------- | --- **returns** | | | Returns nothing +#### Static members + +##### m.redraw.sync + +`m.redraw.sync()` + +Argument | Type | Required | Description +----------- | -------------------- | -------- | --- +**returns** | | | Returns nothing + --- ### How it works @@ -34,4 +44,12 @@ When callbacks outside of Mithril run, you need to notify Mithril's rendering en To trigger a redraw, call `m.redraw()`. Note that `m.redraw` only works if you used `m.mount` or `m.route`. If you rendered via `m.render`, you should use `m.render` to redraw. -You should not call m.redraw from a [lifecycle method](lifecycle-methods.md). Doing so will result in undefined behavior. +`m.redraw()` always triggers an asynchronous redraws, whereas `m.redraw.sync()` triggers a synchronous one. `m.redraw()` is tied to `window.requestAnimationFrame()` (we provide a fallback for IE9). It will thus typically fire at most 60 times per second. It may fire faster if your monitor has a higher refresh rate. + +`m.redraw.sync()` is mostly intended to make videos play work in iOS. That only works in response to user-triggered events. It comes with several caveat: + +- You should not call `m.redraw.sync()` from a [lifecycle method](lifecycle-methods.md) or the `view()` method of a component. Doing so will result in undefined behavior (it throws an error when possible). +- `m.redraw.sync()` called from an event handler can cause the DOM to be modified while an event is bubbling. Depending on the structure of the old and new DOM trees, the event can finish the bubbling phase in the new tree and trigger unwanted handlers. +- It is not throttled. One call to `m.redraw.sync()` causes immediately one `m.render()` call per root registered with [`m.mount()`](mount.md) or [`m.route()`](route.md). + +`m.redraw()` doesn't have any of those issues, you can call it from wherever you like. \ No newline at end of file diff --git a/docs/releasing.md b/docs/releasing.md index df5e3aeb..a6134586 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,5 +1,11 @@ # Mithril Release Processes +**Note** These steps all assume that `MithrilJS/mithril.js` is a git remote named `mithriljs`, adjust accordingly if that doesn't match your setup. + +- [Releasing a new Mithril version](#releasing-a-new-mithril-version) +- [Updating mithril.js.org](#updating-mithriljsorg) +- [Releasing a new ospec version](#releasing-a-new-ospec-version) + ## Releasing a new Mithril version ### Prepare the release @@ -7,8 +13,8 @@ 1. Ensure your local branch is up to date ```bash -$ git co next -$ git pull --rebase lhorie next +$ git checkout next +$ git pull --rebase mithriljs next ``` 2. Determine patch level of the change @@ -22,8 +28,8 @@ $ git commit -m "Preparing for release" # Push to your branch $ git push -# Push to lhorie/mithril.js -$ git push lhorie next +# Push to MithrilJS/mithril.js +$ git push mithriljs next ``` ### Merge from `next` to `master` @@ -31,8 +37,8 @@ $ git push lhorie next 5. Switch to `master` and make sure it's up to date ```bash -$ git co master -$ git pull --rebase lhorie master +$ git checkout master +$ git pull --rebase mithriljs master ``` 6. merge `next` on top of it @@ -53,10 +59,10 @@ $ npm test 8. `npm run release `, see the docs for [`npm version`](https://docs.npmjs.com/cli/version) 9. The changes will be automatically pushed to your fork -10. Push the changes to `lhorie/mithril.js` +10. Push the changes to `MithrilJS/mithril.js` ```bash -$ git push lhorie master +$ git push mithriljs master ``` 11. Travis will push the new release to npm & create a GitHub release @@ -68,8 +74,8 @@ This helps to ensure that the `version` field of `package.json` doesn't get out 12. Switch to `next` and make sure it's up to date ```bash -$ git co next -$ git pull --rebase lhorie next +$ git checkout next +$ git pull --rebase mithriljs next ``` 13. Merge `master` back onto `next` @@ -78,11 +84,11 @@ $ git pull --rebase lhorie next $ git merge master ``` -14. Push the changes to your fork & `lhorie/mithril.js` +14. Push the changes to your fork & `MithrilJS/mithril.js` ```bash $ git push -$ git push lhorie next +$ git push mithriljs next ``` ### Update the GitHub release @@ -94,19 +100,77 @@ $ git push lhorie next Fixes to documentation can land whenever, updates to the site are published via Travis. ```bash -# These steps assume that lhorie/mithril.js is a git remote named "lhorie" +# These steps assume that MithrilJS/mithril.js is a git remote named "mithriljs" # Ensure your next branch is up to date -$ git co next -$ git pull lhorie next +$ git checkout next +$ git pull mithriljs next # Splat the docs folder from next onto master -$ git co master -$ git co next -- ./docs +$ git checkout master +$ git checkout next -- ./docs # Manually ensure that no new feature docs were added -$ git push lhorie +$ git push mithriljs ``` After the Travis build completes the updated docs should appear on https://mithril.js.org in a few minutes. + +## Releasing a new ospec version + +1. Ensure your local branch is up to date + +```bash +$ git checkout next +$ git pull --rebase mithriljs next +``` + +2. Determine patch level of the change +3. Update `version` field in `ospec/package.json` to match new version being prepared for release +4. Commit changes to `next` + +``` +$ git add . +$ git commit -m "chore(ospec): ospec@" + +# Push to your branch +$ git push + +# Push to MithrilJS/mithril.js +$ git push mithriljs next +``` + +### Merge from `next` to `master` + +5. Switch to `master` and make sure it's up to date + +```bash +$ git checkout master +$ git pull --rebase mithriljs master +``` + +6. merge `next` on top of it + +```bash +$ git checkout next -- ./ospec +$ git add . +$ git commit -m "chore(ospec): ospec@" +``` + +7. Ensure the tests are passing! + +### Publish the release + +8. Push the changes to `MithrilJS/mithril.js` + +```bash +$ git push mithriljs master +``` + +9. Publish the changes to npm **from the `/ospec` folder**. That bit is important to ensure you don't accidentally ship a new Mithril release! + +```bash +$ cd ./ospec +$ npm publish +``` diff --git a/docs/render.md b/docs/render.md index 1a0f81d9..534a9d45 100644 --- a/docs/render.md +++ b/docs/render.md @@ -36,7 +36,7 @@ Argument | Type | Required | Description ### How it works -The `m.render(element, vnodes)` method takes a virtual DOM tree (typically generated via the [`m()` hyperscript function](hyperscript.md), generates a DOM tree and mounts it on `element`. If `element` already has a DOM tree mounted via a previous `m.render()` call, `vnodes` is diffed against the previous `vnodes` tree and the existing DOM tree is modified only where needed to reflect the changes. Unchanged DOM nodes are not touched at all. +The `m.render(element, vnodes)` method takes a virtual DOM tree (typically generated via the [`m()` hyperscript function](hyperscript.md)), generates a DOM tree and mounts it on `element`. If `element` already has a DOM tree mounted via a previous `m.render()` call, `vnodes` is diffed against the previous `vnodes` tree and the existing DOM tree is modified only where needed to reflect the changes. Unchanged DOM nodes are not touched at all. `m.render` is synchronous. @@ -68,4 +68,4 @@ Another difference is that `m.render` method expects a [vnode](vnodes.md) (or a The `m.render` module is similar in scope to view libraries like Knockout, React and Vue. It is approximately 500 lines of code (3kb min+gzip) and implements a virtual DOM diffing engine with a modern search space reduction algorithm and DOM recycling, which translate to top-of-class performance, both in terms of initial page load and re-rendering. It has no dependencies on other parts of Mithril and can be used as a standalone library. -Despite being incredibly small, the render module is fully functional and self-suficient. It supports everything you might expect: SVG, custom elements, and all valid attributes and events - without any weird case-sensitive edge cases or exceptions. Of course, it also fully supports [components](components.md) and [lifecycle methods](lifecycle-methods.md). +Despite being incredibly small, the render module is fully functional and self-sufficient. It supports everything you might expect: SVG, custom elements, and all valid attributes and events - without any weird case-sensitive edge cases or exceptions. Of course, it also fully supports [components](components.md) and [lifecycle methods](lifecycle-methods.md). diff --git a/docs/request.md b/docs/request.md index 0af751af..44bb954b 100644 --- a/docs/request.md +++ b/docs/request.md @@ -49,12 +49,13 @@ Argument | Type | Required | Descr `options.user` | `String` | No | A username for HTTP authorization. Defaults to `undefined`. `options.password` | `String` | No | A password for HTTP authorization. Defaults to `undefined`. This option is provided for `XMLHttpRequest` compatibility, but you should avoid using it because it sends the password in plain text over the network. `options.withCredentials` | `Boolean` | No | Whether to send cookies to 3rd party domains. Defaults to `false` +`options.timeout` | `Number` | No | The amount of milliseconds a request can take before automatically being [terminated](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout). Defaults to `undefined`. `options.config` | `xhr = Function(xhr)` | No | Exposes the underlying XMLHttpRequest object for low-level configuration. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function). `options.headers` | `Object` | No | Headers to append to the request before sending it (applied right before `options.config`). `options.type` | `any = Function(any)` | No | A constructor to be applied to each object in the response. Defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function). `options.serialize` | `string = Function(any)` | No | A serialization method to be applied to `data`. Defaults to `JSON.stringify`, or if `options.data` is an instance of [`FormData`](https://developer.mozilla.org/en/docs/Web/API/FormData), defaults to the [identity function](https://en.wikipedia.org/wiki/Identity_function) (i.e. `function(value) {return value}`). `options.deserialize` | `any = Function(string)` | No | A deserialization method to be applied to the `xhr.responseText`. Defaults to a small wrapper around `JSON.parse` that returns `null` for empty responses. If `extract` is defined, `deserialize` will be skipped. -`options.extract` | `any = Function(xhr, options)` | No | A hook to specify how the XMLHttpRequest response should be read. Useful for processing response data, reading headers and cookies. By default this is a function that returns `xhr.responseText`, which is in turn passed to `deserialize`. If a custom `extract` callback is provided, the `xhr` parameter is the XMLHttpRequest instance used for the request, and `options` is the object that was passed to the `m.request` call. Additionally, `deserialize` will be skipped and the value returned from the extract callback will not automatically be parsed as JSON. +`options.extract` | `any = Function(xhr, options)` | No | A hook to specify how the XMLHttpRequest response should be read. Useful for processing response data, reading headers and cookies. By default this is a function that returns `xhr.responseText`, which is in turn passed to `deserialize`. If a custom `extract` callback is provided, the `xhr` parameter is the XMLHttpRequest instance used for the request, and `options` is the object that was passed to the `m.request` call. Additionally, `deserialize` will be skipped and the value returned from the extract callback will be left as-is when the promise resolves. Furthermore, when an extract callback is provided, exceptions are *not* thrown when the server response status code indicates an error. `options.useBody` | `Boolean` | No | Force the use of the HTTP body section for `data` in `GET` requests when set to `true`, or the use of querystring for other HTTP methods when set to `false`. Defaults to `false` for `GET` requests and `true` for other methods. `options.background` | `Boolean` | No | If `false`, redraws mounted components upon completion of the request. If `true`, it does not. Defaults to `false`. **returns** | `Promise` | | A promise that resolves to the response data, after it has been piped through the `extract`, `deserialize` and `type` methods @@ -77,10 +78,12 @@ m.request({ }) ``` -A call to `m.request` return a [promise](promise.md) and trigger a redraw upon completion of its promise chain. +A call to `m.request` returns a [promise](promise.md) and triggers a redraw upon completion of its promise chain. By default, `m.request` assumes the response is in JSON format and parses it into a Javascript object (or array). +If the HTTP response status code indicates an error, the returned Promise will be rejected. Supplying an extract callback will prevent the promise rejection. + --- ### Typical usage @@ -287,7 +290,7 @@ function upload(e) { var data = new FormData() for (var i = 0; i < files.length; i++) { - data.append("file" + i, file) + data.append("file" + i, files[i]) } m.request({ @@ -329,7 +332,7 @@ function upload(e) { url: "/api/v1/upload", data: data, config: function(xhr) { - xhr.addEventListener("progress", function(e) { + xhr.upload.addEventListener("progress", function(e) { progress = e.loaded / e.total m.redraw() // tell Mithril that data changed and a re-render is needed @@ -426,7 +429,7 @@ m.request({ ### Retrieving response details -By default Mithril attempts to parse a response as JSON and returns `xhr.responseText`. It may be useful to inspect a server response in more detail, this can be accomplished by passing a custom `options.extract` function: +By default Mithril attempts to parse `xhr.responseText` as JSON and returns the parsed object. It may be useful to inspect a server response in more detail and process it manually. This can be accomplished by passing a custom `options.extract` function: ```javascript m.request({ diff --git a/docs/route.md b/docs/route.md index d5a9c83a..0f253f5b 100644 --- a/docs/route.md +++ b/docs/route.md @@ -65,7 +65,7 @@ Argument | Type | Required | D ##### m.route.set -Redirects to a matching route, or to the default route if no matching routes can be found. +Redirects to a matching route, or to the default route if no matching routes can be found. Triggers an asynchronous redraw off all mount points. `m.route.set(path, data, options)` @@ -104,27 +104,33 @@ Argument | Type | Required | Description This function can be used as the `oncreate` (and `onupdate`) hook in a `m("a")` vnode: ```JS -m("a[href=/]", {oncreate: m.route.link})`. +m("a[href=/]", {oncreate: m.route.link}) ``` -Using `m.route.link` as a `oncreate` hook causes the link to behave as a router link (i.e. it navigates to the route specified in `href`, instead of nagivating away from the current page to the URL specified in `href`. +Using `m.route.link` as a `oncreate` hook causes the link to behave as a router link (i.e. it navigates to the route specified in `href`, instead of navigating away from the current page to the URL specified in `href`. If the `href` attribute is not static, the `onupdate` hook must also be set: ```JS -m("a", {href: someVariable, oncreate: m.route.link, onupdate: m.route.link})` +m("a", {href: someVariable, oncreate: m.route.link, onupdate: m.route.link}) ``` -`m.route.link(vnode)` +`m.route.link` can also set the `options` passed to `m.route.set` when the link is clicked by calling the function in the lifecycle methods: -Argument | Type | Required | Description ------------------ | ----------- | -------- | --- -`vnode` | `Vnode` | Yes | This method is meant to be used as or in conjunction with an `` [vnode](vnodes.md)'s [`oncreate` and `onupdate` hooks](lifecycle-methods.md) -**returns** | | | Returns `undefined` +```JS +m("a[href=/]", {oncreate: m.route.link({replace: true})}) +``` + +`m.route.link(args)` + +Argument | Type | Required | Description +----------------- | ---------------| -------- | --- +`args` | `Vnode|Object` | Yes | This method is meant to be used as or in conjunction with an `` [vnode](vnodes.md)'s [`oncreate` and `onupdate` hooks](lifecycle-methods.md) +**returns** | `function` | | Returns the onclick handler function for the component ##### m.route.param -Retrieves a route parameter. A route parameter is a key-value pair. Route parameters may come from a few different places: +Retrieves a route parameter from the last fully resolved route. A route parameter is a key-value pair. Route parameters may come from a few different places: - route interpolations (e.g. if a route is `/users/:id`, and it resolves to `/users/1`, the route parameter has a key `id` and value `"1"`) - router querystrings (e.g. if the path is `/users?page=1`, the route parameter has a key `page` and value `"1"`) @@ -137,9 +143,11 @@ Argument | Type | Required | Description `key` | `String` | No | A route parameter name (e.g. `id` in route `/users/:id`, or `page` in path `/users/1?page=3`, or a key in `history.state`) **returns** | `String|Object` | | Returns a value for the specified key. If a key is not specified, it returns an object that contains all the interpolation keys + Note that in the `onmatch` function of a RouteResolver, the new route hasn't yet been fully resolved, and `m.route.params()` will return the parameters of the previous route, if any. `onmatch` receives the parameters of the new route as an argument. + #### RouteResolver -A RouterResolver is an object that contains an `onmatch` method and/or a `render` method. Both methods are optional, but at least one must be present. A RouteResolver is not a component, and therefore it does NOT have lifecycle methods. As a rule of thumb, RouteResolvers should be in the same file as the `m.route` call, whereas component definitions should be in their own modules. +A RouteResolver is an object that contains an `onmatch` method and/or a `render` method. Both methods are optional, but at least one must be present. A RouteResolver is not a component, and therefore it does NOT have lifecycle methods. As a rule of thumb, RouteResolvers should be in the same file as the `m.route` call, whereas component definitions should be in their own modules. `routeResolver = {onmatch, render}` @@ -315,10 +323,24 @@ It's also possible to have variadic routes, i.e. a route with an argument that c ```javascript m.route(document.body, "/edit/pictures/image.jpg", { - "/files/:file...": Edit, + "/edit/:file...": Edit, }) ``` +#### Handling 404s + +For isomorphic / universal javascript app, an url param and a variadic route combined is very useful to display custom 404 error page. + +In a case of 404 Not Found error, the server send back the custom page to client. When Mithril is loaded, it will redirect client to the default route because it can't know that route. + +```javascript +m.route(document.body, "/", { + "/": homeComponent, + // [...] + "/:404...": errorPageComponent +}); + ``` + #### History state It's possible to take full advantage of the underlying `history.pushState` API to improve user's navigation experience. For example, an application could "remember" the state of a large form when the user leaves a page by navigating away, such that if the user pressed the back button in the browser, they'd have the form filled rather than a blank form. @@ -453,7 +475,7 @@ m.route(document.body, "/", { }) ``` -Note that in this case, if the Layout component the `oninit` and `oncreate` lifecycle methods would only fire on the Layout component on the first route change (assuming all routes use the same layout). +Note that in this case, if the Layout component has `oninit` and `oncreate` lifecycle methods, they would only fire on the first route change (assuming all routes use the same layout). To clarify the difference between the two examples, example 1 is equivalent to this code: @@ -492,7 +514,7 @@ In example 2, since `Layout` is the top-level component in both routes, the DOM #### Authentication -The RouterResolver's `onmatch` hook can be used to run logic before the top level component in a route is initializated. The example below shows how to implement a login wall that prevents users from seeing the `/secret` page unless they login. +The RouteResolver's `onmatch` hook can be used to run logic before the top level component in a route is initialized. The example below shows how to implement a login wall that prevents users from seeing the `/secret` page unless they login. ```javascript var isLoggedIn = false @@ -552,7 +574,7 @@ var Login = { return m("form", [ m("input[type=text]", {oninput: m.withAttr("value", Auth.setUsername), value: Auth.username}), m("input[type=password]", {oninput: m.withAttr("value", Auth.setPassword), value: Auth.password}), - m("button[type=button]", {onclick: Auth.login, "Login") + m("button[type=button]", {onclick: Auth.login}, "Login") ]) } } @@ -572,7 +594,7 @@ m.route(document.body, "/secret", { #### Preloading data -Typically, a component can load data upon initialization. Loading data this way renders the component twice (once upon routing, and once after the request completes). +Typically, a component can load data upon initialization. Loading data this way renders the component twice. The first render pass occurs upon routing, and the second fires after the request completes. Take care to note that `loadUsers()` returns a Promise, but any Promise returned by `oninit` is currently ignored. The second render pass comes from the [`background` option for `m.request`](request.md). ```javascript var state = { diff --git a/docs/simple-application.md b/docs/simple-application.md index b226aa2c..9ca3a698 100644 --- a/docs/simple-application.md +++ b/docs/simple-application.md @@ -2,6 +2,8 @@ Let's develop a simple application that covers some of the major aspects of Single Page Applications +An interactive running example can be seen here [flems: Simple Application](https://flems.io/#0=N4IgzgpgNhDGAuEAmIBcICGAHLA6AVmCADQgBmAljEagNqgB2GAthGiAKqQBOAsgPZJoBIqVj8GiSewBuGbgAIuERQF4FwADoMFuhVAph4qBbQC6xbXv38MSADKHjCsgFcGCChIAUASg1W1nrcEPCu3DrMuCEAjq4QRt5aOkGprPAAFoImmiAA4gCiACq5limp1uFQOSAZ8PBYYKgA9M0hzAC0IUYd2BS4GSr8ANau2HjizM19za48YKWBFXoA7hSZAMIhQpIUGFBNCvDc8WXLAL6+S0G4mRAM3m4e8F4P3a5Q8P7Jy9bK3LgDEYFOp3p9cEgMPAMNdrJdrucytdYOEQpITMBEdcoLYkCYnp4fBQkN9YcFQuFItEIHEEvAkmS0qEsniFLlCiUSIyglUanUGk1Wu0unTelh+oNuCMxjhcJNpuLZvNmrkFABqBTEs6-XRrTbbe4vfaHY6nbnw8o3O4PAkvHxgr4BS3Lf5y1GGkEKB3mq6WrEMa5gDAyCD49yEh6k53ksIRBRRWLxRI-HXx5nZNkgAAKHE52p1vMz-MaLTaEE63XgYolQ1G4zl-CmMzmKjAKpA6qUPDd3DR8FwWu51kh0JMrpRvcN+d+eoyW2Qhr2BxMpog07hvrh2nOIERjBYbHQ-0cRhEJBA4kkhtk8i7KhP8E9Kd0EgoDHWY+7OLsD+nMgoEArGGzyvH4TrLCEsaRN4uS4C23AdEC8ClHeAJIbgzDYI84Z2g88FRqmXoUnGzAwZgcE8IhTgdOs5YocAGQhGQNTNMg6ztp28EDkgxAKBIsAhFCobxtE-CuIggJvsMiIKFxlDcEYAByB6dqqqoalxUAYEpB6bhUlx6bo5zbruxD7qw7D-AAYvw3BRIQ56XlI8A3oo1m2cwT7XK+77OLaoEyAwggQN8rrfkg3iBcFuBQscYDcb4-rWP+gHARGYHPkEkGUvGZFkB59FDhUEhgK4ABGzAfi4OGgSF4GERUEC4FgIQhpIAAiEBkBgHz0oZDV-N2QYhn4RWpMZ0bjbxtBjbluRaWVwgLdAKG5FZFAKY+TCsLkvjrhUpG5G+WDiQODAnfAtDwAAnlgECqIgAAe8BmLQWBabAEBZFAQjcKo62bQo20QGYhWTb8PkXSYUSzgAgvU3BkXIUDxCh-k+Mj8Shd2E59rg8k6awnqYxAlz7TqJOfioPZ4wT8DKTt4MbuTQSHSAy1QICGCLVAq0gPY2lbQeu0s9YbPHadEuXe9GCfd9v2qALwLA6DJD1QNkPidDuBwwjSP7Kjavow8JPY9TuOGlzhMQMTBuk3ts3JXbVMAhbkhW-TwtM3oZOzWzZXifAEi4AH9QSFdt33aVFXrKrvG5AAysGEAi9yZj9RNO57iAwPsAL11if2DliBIzmuQo+eF15lopUB1UgRjQVCARFTZSRZGYW+XMF+JKEzd7uhs0wMgYfcrh947ehszCauZQNjFdSYADkzRIUvosQx4gmINrUriU1BgMMMk9GfHnDzLts3pxvg9kZAEYoVFQhyhkVBIGi-XWOnCImdnufoPWYuF5S7XnQAmQuTUWpdQoI9bwS8ADES9fTgP3t4JA-AUSsHdmVQQ10z6rycGDawuQCFGFyBibkaJfppVwhlWabdoKV3ErxUix4nC+E-j7BE04SFsXgM0VAxJyHqyyvcah9d0pPzqnPVuxFGEYB7vAFh3h3J2V4lImKCMwAcPNNw7cvhTLmUPCAOUYBRDAKvNIdAOCkB4LOhdYgIdA4SA0PldEQU7L7AUAARgAGxYEegoAAaioSETAADcmFuAAHM3wmAAEwAAYAkTW0N3KuwAomxIYKgbxyTAk9SDpEjAj0OhrCQJkXJiTqkBPCRNUeDBXAaCyXExJCg2kAGZ8l1O0Gk+CVFgTACQh0Iw10YCoCCgwCAxSYmtPaT47pWA7BIDfNE1AiSekMAoioAZVZaKeWAGVWWwxol7wYHieB3UrkYHCTg7g1DvEBIUGAfgBgkAKHgUgL54TxA4m4KgeBHSgXhJWWAGW11UBlRxLAYYMzsnrPmY8x64SllfNWagAAHE87xABWWpT0qxCHENwKErwJkSGmfU-pwz9moCyCGRQwACUdCJbZUlEhUDuF+ofSlvStkcw0KC8FkLoWwpaTktpbS8XIvqVLDQdyHlPJeW8j5XykC3Nsr9LodgKBzFQB02pODSlgAoAAL3RQqnZRqQWGGFVCjBYr5DwslQs2pqKVkMDWXk7F0rwnlMqXkxJABSTZTiw46EOcc05YlzkAogPGjV9yVC5KVa84kqrvmWoQiSlZeqDXIt+bZAFQKOk2rBVpCFb4eUdHtTCuFcy2neuRe69FTafG+uZaykluFyVTNDaHIOOT6UqHlVGs5FyIAYsnZOupu4LDsykjQegOcDzsEqpkbgVBzxVHYMWQUsxzonIbFMddjEqAAAFvG4Cvb45op7N2cyATdO67AHLnDMOcIAA) + First let's create an entry point for the application. Create a file `index.html`: ```markup @@ -136,6 +138,7 @@ By default, Mithril views are described using [hyperscript](hyperscript.md). Hyp Let's use Mithril hyperscript to create a list of items. Hyperscript is the most idiomatic way of writing Mithril views, but [JSX is another popular alternative that you could explore](jsx.md) once you're more comfortable with the basics: ```javascript +// src/views/UserList.js var m = require("mithril") var User = require("../models/User") @@ -392,8 +395,7 @@ var User = { load: function(id) { return m.request({ method: "GET", - url: "https://rem-rest-api.herokuapp.com/api/users/:id", - data: {id: id}, + url: "https://rem-rest-api.herokuapp.com/api/users/" + id, withCredentials: true, }) .then(function(result) { @@ -508,8 +510,7 @@ var User = { load: function(id) { return m.request({ method: "GET", - url: "https://rem-rest-api.herokuapp.com/api/users/:id", - data: {id: id}, + url: "https://rem-rest-api.herokuapp.com/api/users/" + id, withCredentials: true, }) .then(function(result) { @@ -520,7 +521,7 @@ var User = { save: function() { return m.request({ method: "PUT", - url: "https://rem-rest-api.herokuapp.com/api/users/:id", + url: "https://rem-rest-api.herokuapp.com/api/users/" + User.current.id, data: User.current, withCredentials: true, }) @@ -541,6 +542,7 @@ Currently, we're only able to navigate back to the user list via the browser bac Let's create a file `src/views/Layout.js`: ```javascript +// src/views/Layout.js var m = require("mithril") module.exports = { @@ -616,4 +618,4 @@ This concludes the tutorial. In this tutorial, we went through the process of creating a very simple application where we can list users from a server and edit them individually. As an extra exercise, try to implement user creation and deletion on your own. -If you want to see more examples of Mithril code, check the [examples](examples.md) page. If you have questions, feel free to drop by the [Mithril chat room](https://gitter.im/lhorie/mithril.js). +If you want to see more examples of Mithril code, check the [examples](examples.md) page. If you have questions, feel free to drop by the [Mithril chat room](https://gitter.im/MithrilJS/mithril.js). diff --git a/docs/stream.md b/docs/stream.md index dc720ef4..2ad04c20 100644 --- a/docs/stream.md +++ b/docs/stream.md @@ -9,10 +9,10 @@ - [Stream.scanMerge](#streamscanmerge) - [Stream.HALT](#streamhalt) - [Stream["fantasy-land/of"]](#streamfantasy-landof) - - [Instance members](#static-members) + - [Instance members](#instance-members) - [stream.map](#streammap) - [stream.end](#streamend) - - [stream["fantasy-land/of"]](#streamfantasy-landof) + - [stream["fantasy-land/of"]](#streamfantasy-landof-1) - [stream["fantasy-land/map"]](#streamfantasy-landmap) - [stream["fantasy-land/ap"]](#streamfantasy-landap) - [Basic usage](#basic-usage) @@ -120,11 +120,13 @@ Argument | Type | Required | Description Creates a new stream with the results of calling the function on every value in the stream with an accumulator and the incoming value. +Note that you can prevent dependent streams from being updated by returning the special value `stream.HALT` inside the accumulator function. + `stream = Stream.scan(fn, accumulator, stream)` Argument | Type | Required | Description ------------- | -------------------------------- | -------- | --- -`fn` | `(accumulator, value) -> result` | Yes | A function that takes an accumulator and value parameter and returns a new accumulator value +`fn` | `(accumulator, value) -> result \| HALT` | Yes | A function that takes an accumulator and value parameter and returns a new accumulator value `accumulator` | `any` | Yes | The starting value for the accumulator `stream` | `Stream` | Yes | Stream containing the values **returns** | `Stream` | | Returns a new stream containing the result @@ -159,7 +161,7 @@ A special value that can be returned to stream callbacks to halt execution of do This method is functionally identical to `stream`. It exists to conform to [Fantasy Land's Applicative specification](https://github.com/fantasyland/fantasy-land). For more information, see the [What is Fantasy Land](#what-is-fantasy-land) section. -`stream = stream["fantasy-land/of"](value)` +`stream = Stream["fantasy-land/of"](value)` Argument | Type | Required | Description ----------- | -------------------- | -------- | --- @@ -212,7 +214,7 @@ Creates a dependent stream whose value is set to the result of the callback func This method exists to conform to [Fantasy Land's Applicative specification](https://github.com/fantasyland/fantasy-land). For more information, see the [What is Fantasy Land](#what-is-fantasy-land) section. -`dependentStream = stream()["fantasy-land/of"](callback)` +`dependentStream = stream()["fantasy-land/map"](callback)` Argument | Type | Required | Description ------------ | -------------------- | -------- | --- @@ -384,7 +386,7 @@ var added = stream.combine(function(a, b) { console.log(added()) // logs 12 ``` -A stream can depend on any number of streams and it's guaranteed to update atomically. For example, if a stream A has two dependent streams B and C, and a fourth stream D is dependent on both B and C, the stream D will only update once if the value of A changes. This guarantees that the callback for stream D is never called with unstable values such as when B has a new value but C has the old value. Atomicity also bring the performance benefits of not recomputing downstreams unnecessarily. +A stream can depend on any number of streams and it's guaranteed to update atomically. For example, if a stream A has two dependent streams B and C, and a fourth stream D is dependent on both B and C, the stream D will only update once if the value of A changes. This guarantees that the callback for stream D is never called with unstable values such as when B has a new value but C has the old value. Atomicity also brings the performance benefits of not recomputing downstreams unnecessarily. You can prevent dependent streams from being updated by returning the special value `stream.HALT` diff --git a/docs/style.css b/docs/style.css index cb2f1e10..3da4e572 100644 --- a/docs/style.css +++ b/docs/style.css @@ -1,4 +1,4 @@ -body {-webkit-text-size-adjust: 100%;} +body {background:white;-webkit-text-size-adjust: 100%;} body,table,h5 {font:normal 16px 'Open Sans';} header,main {margin:auto;max-width:1000px;} header section {position:absolute;width:250px;} @@ -37,7 +37,7 @@ h2 a:visited,h3 a:visited,h4 a:visited,h5 a:visited {color:#000;text-decoration: h2::before,h3::before,h4::before,h5::before {content:"#";position:absolute;left:-20px;visibility:hidden;} h2:hover::before,h3:hover::before,h4:hover::before,h5:hover::before {visibility:visible;} #signature + p code {padding:3px 10px;} -h1 + ul {margin:40px 0 0 -270px;padding:0;position:absolute;width:250px;} +h1 + ul {margin:40px 0 0 -270px;padding:0;position:absolute;width:250px;z-index:1;} h1 + ul + hr {display:none;} h1 + ul li {list-style:none;margin:0;padding:0;} h1 + ul li:last-child {border-bottom:0;} @@ -57,7 +57,7 @@ h1 + ul strong + ul {border-left:3px solid #1e5799;} .hamburger:hover {text-decoration:none;} main section {margin:0;} header section {margin:0 0 20px;position:static;width:auto;} - h1 + ul {background:#eee;border:1px solid #ccc;box-sizing:border-box;display:none;height:100%;margin:0;overflow:auto;padding:20px;position:fixed;right:0;top:0;width:100%;} + h1 + ul {background:#eee;border:1px solid #ccc;box-sizing:border-box;display:none;height:100%;margin:0;overflow:auto;padding:20px;position:fixed;right:0;top:0;width:100%;z-index:1} h1 + ul + hr {display:block;} .navigating h1 + ul {display:block;} .navigating {overflow:hidden;} diff --git a/docs/support.md b/docs/support.md new file mode 100644 index 00000000..b157a5a4 --- /dev/null +++ b/docs/support.md @@ -0,0 +1,3 @@ +# Getting Help + +Mithril has an active & welcoming community on [Gitter](https://gitter.im/mithriljs/mithril.js), or feel free to ask questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/mithril.js) using the `mithril.js` tag. diff --git a/docs/testing.md b/docs/testing.md index 1ac9a7d2..ead11517 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,6 +1,6 @@ # Testing -Mithril comes with a testing framework called [ospec](https://github.com/lhorie/mithril.js/tree/rewrite/ospec). What makes it different from most test frameworks is that it avoids all configurability for the sake of avoiding [yak shaving](http://catb.org/jargon/html/Y/yak-shaving.html) and [analysis paralysis](https://en.wikipedia.org/wiki/Analysis_paralysis). +Mithril comes with a testing framework called [ospec](https://github.com/MithrilJS/mithril.js/tree/master/ospec). What makes it different from most test frameworks is that it avoids all configurability for the sake of avoiding [yak shaving](http://catb.org/jargon/html/Y/yak-shaving.html) and [analysis paralysis](https://en.wikipedia.org/wiki/Analysis_paralysis). The easist way to setup the test runner is to create an NPM script for it. Open your project's `package.json` file and edit the `test` line under the `scripts` section: @@ -58,7 +58,7 @@ Generally speaking, there are two ways to write tests: upfront and after the fac Writing tests upfront requires specifications to be frozen. Upfront tests are a great way of codifying the rules that a yet-to-be-implemented API must obey. However, writing tests upfront may not be a suitable strategy if you don't have a reasonable idea of what your project will look like, if the scope of the API is not well known or if it's likely to change (e.g. based on previous history at the company). -Writing tests after the fact is a way to document the behavior of a system and avoid regressions. They are useful to ensure that obscure corner cases are not inadvertedly broken and that previously fixed bugs do not get re-introduced by unrelated changes. +Writing tests after the fact is a way to document the behavior of a system and avoid regressions. They are useful to ensure that obscure corner cases are not inadvertently broken and that previously fixed bugs do not get re-introduced by unrelated changes. --- @@ -74,7 +74,9 @@ var m = require("mithril") module.exports = { view: function() { - return m("div", "Hello world") + return m("div", + m("p", "Hello World") + ) } } ``` @@ -90,7 +92,7 @@ o.spec("MyComponent", function() { o(vnode.tag).equals("div") o(vnode.children.length).equals(1) - o(vnode.children[0].tag).equals("#") + o(vnode.children[0].tag).equals("p") o(vnode.children[0].children).equals("Hello world") }) }) diff --git a/docs/trust.md b/docs/trust.md index d960e743..34e6ace8 100644 --- a/docs/trust.md +++ b/docs/trust.md @@ -11,7 +11,7 @@ ### Description -Turns an HTML string into unescaped HTML. **Do not use `m.trust` on unsanitized user input.** +Turns an HTML or SVG string into unescaped HTML or SVG. **Do not use `m.trust` on unsanitized user input.** Always try to use an [alternative method](#avoid-trusting-html) first, before considering using `m.trust`. @@ -23,7 +23,7 @@ Always try to use an [alternative method](#avoid-trusting-html) first, before co Argument | Type | Required | Description ----------- | -------------------- | -------- | --- -`html` | `String` | Yes | A string containing HTML text +`html` | `String` | Yes | A string containing HTML or SVG text **returns** | `Vnode` | | A trusted HTML [vnode](vnodes.md) that represents the input string [How to read signatures](signatures.md) diff --git a/docs/vnodes.md b/docs/vnodes.md index cff0ebe4..339b6175 100644 --- a/docs/vnodes.md +++ b/docs/vnodes.md @@ -21,11 +21,11 @@ It may seem wasteful to recreate vnodes so frequently, but as it turns out, mode For that reason, Mithril uses a sophisticated and highly optimized virtual DOM diffing algorithm to minimize the amount of DOM updates. Mithril *also* generates carefully crafted vnode data structures that are compiled by Javascript engines for near-native data structure access performance. In addition, Mithril aggressively optimizes the function that creates vnodes as well. -The reason Mithril goes to such great lengths to support a rendering model that recreates the entire virtual DOM tree on every render is to provide [retained mode rendering](https://en.wikipedia.org/wiki/Retained_mode), a style of rendering that makes it drastically easier to manage UI complexity. +The reason Mithril goes to such great lengths to support a rendering model that recreates the entire virtual DOM tree on every render is to provide a declarative [immediate mode](https://en.wikipedia.org/wiki/Immediate_mode_%28computer_graphics%29) API, a style of rendering that makes it drastically easier to manage UI complexity. -To illustrate why retained mode is so important, consider the DOM API and HTML. The DOM API is an [immediate mode](https://en.wikipedia.org/wiki/Immediate_mode_(computer_graphics)) rendering system and requires writing out exact instructions to assemble a DOM tree procedurally. The imperative nature of the DOM API means you have many opportunities to micro-optimize your code, but it also means that you have more chances of introducing bugs and more chances to make code harder to understand. +To illustrate why immediate mode is so important, consider the DOM API and HTML. The DOM API is an imperative [retained mode](https://en.wikipedia.org/wiki/Retained_mode) API and requires 1. writing out exact instructions to assemble a DOM tree procedurally, and 2. writing out other instructions to update that tree. The imperative nature of the DOM API means you have many opportunities to micro-optimize your code, but it also means that you have more chances of introducing bugs and more chances to make code harder to understand. -In contrast, HTML is a retained mode rendering system. With HTML, you can write a DOM tree in a far more natural and readable way, without worrying about forgetting to append a child to a parent, running into stack overflows when rendering extremely deep trees, etc. +In contrast, HTML is closer to an immediate mode rendering system. With HTML, you can write a DOM tree in a far more natural and readable way, without worrying about forgetting to append a child to a parent, running into stack overflows when rendering extremely deep trees, etc. Virtual DOM goes one step further than HTML by allowing you to write *dynamic* DOM trees without having to manually write multiple sets of DOM API calls to efficiently synchronize the UI to arbitrary data changes. @@ -74,7 +74,6 @@ Property | Type | Description `dom` | `Element?` | Points to the element that corresponds to the vnode. This property is `undefined` in the `oninit` lifecycle method. In fragments and trusted HTML vnodes, `dom` points to the first element in the range. `domSize` | `Number?` | This is only set in fragment and trusted HTML vnodes, and it's `undefined` in all other vnode types. It defines the number of DOM elements that the vnode represents (starting from the element referenced by the `dom` property). `state` | `Object?` | An object that is persisted between redraws. It is provided by the core engine when needed. In POJO component vnodes, the `state` inherits prototypically from the component object/class. In class component vnodes it is an instance of the class. In closure components it is the object returned by the closure. -`_state` | `Object?` | For components, a reference to the original `vnode.state` object, used to lookup the `view` and hooks. This property is only used internally by Mithril, do not use or modify it. `events` | `Object?` | An object that is persisted between redraws and that stores event handlers so that they can be removed using the DOM API. The `events` property is `undefined` if there are no event handlers defined. This property is only used internally by Mithril, do not use or modify it. `instance` | `Object?` | For components, a storage location for the value returned by the `view`. This property is only used internally by Mithril, do not use or modify it. `skip` | `Boolean` | This property is only used internally by Mithril when diffing keyed lists, do not use or modify it. @@ -89,7 +88,7 @@ The `tag` property of a vnode determines its type. There are five vnode types: Vnode type | Example | Description ------------ | ------------------------------ | --- Element | `{tag: "div"}` | Represents a DOM element. -Fragment | `{tag: "[", children: []}` | Represents a list of DOM elements whose parent DOM element may also contain other elements that are not in the fragment. When using the [`m()`](hyperscript.md) helper function, fragment vnodes can only be created by nesting arrays into the `children` parameter of `m()`. `m("[")` does not create a valid vnode. +Fragment | `{tag: "[", children: []}` | Represents a list of DOM elements whose parent DOM element may also contain other elements that are not in the fragment. When using the [`m()`](hyperscript.md) helper function, fragment vnodes can only be created by nesting arrays into the `children` parameter of `m()`. `m("[")` does not create a valid vnode. Text | `{tag: "#", children: ""}` | Represents a DOM text node. Trusted HTML | `{tag: "<", children: "
"}` | Represents a list of DOM elements from an HTML string. Component | `{tag: ExampleComponent}` | If `tag` is a Javascript object with a `view` method, the vnode represents the DOM generated by rendering the component. diff --git a/examples/todomvc/todomvc.js b/examples/todomvc/todomvc.js index 83d2a822..5d842258 100644 --- a/examples/todomvc/todomvc.js +++ b/examples/todomvc/todomvc.js @@ -62,7 +62,7 @@ var state = { //view var Todos = { add: function(e) { - if (e.keyCode === 13) { + if (e.keyCode === 13 && e.target.value) { state.dispatch("createTodo", [e.target.value]) e.target.value = "" } diff --git a/index.js b/index.js index 6e69cea4..123ba4ca 100644 --- a/index.js +++ b/index.js @@ -17,5 +17,6 @@ m.parseQueryString = require("./querystring/parse") m.buildQueryString = require("./querystring/build") m.version = "bleeding-edge" m.vnode = require("./render/vnode") +m.PromisePolyfill = require("./promise/polyfill") module.exports = m diff --git a/mithril.js b/mithril.js index 1b1a7b0a..11b40899 100644 --- a/mithril.js +++ b/mithril.js @@ -1,7 +1,7 @@ ;(function() { "use strict" function Vnode(tag, key, attrs0, children, text, dom) { - return {tag: tag, key: key, attrs: attrs0, children: children, text: text, dom: dom, domSize: undefined, state: undefined, _state: undefined, events: undefined, instance: undefined, skip: false} + return {tag: tag, key: key, attrs: attrs0, children: children, text: text, dom: dom, domSize: undefined, state: undefined, events: undefined, instance: undefined, skip: false} } Vnode.normalize = function(node) { if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) @@ -17,6 +17,10 @@ Vnode.normalizeChildren = function normalizeChildren(children) { var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g var selectorCache = {} var hasOwn = {}.hasOwnProperty +function isEmpty(object) { + for (var key in object) if (hasOwn.call(object, key)) return false + return true +} function compileSelector(selector) { var match, tag = "div", classes = [], attrs = {} while (match = selectorParser.exec(selector)) { @@ -28,7 +32,7 @@ function compileSelector(selector) { var attrValue = match[6] if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, "$1").replace(/\\\\/g, "\\") if (match[4] === "class") classes.push(attrValue) - else attrs[match[4]] = attrValue || true + else attrs[match[4]] = attrValue === "" ? attrValue : attrValue || true } } if (classes.length > 0) attrs.className = classes.join(" ") @@ -37,6 +41,15 @@ function compileSelector(selector) { function execSelector(state, attrs, children) { var hasAttrs = false, childList, text var className = attrs.className || attrs.class + if (!isEmpty(state.attrs) && !isEmpty(attrs)) { + var newAttrs = {} + for(var key in attrs) { + if (hasOwn.call(attrs, key)) { + newAttrs[key] = attrs[key] + } + } + attrs = newAttrs + } for (var key in state.attrs) { if (hasOwn.call(state.attrs, key)) { attrs[key] = state.attrs[key] @@ -161,6 +174,20 @@ PromisePolyfill.prototype.then = function(onFulfilled, onRejection) { PromisePolyfill.prototype.catch = function(onRejection) { return this.then(null, onRejection) } +PromisePolyfill.prototype.finally = function(callback) { + return this.then( + function(value) { + return PromisePolyfill.resolve(callback()).then(function() { + return value + }) + }, + function(reason) { + return PromisePolyfill.resolve(callback()).then(function() { + return PromisePolyfill.reject(reason); + }) + } + ) +} PromisePolyfill.resolve = function(value) { if (value instanceof PromisePolyfill) return value return new PromisePolyfill(function(resolve) {resolve(value)}) @@ -195,10 +222,18 @@ PromisePolyfill.race = function(list) { }) } if (typeof window !== "undefined") { - if (typeof window.Promise === "undefined") window.Promise = PromisePolyfill + if (typeof window.Promise === "undefined") { + window.Promise = PromisePolyfill + } else if (!window.Promise.prototype.finally) { + window.Promise.prototype.finally = PromisePolyfill.prototype.finally + } var PromisePolyfill = window.Promise } else if (typeof global !== "undefined") { - if (typeof global.Promise === "undefined") global.Promise = PromisePolyfill + if (typeof global.Promise === "undefined") { + global.Promise = PromisePolyfill + } else if (!global.Promise.prototype.finally) { + global.Promise.prototype.finally = PromisePolyfill.prototype.finally + } var PromisePolyfill = global.Promise } else { } @@ -224,7 +259,7 @@ var buildQueryString = function(object) { } } var FILE_PROTOCOL_REGEX = new RegExp("^file://", "i") -var _8 = function($window, Promise) { +var _9 = function($window, Promise) { var callbackCount = 0 var oncompletion function setCompletionCallback(callback) {oncompletion = callback} @@ -274,13 +309,14 @@ var _8 = function($window, Promise) { _abort.call(xhr) } xhr.open(args.method, args.url, typeof args.async === "boolean" ? args.async : true, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined) - if (args.serialize === JSON.stringify && useBody) { + if (args.serialize === JSON.stringify && useBody && !(args.headers && args.headers.hasOwnProperty("Content-Type"))) { xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8") } - if (args.deserialize === deserialize) { + if (args.deserialize === deserialize && !(args.headers && args.headers.hasOwnProperty("Accept"))) { xhr.setRequestHeader("Accept", "application/json, text/*") } if (args.withCredentials) xhr.withCredentials = args.withCredentials + if (args.timeout) xhr.timeout = args.timeout for (var key in args.headers) if ({}.hasOwnProperty.call(args.headers, key)) { xhr.setRequestHeader(key, args.headers[key]) } @@ -291,12 +327,13 @@ var _8 = function($window, Promise) { if (xhr.readyState === 4) { try { var response = (args.extract !== extract) ? args.extract(xhr, args) : args.deserialize(args.extract(xhr, args)) - if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) { + if (args.extract !== extract || (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) { resolve(cast(args.type, response)) } else { var error = new Error(xhr.responseText) - for (var key in response) error[key] = response[key] + error.code = xhr.status + error.response = response reject(error) } } @@ -371,12 +408,35 @@ var _8 = function($window, Promise) { } return {request: request, jsonp: jsonp, setCompletionCallback: setCompletionCallback} } -var requestService = _8(window, PromisePolyfill) +var requestService = _9(window, PromisePolyfill) var coreRenderer = function($window) { var $doc = $window.document var $emptyFragment = $doc.createDocumentFragment() + var nameSpace = { + svg: "http://www.w3.org/2000/svg", + math: "http://www.w3.org/1998/Math/MathML" + } var onevent function setEventCallback(callback) {return onevent = callback} + function getNameSpace(vnode) { + return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag] + } + //sanity check to discourage people from doing `vnode.state = ...` + function checkState(vnode, original) { + if (vnode.state !== original) throw new Error("`vnode.state` must not be modified") + } + //Note: the hook is passed as the `this` argument to allow proxying the + //arguments without requiring a full array allocation to do so. It also + //takes advantage of the fact the current `vnode` is the first argument in + //all lifecycle methods. + function callHook(vnode) { + var original = vnode.state + try { + return this.apply(original, arguments) + } finally { + checkState(vnode, original) + } + } //create function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) { for (var i = start; i < end; i++) { @@ -392,24 +452,33 @@ var coreRenderer = function($window) { vnode.state = {} if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) switch (tag) { - case "#": return createText(parent, vnode, nextSibling) - case "<": return createHTML(parent, vnode, nextSibling) - case "[": return createFragment(parent, vnode, hooks, ns, nextSibling) - default: return createElement(parent, vnode, hooks, ns, nextSibling) + case "#": createText(parent, vnode, nextSibling); break + case "<": createHTML(parent, vnode, ns, nextSibling); break + case "[": createFragment(parent, vnode, hooks, ns, nextSibling); break + default: createElement(parent, vnode, hooks, ns, nextSibling) } } - else return createComponent(parent, vnode, hooks, ns, nextSibling) + else createComponent(parent, vnode, hooks, ns, nextSibling) } function createText(parent, vnode, nextSibling) { vnode.dom = $doc.createTextNode(vnode.children) insertNode(parent, vnode.dom, nextSibling) - return vnode.dom } - function createHTML(parent, vnode, nextSibling) { + var possibleParents = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"} + function createHTML(parent, vnode, ns, nextSibling) { var match1 = vnode.children.match(/^\s*?<(\w+)/im) || [] - var parent1 = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"}[match1[1]] || "div" - var temp = $doc.createElement(parent1) - temp.innerHTML = vnode.children + // not using the proper parent makes the child element(s) vanish. + // var div = document.createElement("div") + // div.innerHTML = "ij" + // console.log(div.innerHTML) + // --> "ij", no in sight. + var temp = $doc.createElement(possibleParents[match1[1]] || "div") + if (ns === "http://www.w3.org/2000/svg") { + temp.innerHTML = "" + vnode.children + "" + temp = temp.firstChild + } else { + temp.innerHTML = vnode.children + } vnode.dom = temp.firstChild vnode.domSize = temp.childNodes.length var fragment = $doc.createDocumentFragment() @@ -418,7 +487,6 @@ var coreRenderer = function($window) { fragment.appendChild(child) } insertNode(parent, fragment, nextSibling) - return fragment } function createFragment(parent, vnode, hooks, ns, nextSibling) { var fragment = $doc.createDocumentFragment() @@ -429,16 +497,12 @@ var coreRenderer = function($window) { vnode.dom = fragment.firstChild vnode.domSize = fragment.childNodes.length insertNode(parent, fragment, nextSibling) - return fragment } function createElement(parent, vnode, hooks, ns, nextSibling) { var tag = vnode.tag - switch (vnode.tag) { - case "svg": ns = "http://www.w3.org/2000/svg"; break - case "math": ns = "http://www.w3.org/1998/Math/MathML"; break - } var attrs2 = vnode.attrs var is = attrs2 && attrs2.is + ns = getNameSpace(vnode) || ns var element = ns ? is ? $doc.createElementNS(ns, tag, {is: is}) : $doc.createElementNS(ns, tag) : is ? $doc.createElement(tag, {is: is}) : $doc.createElement(tag) @@ -461,7 +525,6 @@ var coreRenderer = function($window) { setLateAttrs(vnode) } } - return element } function initComponent(vnode, hooks) { var sentinel @@ -477,145 +540,242 @@ var coreRenderer = function($window) { sentinel.$$reentrantLock$$ = true vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) } - vnode._state = vnode.state if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) - initLifecycle(vnode._state, vnode, hooks) - vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode)) + initLifecycle(vnode.state, vnode, hooks) + vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") sentinel.$$reentrantLock$$ = null } function createComponent(parent, vnode, hooks, ns, nextSibling) { initComponent(vnode, hooks) if (vnode.instance != null) { - var element = createNode(parent, vnode.instance, hooks, ns, nextSibling) + createNode(parent, vnode.instance, hooks, ns, nextSibling) vnode.dom = vnode.instance.dom vnode.domSize = vnode.dom != null ? vnode.instance.domSize : 0 - insertNode(parent, element, nextSibling) - return element } else { vnode.domSize = 0 - return $emptyFragment } } //update - function updateNodes(parent, old, vnodes, recycling, hooks, nextSibling, ns) { + /** + * @param {Element|Fragment} parent - the parent element + * @param {Vnode[] | null} old - the list of vnodes of the last0 `render()` call for + * this part of the tree + * @param {Vnode[] | null} vnodes - as above, but for the current `render()` call. + * @param {Function[]} hooks - an accumulator of post-render hooks (oncreate/onupdate) + * @param {Element | null} nextSibling - the next0 DOM node if we're dealing with a + * fragment that is not the last0 item in its + * parent + * @param {'svg' | 'math' | String | null} ns) - the current XML namespace, if any + * @returns void + */ + // This function diffs and patches lists of vnodes, both keyed and unkeyed. + // + // We will: + // + // 1. describe its general structure + // 2. focus on the diff algorithm optimizations + // 3. discuss DOM node operations. + // ## Overview: + // + // The updateNodes() function: + // - deals with trivial cases + // - determines whether the lists are keyed or unkeyed based on the first non-null node + // of each list. + // - diffs them and patches the DOM if needed (that's the brunt of the code) + // - manages the leftovers: after diffing, are there: + // - old nodes left to remove? + // - new nodes to insert? + // deal with them! + // + // The lists are only iterated over once, with an exception for the nodes in `old` that + // are visited in the fourth part of the diff and in the `removeNodes` loop. + // ## Diffing + // + // If one list is keyed and the other is unkeyed, the old is removed, and the new one is + // inserted (since the keys are guaranteed to differ). + // + // Then comes the unkeyed diff algo, and at last0, the keyed diff algorithm that is split + // in four parts (simplifying a bit). + // + // The first part goes through both lists top-down as long as the nodes at each level have + // the same key2. + // + // The second part deals with lists reversals, and traverses one list top-down and the other + // bottom-up (as long as the keys match1). + // + // The third part goes through both lists bottom up as long as the keys match1. + // + // The first and third sections allow us to deal efficiently with situations where one or + // more contiguous nodes were either inserted into, removed from or re-ordered in an otherwise + // sorted list. They may reduce the number of nodes to be processed in the fourth section. + // + // The fourth section does keyed diff for the situations not covered by the other three. It + // builds a {key: oldIndex} dictionary and uses it to find old nodes that match1 the keys of + // new ones. + // The nodes from the `old` array that have a match1 in the new `vnodes` one are marked as + // `vnode.skip: true`. + // + // If there are still nodes in the new `vnodes` array that haven't been matched to old ones, + // they are created. + // The range of old nodes that wasn't covered by the first three sections is passed to + // `removeNodes()`. Those nodes are removed unless marked as `.skip: true`. + // + // It should be noted that the description of the four sections above is not perfect, because those + // parts are actually implemented as only two loops, one for the first two parts, and one for + // the other two. I'm1 not sure it wins us anything except maybe a few bytes of file size. + // ## Finding the next0 sibling. + // + // `updateNode()` and `createNode()` expect a nextSibling parameter to perform DOM operations. + // When the list is being traversed top-down, at any index0, the DOM nodes up to the previous + // vnode reflect the content of the new list, whereas the rest of the DOM nodes reflect the old + // list. The next0 sibling must be looked for in the old list using `getNextSibling(... oldStart + 1 ...)`. + // + // In the other scenarios (swaps, upwards traversal, map-based diff), + // the new vnodes list is traversed upwards. The DOM nodes at the bottom of the list reflect the + // bottom part of the new vnodes list, and we can use the `v.dom` value of the previous node + // as the next0 sibling (cached0 in the `nextSibling` variable). + // ## DOM node moves + // + // In most scenarios `updateNode()` and `createNode()` perform the DOM operations. However, + // this is not the case if the node moved (second and fourth part of the diff algo). We move + // the old DOM nodes before updateNode runs0 because it enables us to use the cached0 `nextSibling` + // variable rather than fetching it using `getNextSibling()`. + // + // The fourth part of the diff currently inserts nodes unconditionally, leading to issues + // like #1791 and #1999. We need to be smarter about those situations where adjascent old + // nodes remain together in the new list in a way that isn't covered by parts one and + // three of the diff algo. + function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { if (old === vnodes || old == null && vnodes == null) return - else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, undefined) - else if (vnodes == null) removeNodes(old, 0, old.length, vnodes) + else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) + else if (vnodes == null) removeNodes(old, 0, old.length) else { - if (old.length === vnodes.length) { - var isUnkeyed = false - for (var i = 0; i < vnodes.length; i++) { - if (vnodes[i] != null && old[i] != null) { - isUnkeyed = vnodes[i].key == null && old[i].key == null - break - } - } - if (isUnkeyed) { - for (var i = 0; i < old.length; i++) { - if (old[i] === vnodes[i]) continue - else if (old[i] == null && vnodes[i] != null) createNode(parent, vnodes[i], hooks, ns, getNextSibling(old, i + 1, nextSibling)) - else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes) - else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), recycling, ns) - } - return + // default to keyed because, when either list is full of null nodes, it has fewer branches + var start = 0, oldStart = 0, isOldKeyed = true, isKeyed = true + for (; oldStart < old.length; oldStart++) { + if (old[oldStart] != null) { + isOldKeyed = old[oldStart].key != null + break } } - recycling = recycling || isRecyclable(old, vnodes) - if (recycling) { - var pool = old.pool - old = old.concat(old.pool) + for (; start < vnodes.length; start++) { + if (vnodes[start] != null) { + isKeyed = vnodes[start].key != null + break + } } - var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map + if (isOldKeyed !== isKeyed) { + removeNodes(old, oldStart, old.length) + createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + return + } + if (!isKeyed) { + // Don't index0 past the end of either list (causes deopts). + var commonLength = old.length < vnodes.length ? old.length : vnodes.length + // Rewind if necessary to the first non-null index0 on either side. + // We could alternatively either explicitly create or remove nodes when `start !== oldStart` + // but that would be optimizing for sparse lists which are more rare than dense ones. + start = start < oldStart ? start : oldStart + for (; start < commonLength; start++) { + o = old[start] + v = vnodes[start] + if (o === v || o == null && v == null) continue + else if (o == null) createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling)) + else if (v == null) removeNode(o) + else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns) + } + if (old.length > commonLength) removeNodes(old, start, old.length) + if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + return + } + // keyed diff + var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v while (oldEnd >= oldStart && end >= start) { - var o = old[oldStart], v = vnodes[start] - if (o === v && !recycling) oldStart++, start++ - else if (o == null) oldStart++ + // both top-down + o = old[oldStart] + v = vnodes[start] + if (o == null) oldStart++ else if (v == null) start++ else if (o.key === v.key) { - var shouldRecycle = (pool != null && oldStart >= old.length - pool.length) || ((pool == null) && recycling) oldStart++, start++ - updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), shouldRecycle, ns) - if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) - } - else { - var o = old[oldEnd] - if (o === v && !recycling) oldEnd--, start++ - else if (o == null) oldEnd-- - else if (v == null) start++ + if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns) + } else { + // reversal: old top-down, new bottom-up + v = vnodes[end] + if (o == null) oldStart++ + else if (v == null) end-- else if (o.key === v.key) { - var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling) - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) - if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling)) - oldEnd--, start++ + oldStart++ + if (start < end--) insertNode(parent, toFragment(o), nextSibling) + if (o !== v) updateNode(parent, o, v, hooks, nextSibling, ns) + if (v.dom != null) nextSibling = v.dom } else break } } while (oldEnd >= oldStart && end >= start) { - var o = old[oldEnd], v = vnodes[end] - if (o === v && !recycling) oldEnd--, end-- - else if (o == null) oldEnd-- + // both bottom-up + o = old[oldEnd] + v = vnodes[end] + if (o == null) oldEnd-- else if (v == null) end-- else if (o.key === v.key) { - var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling) - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) - if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) - if (o.dom != null) nextSibling = o.dom + if (o !== v) updateNode(parent, o, v, hooks, nextSibling, ns) + if (v.dom != null) nextSibling = v.dom oldEnd--, end-- - } - else { - if (!map) map = getKeyMap(old, oldEnd) + } else { + // old map-based, new bottom-up + if (map == null) { + // the last0 node can be left out of the map because it will be caught by the + // bottom-up part of the diff loop. If we were to refactor this to use distinct + // loops, we'd have to pass `oldEnd + 1` (or change `start < end` to `<=` in getKeyMap). + map = getKeyMap(old, oldStart, oldEnd) + } if (v != null) { var oldIndex = map[v.key] - if (oldIndex != null) { - var movable = old[oldIndex] - var shouldRecycle = (pool != null && oldIndex >= old.length - pool.length) || ((pool == null) && recycling) - updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) - insertNode(parent, toFragment(movable), nextSibling) - old[oldIndex].skip = true - if (movable.dom != null) nextSibling = movable.dom - } - else { - var dom = createNode(parent, v, hooks, undefined, nextSibling) - nextSibling = dom + if (oldIndex == null) { + createNode(parent, v, hooks, ns, nextSibling) + if (v.dom != null) nextSibling = v.dom + } else { + o = old[oldIndex] + insertNode(parent, toFragment(o), nextSibling) + if (o !== v) updateNode(parent, o, v, hooks, nextSibling, ns) + o.skip = true + if (v.dom != null) nextSibling = v.dom } } end-- } if (end < start) break } + // deal with the leftovers. createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) - removeNodes(old, oldStart, oldEnd + 1, vnodes) + removeNodes(old, oldStart, oldEnd + 1) } } - function updateNode(parent, old, vnode, hooks, nextSibling, recycling, ns) { + function updateNode(parent, old, vnode, hooks, nextSibling, ns) { var oldTag = old.tag, tag = vnode.tag if (oldTag === tag) { vnode.state = old.state - vnode._state = old._state vnode.events = old.events - if (!recycling && shouldNotUpdate(vnode, old)) return + if (shouldNotUpdate(vnode, old)) return if (typeof oldTag === "string") { if (vnode.attrs != null) { - if (recycling) { - vnode.state = {} - initLifecycle(vnode.attrs, vnode, hooks) - } - else updateLifecycle(vnode.attrs, vnode, hooks) + updateLifecycle(vnode.attrs, vnode, hooks) } switch (oldTag) { case "#": updateText(old, vnode); break - case "<": updateHTML(parent, old, vnode, nextSibling); break - case "[": updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns); break - default: updateElement(old, vnode, recycling, hooks, ns) + case "<": updateHTML(parent, old, vnode, ns, nextSibling); break + case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns); break + default: updateElement(old, vnode, hooks, ns) } } - else updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) + else updateComponent(parent, old, vnode, hooks, nextSibling, ns) } else { - removeNode(old, null) + removeNode(old) createNode(parent, vnode, hooks, ns, nextSibling) } } @@ -625,15 +785,15 @@ var coreRenderer = function($window) { } vnode.dom = old.dom } - function updateHTML(parent, old, vnode, nextSibling) { + function updateHTML(parent, old, vnode, ns, nextSibling) { if (old.children !== vnode.children) { toFragment(old) - createHTML(parent, vnode, nextSibling) + createHTML(parent, vnode, ns, nextSibling) } else vnode.dom = old.dom, vnode.domSize = old.domSize } - function updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns) { - updateNodes(parent, old.children, vnode.children, recycling, hooks, nextSibling, ns) + function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { + updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns) var domSize = 0, children = vnode.children vnode.dom = null if (children != null) { @@ -647,12 +807,9 @@ var coreRenderer = function($window) { if (domSize !== 1) vnode.domSize = domSize } } - function updateElement(old, vnode, recycling, hooks, ns) { + function updateElement(old, vnode, hooks, ns) { var element = vnode.dom = old.dom - switch (vnode.tag) { - case "svg": ns = "http://www.w3.org/2000/svg"; break - case "math": ns = "http://www.w3.org/1998/Math/MathML"; break - } + ns = getNameSpace(vnode) || ns if (vnode.tag === "textarea") { if (vnode.attrs == null) vnode.attrs = {} if (vnode.text != null) { @@ -670,26 +827,22 @@ var coreRenderer = function($window) { else { if (old.text != null) old.children = [Vnode("#", undefined, undefined, old.text, undefined, old.dom.firstChild)] if (vnode.text != null) vnode.children = [Vnode("#", undefined, undefined, vnode.text, undefined, undefined)] - updateNodes(element, old.children, vnode.children, recycling, hooks, null, ns) + updateNodes(element, old.children, vnode.children, hooks, null, ns) } } - function updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) { - if (recycling) { - initComponent(vnode, hooks) - } else { - vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode)) - if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") - if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) - updateLifecycle(vnode._state, vnode, hooks) - } + function updateComponent(parent, old, vnode, hooks, nextSibling, ns) { + vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) + if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") + if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) + updateLifecycle(vnode.state, vnode, hooks) if (vnode.instance != null) { if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) - else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, recycling, ns) + else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns) vnode.dom = vnode.instance.dom vnode.domSize = vnode.instance.domSize } else if (old.instance != null) { - removeNode(old.instance, null) + removeNode(old.instance) vnode.dom = undefined vnode.domSize = 0 } @@ -698,24 +851,13 @@ var coreRenderer = function($window) { vnode.domSize = old.domSize } } - function isRecyclable(old, vnodes) { - if (old.pool != null && Math.abs(old.pool.length - vnodes.length) <= Math.abs(old.length - vnodes.length)) { - var oldChildrenLength = old[0] && old[0].children && old[0].children.length || 0 - var poolChildrenLength = old.pool[0] && old.pool[0].children && old.pool[0].children.length || 0 - var vnodesChildrenLength = vnodes[0] && vnodes[0].children && vnodes[0].children.length || 0 - if (Math.abs(poolChildrenLength - vnodesChildrenLength) <= Math.abs(oldChildrenLength - vnodesChildrenLength)) { - return true - } - } - return false - } - function getKeyMap(vnodes, end) { - var map = {}, i = 0 - for (var i = 0; i < end; i++) { - var vnode = vnodes[i] + function getKeyMap(vnodes, start, end) { + var map = {} + for (; start < end; start++) { + var vnode = vnodes[start] if (vnode != null) { var key2 = vnode.key - if (key2 != null) map[key2] = i + if (key2 != null) map[key2] = start } } return map @@ -740,7 +882,7 @@ var coreRenderer = function($window) { return nextSibling } function insertNode(parent, dom, nextSibling) { - if (nextSibling && nextSibling.parentNode) parent.insertBefore(dom, nextSibling) + if (nextSibling != null) parent.insertBefore(dom, nextSibling) else parent.appendChild(dom) } function setContentEditable(vnode) { @@ -752,26 +894,27 @@ var coreRenderer = function($window) { else if (vnode.text != null || children != null && children.length !== 0) throw new Error("Child node of a contenteditable must be trusted") } //remove - function removeNodes(vnodes, start, end, context) { + function removeNodes(vnodes, start, end) { for (var i = start; i < end; i++) { var vnode = vnodes[i] if (vnode != null) { if (vnode.skip) vnode.skip = false - else removeNode(vnode, context) + else removeNode(vnode) } } } - function removeNode(vnode, context) { + function removeNode(vnode) { var expected = 1, called = 0 + var original = vnode.state if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { - var result = vnode.attrs.onbeforeremove.call(vnode.state, vnode) + var result = callHook.call(vnode.attrs.onbeforeremove, vnode) if (result != null && typeof result.then === "function") { expected++ result.then(continuation, continuation) } } - if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeremove === "function") { - var result = vnode._state.onbeforeremove.call(vnode.state, vnode) + if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") { + var result = callHook.call(vnode.state.onbeforeremove, vnode) if (result != null && typeof result.then === "function") { expected++ result.then(continuation, continuation) @@ -780,6 +923,7 @@ var coreRenderer = function($window) { continuation() function continuation() { if (++called === expected) { + checkState(vnode, original) onremove(vnode) if (vnode.dom) { var count0 = vnode.domSize || 1 @@ -790,10 +934,6 @@ var coreRenderer = function($window) { } } removeNodeFromDOM(vnode.dom) - if (context != null && vnode.domSize == null && !hasIntegrationMethods(vnode.attrs) && typeof vnode.tag === "string") { //TODO test custom elements - if (!context.pool) context.pool = [vnode] - else context.pool.push(vnode) - } } } } @@ -803,10 +943,11 @@ var coreRenderer = function($window) { if (parent != null) parent.removeChild(node) } function onremove(vnode) { - if (vnode.attrs && typeof vnode.attrs.onremove === "function") vnode.attrs.onremove.call(vnode.state, vnode) - if (typeof vnode.tag !== "string" && typeof vnode._state.onremove === "function") vnode._state.onremove.call(vnode.state, vnode) - if (vnode.instance != null) onremove(vnode.instance) - else { + if (vnode.attrs && typeof vnode.attrs.onremove === "function") callHook.call(vnode.attrs.onremove, vnode) + if (typeof vnode.tag !== "string") { + if (typeof vnode.state.onremove === "function") callHook.call(vnode.state.onremove, vnode) + if (vnode.instance != null) onremove(vnode.instance) + } else { var children = vnode.children if (Array.isArray(children)) { for (var i = 0; i < children.length; i++) { @@ -823,22 +964,33 @@ var coreRenderer = function($window) { } } function setAttr(vnode, key2, old, value, ns) { - var element = vnode.dom - if (key2 === "key" || key2 === "is" || (old === value && !isFormAttribute(vnode, key2)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key2)) return - var nsLastIndex = key2.indexOf(":") - if (nsLastIndex > -1 && key2.substr(0, nsLastIndex) === "xlink") { - element.setAttributeNS("http://www.w3.org/1999/xlink", key2.slice(nsLastIndex + 1), value) + if (key2 === "key" || key2 === "is" || isLifecycleMethod(key2)) return + if (key2[0] === "o" && key2[1] === "n") return updateEvent(vnode, key2, value) + if (typeof value === "undefined" && key2 === "value" && old !== value) { + vnode.dom.value = "" + return } - else if (key2[0] === "o" && key2[1] === "n" && typeof value === "function") updateEvent(vnode, key2, value) + if ((old === value && !isFormAttribute(vnode, key2)) && typeof value !== "object" || value === undefined) return + var element = vnode.dom + if (key2.slice(0, 6) === "xlink:") element.setAttributeNS("http://www.w3.org/1999/xlink", key2, value) else if (key2 === "style") updateStyle(element, old, value) else if (key2 in element && !isAttribute(key2) && ns === undefined && !isCustomElement(vnode)) { - //setting input[value] to same value by typing on focused element moves cursor to end in Chrome - if (vnode.tag === "input" && key2 === "value" && vnode.dom.value == value && vnode.dom === $doc.activeElement) return - //setting select[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "select" && key2 === "value" && vnode.dom.value == value && vnode.dom === $doc.activeElement) return - //setting option[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "option" && key2 === "value" && vnode.dom.value == value) return - // If you assign an input type1 that is not supported by IE 11 with an assignment expression, an error0 will occur. + if (key2 === "value") { + var normalized0 = "" + value // eslint-disable-line no-implicit-coercion + //setting input[value] to same value by typing on focused element moves cursor to end in Chrome + if ((vnode.tag === "input" || vnode.tag === "textarea") && vnode.dom.value === normalized0 && vnode.dom === $doc.activeElement) return + //setting select[value] to same value while having select open blinks select dropdown in Chrome + if (vnode.tag === "select") { + if (value === null) { + if (vnode.dom.selectedIndex === -1 && vnode.dom === $doc.activeElement) return + } else { + if (old !== null && vnode.dom.value === normalized0 && vnode.dom === $doc.activeElement) return + } + } + //setting option[value] to same value while having select open blinks select dropdown in Chrome + if (vnode.tag === "option" && old != null && vnode.dom.value === normalized0) return + } + // If you assign an input type1 that is not supported by IE 11 with an assignment expression, an error1 will occur. if (vnode.tag === "input" && key2 === "type") { element.setAttribute(key2, value) return @@ -877,7 +1029,7 @@ var coreRenderer = function($window) { } } function isFormAttribute(vnode, attr) { - return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement + return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement || vnode.tag === "option" && vnode.dom.parentNode === $doc.activeElement } function isLifecycleMethod(attr) { return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate" @@ -888,11 +1040,20 @@ var coreRenderer = function($window) { function isCustomElement(vnode){ return vnode.attrs.is || vnode.tag.indexOf("-") > -1 } - function hasIntegrationMethods(source) { - return source != null && (source.oncreate || source.onupdate || source.onbeforeremove || source.onremove) - } //style function updateStyle(element, old, style) { + if (old != null && style != null && typeof old === "object" && typeof style === "object" && style !== old) { + // Both old & new are (different) objects. + // Update style properties that have changed + for (var key2 in style) { + if (style[key2] !== old[key2]) element.style[key2] = style[key2] + } + // Remove style properties that no longer exist + for (var key2 in old) { + if (!(key2 in style)) element.style[key2] = "" + } + return + } if (old === style) element.style.cssText = "", old = null if (style == null) element.style.cssText = "" else if (typeof style === "string") element.style.cssText = style @@ -901,45 +1062,58 @@ var coreRenderer = function($window) { for (var key2 in style) { element.style[key2] = style[key2] } - if (old != null && typeof old !== "string") { - for (var key2 in old) { - if (!(key2 in style)) element.style[key2] = "" - } - } } } + // Here's an explanation of how this works: + // 1. The event names are always (by design) prefixed by `on`. + // 2. The EventListener interface accepts either a function or an object + // with a `handleEvent` method. + // 3. The object does not inherit from `Object.prototype`, to avoid + // any potential interference with that (e.g. setters). + // 4. The event name is remapped to the handler0 before calling it. + // 5. In function-based event handlers, `ev.target === this`. We replicate + // that below. + function EventDict() {} + EventDict.prototype = Object.create(null) + EventDict.prototype.handleEvent = function (ev) { + var handler0 = this["on" + ev.type] + if (typeof handler0 === "function") handler0.call(ev.target, ev) + else if (typeof handler0.handleEvent === "function") handler0.handleEvent(ev) + if (typeof onevent === "function") onevent.call(ev.target, ev) + } //event function updateEvent(vnode, key2, value) { - var element = vnode.dom - var callback = typeof onevent !== "function" ? value : function(e) { - var result = value.call(element, e) - onevent.call(element, e) - return result - } - if (key2 in element) element[key2] = typeof value === "function" ? callback : null - else { - var eventName = key2.slice(2) - if (vnode.events === undefined) vnode.events = {} - if (vnode.events[key2] === callback) return - if (vnode.events[key2] != null) element.removeEventListener(eventName, vnode.events[key2], false) - if (typeof value === "function") { - vnode.events[key2] = callback - element.addEventListener(eventName, vnode.events[key2], false) + if (vnode.events != null) { + if (vnode.events[key2] === value) return + if (value != null && (typeof value === "function" || typeof value === "object")) { + if (vnode.events[key2] == null) vnode.dom.addEventListener(key2.slice(2), vnode.events, false) + vnode.events[key2] = value + } else { + if (vnode.events[key2] != null) vnode.dom.removeEventListener(key2.slice(2), vnode.events, false) + vnode.events[key2] = undefined } + } else if (value != null && (typeof value === "function" || typeof value === "object")) { + vnode.events = new EventDict() + vnode.dom.addEventListener(key2.slice(2), vnode.events, false) + vnode.events[key2] = value } } //lifecycle function initLifecycle(source, vnode, hooks) { - if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode) - if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode)) + if (typeof source.oninit === "function") callHook.call(source.oninit, vnode) + if (typeof source.oncreate === "function") hooks.push(callHook.bind(source.oncreate, vnode)) } function updateLifecycle(source, vnode, hooks) { - if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) + if (typeof source.onupdate === "function") hooks.push(callHook.bind(source.onupdate, vnode)) } function shouldNotUpdate(vnode, old) { var forceVnodeUpdate, forceComponentUpdate - if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") forceVnodeUpdate = vnode.attrs.onbeforeupdate.call(vnode.state, vnode, old) - if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeupdate === "function") forceComponentUpdate = vnode._state.onbeforeupdate.call(vnode.state, vnode, old) + if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") { + forceVnodeUpdate = callHook.call(vnode.attrs.onbeforeupdate, vnode, old) + } + if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") { + forceComponentUpdate = callHook.call(vnode.state.onbeforeupdate, vnode, old) + } if (!(forceVnodeUpdate === undefined && forceComponentUpdate === undefined) && !forceVnodeUpdate && !forceComponentUpdate) { vnode.dom = old.dom vnode.domSize = old.domSize @@ -952,60 +1126,63 @@ var coreRenderer = function($window) { if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.") var hooks = [] var active = $doc.activeElement - // First time0 rendering into a node clears it out + var namespace = dom.namespaceURI + // First time rendering0 into a node clears it out if (dom.vnodes == null) dom.textContent = "" if (!Array.isArray(vnodes)) vnodes = [vnodes] - updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), false, hooks, null, undefined) + updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace) dom.vnodes = vnodes + // document.activeElement can return null in IE https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement + if (active != null && $doc.activeElement !== active) active.focus() for (var i = 0; i < hooks.length; i++) hooks[i]() - if ($doc.activeElement !== active) active.focus() } return {render: render, setEventCallback: setEventCallback} } function throttle(callback) { //60fps translates to 16.6ms, round it down since setTimeout requires int - var time = 16 + var delay = 16 var last = 0, pending = null var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout return function() { - var now = Date.now() - if (last === 0 || now - last >= time) { - last = now - callback() - } - else if (pending === null) { + var elapsed = Date.now() - last + if (pending === null) { pending = timeout(function() { pending = null callback() last = Date.now() - }, time - (now - last)) + }, delay - elapsed) } } } -var _11 = function($window) { +var _12 = function($window, throttleMock) { var renderService = coreRenderer($window) renderService.setEventCallback(function(e) { - if (e.redraw !== false) redraw() + if (e.redraw === false) e.redraw = undefined + else redraw() }) var callbacks = [] + var rendering = false function subscribe(key1, callback) { unsubscribe(key1) - callbacks.push(key1, throttle(callback)) + callbacks.push(key1, callback) } function unsubscribe(key1) { var index = callbacks.indexOf(key1) if (index > -1) callbacks.splice(index, 2) } - function redraw() { - for (var i = 1; i < callbacks.length; i += 2) { - callbacks[i]() - } + function sync() { + if (rendering) throw new Error("Nested m.redraw.sync() call") + rendering = true + for (var i = 1; i < callbacks.length; i+=2) try {callbacks[i]()} catch (e) {if (typeof console !== "undefined") console.error(e)} + rendering = false } + var redraw = (throttleMock || throttle)(sync) + redraw.sync = sync return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render} } -var redrawService = _11(window) +var redrawService = _12(window) requestService.setCompletionCallback(redrawService.redraw) -var _16 = function(redrawService0) { +var _17 = function(redrawService0) { return function(root, component) { if (component === null) { redrawService0.render(root, []) @@ -1019,10 +1196,10 @@ var _16 = function(redrawService0) { redrawService0.render(root, Vnode(component)) } redrawService0.subscribe(root, run0) - redrawService0.redraw() + run0() } } -m.mount = _16(redrawService) +m.mount = _17(redrawService) var Promise = PromisePolyfill var parseQueryString = function(string) { if (string === "" || string == null) return {} @@ -1063,12 +1240,12 @@ var coreRouter = function($window) { return data } var asyncId - function debounceAsync(callback0) { + function debounceAsync(callback) { return function() { if (asyncId != null) return asyncId = callAsync0(function() { asyncId = null - callback0() + callback() }) } } @@ -1150,15 +1327,20 @@ var coreRouter = function($window) { } return router } -var _20 = function($window, redrawService0) { +var _21 = function($window, redrawService0) { var routeService = coreRouter($window) var identity = function(v) {return v} var render1, component, attrs3, currentPath, lastUpdate var route = function(root, defaultRoute, routes) { if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") - var run1 = function() { + function run1() { if (render1 != null) redrawService0.render(root, render1(Vnode(component, attrs3.key, attrs3))) } + var redraw2 = function() { + run1() + redraw2 = redrawService0.redraw + } + redrawService0.subscribe(root, run1) var bail = function(path) { if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true}) else throw new Error("Could not resolve default route " + defaultRoute) @@ -1169,7 +1351,7 @@ var _20 = function($window, redrawService0) { component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div" attrs3 = params, currentPath = path, lastUpdate = null render1 = (routeResolver.render || identity).bind(routeResolver) - run1() + redraw2() } if (payload.view || typeof payload === "function") update({}, payload) else { @@ -1181,16 +1363,18 @@ var _20 = function($window, redrawService0) { else update(payload, "div") } }, bail) - redrawService0.subscribe(root, run1) } route.set = function(path, data, options) { - if (lastUpdate != null) options = {replace: true} + if (lastUpdate != null) { + options = options || {} + options.replace = true + } lastUpdate = null routeService.setPath(path, data, options) } route.get = function() {return currentPath} route.prefix = function(prefix0) {routeService.prefix = prefix0} - route.link = function(vnode1) { + var link = function(options, vnode1) { vnode1.dom.setAttribute("href", routeService.prefix + vnode1.attrs.href) vnode1.dom.onclick = function(e) { if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return @@ -1198,30 +1382,35 @@ var _20 = function($window, redrawService0) { e.redraw = false var href = this.getAttribute("href") if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length) - route.set(href, undefined, undefined) + route.set(href, undefined, options) } } + route.link = function(args0) { + if (args0.tag == null) return link.bind(link, args0) + return link({}, args0) + } route.param = function(key3) { if(typeof attrs3 !== "undefined" && typeof key3 !== "undefined") return attrs3[key3] return attrs3 } return route } -m.route = _20(window, redrawService) -m.withAttr = function(attrName, callback1, context) { +m.route = _21(window, redrawService) +m.withAttr = function(attrName, callback, context) { return function(e) { - callback1.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) + callback.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) } } -var _28 = coreRenderer(window) -m.render = _28.render +var _29 = coreRenderer(window) +m.render = _29.render m.redraw = redrawService.redraw m.request = requestService.request m.jsonp = requestService.jsonp m.parseQueryString = parseQueryString m.buildQueryString = buildQueryString -m.version = "1.1.1" +m.version = "1.1.3" m.vnode = Vnode +m.PromisePolyfill = PromisePolyfill if (typeof module !== "undefined") module["exports"] = m else window.m = m }()); \ No newline at end of file diff --git a/mithril.min.js b/mithril.min.js index 1a5bb54a..67c396ce 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,43 +1,45 @@ -(function(){function B(b,d,f,g,e,n){return{tag:b,key:d,attrs:f,children:g,text:e,dom:n,domSize:void 0,state:void 0,_state:void 0,events:void 0,instance:void 0,skip:!1}}function C(b){var d=arguments[1],f=2,g;if(null==b||"string"!==typeof b&&"function"!==typeof b&&"function"!==typeof b.view)throw Error("The selector must be either a string or a component.");if("string"===typeof b){var e;if(!(e=M[b])){g="div";for(var n=[],k={};e=P.exec(b);){var q=e[1],m=e[2];""===q&&""!==m?g=m:"#"===q?k.id=m:"."===q? -n.push(m):"["===e[3][0]&&((q=e[6])&&(q=q.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")),"class"===e[4]?n.push(q):k[e[4]]=q||!0)}0a.indexOf("?")?"?":"&";a+=e+d}return a}function k(a){try{return""!==a?JSON.parse(a):null}catch(w){throw Error(a); -}}function q(a){return a.responseText}function m(a,b){if("function"===typeof a)if(Array.isArray(b))for(var d=0;dl.status||304===l.status||S.test(a.url))d(m(a.type, -h));else{var c=Error(l.responseText),p;for(p in h)c[p]=h[p];f(c)}}catch(v){f(v)}};g&&null!=a.data?l.send(a.data):l.send()});return!0===a.background?w:u(w)},jsonp:function(a,k){var u=f();a=g(a,k);var q=new d(function(d,f){var g=a.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+l++,k=b.document.createElement("script");b[g]=function(e){k.parentNode.removeChild(k);d(m(a.type,e));delete b[g]};k.onerror=function(){k.parentNode.removeChild(k);f(Error("JSONP request failed"));delete b[g]};null== -a.data&&(a.data={});a.url=e(a.url,a.data);a.data[a.callbackKey||"callback"]=g;k.src=n(a.url,a.data);b.document.documentElement.appendChild(k)});return!0===a.background?q:u(q)},setCompletionCallback:function(a){u=a}}}(window,x),O=function(b){function d(h,c,p,a,b,d,e){for(;p=v&&y>=t;){var r=c[v],z=p[t];if(r!==z||b)if(null==r)v++;else if(null==z)t++;else if(r.key===z.key){var A=null!=u&&v>=c.length-u.length||null==u&&b;v++;t++;k(h,r,z,e,m(c,v,g),A,n);b&&r.tag===z.tag&&l(h,q(r),g)}else if(r=c[w],r!==z||b)if(null==r)w--;else if(null==z)t++; -else if(r.key===z.key)A=null!=u&&w>=c.length-u.length||null==u&&b,k(h,r,z,e,m(c,w+1,g),A,n),(b||t=v&&y>=t;){r=c[w];z=p[y];if(r!==z||b)if(null==r)w--;else{if(null!=z)if(r.key===z.key)A=null!=u&&w>=c.length-u.length||null==u&&b,k(h,r,z,e,m(c,w+1,g),A,n),b&&r.tag===z.tag&&l(h,q(r),g),null!=r.dom&&(g=r.dom),w--;else{if(!G){G=c;var r=w,A={},E;for(E=0;Eb.indexOf("?")?"?":"&";b+=e+d}return b}function m(b){try{return""!==b?JSON.parse(b):null}catch(A){throw Error(b);}}function p(b){return b.responseText}function u(b,a){if("function"=== +typeof b)if(Array.isArray(a))for(var d=0;dh.status||304===h.status||U.test(b.url))d(u(b.type,a));else{var f=Error(h.responseText);f.code=h.status;f.response=a;e(f)}}catch(V){e(V)}};f&&null!=b.data?h.send(b.data):h.send()});return!0===b.background?A:h(A)},jsonp:function(b,p){var h=e();b=f(b,p);var m=new d(function(d,e){var f=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+r++,h=a.document.createElement("script");a[f]=function(e){h.parentNode.removeChild(h);d(u(b.type,e));delete a[f]};h.onerror=function(){h.parentNode.removeChild(h); +e(Error("JSONP request failed"));delete a[f]};null==b.data&&(b.data={});b.url=k(b.url,b.data);b.data[b.callbackKey||"callback"]=f;h.src=l(b.url,b.data);a.document.documentElement.appendChild(h)});return!0===b.background?m:h(m)},setCompletionCallback:function(b){z=b}}}(window,n),Q=function(a){function d(b,c){if(b.state!==c)throw Error("`vnode.state` must not be modified");}function e(b){var c=b.state;try{return this.apply(c,arguments)}finally{d(b,c)}}function f(b,c,g,a,d,e,f){for(;g'+c.children+"",q=q.firstChild):q.innerHTML=c.children;c.dom=q.firstChild;c.domSize=q.childNodes.length;for(c=y.createDocumentFragment();a=q.firstChild;)c.appendChild(a);z(b,c,d)}function m(b, +c,a,d,e,l){if(c!==a&&(null!=c||null!=a))if(null==c)f(b,a,0,a.length,d,e,l);else if(null==a)A(c,0,c.length);else{for(var q=0,g=0,t=!0,m=!0;g=g&&B>=q;)if(v=c[g],t=a[q],null==v)g++;else if(null==t)q++;else if(v.key===t.key)g++,q++,v!==t&&p(b,v,t,d,r(c,g,e),l);else if(t=a[B],null==v)g++;else if(null== +t)B--;else if(v.key===t.key)g++,q=g&&B>=q;){v=c[m];t=a[B];if(null==v)m--;else{if(null!=t)if(v.key===t.key)v!==t&&p(b,v,t,d,e,l),null!=t.dom&&(e=t.dom),m--;else{if(null==n){n=c;v=g;for(var y=m,x={};vm&&A(c,q,c.length);a.length>m&&f(b,a,q,a.length,d,e,l)}}}function p(a,c,g,d,f,r){var q=c.tag;if(q===g.tag){g.state=c.state;g.events=c.events;var t;var A;null!=g.attrs&&"function"===typeof g.attrs.onbeforeupdate&&(t=e.call(g.attrs.onbeforeupdate,g,c));"string"!==typeof g.tag&&"function"===typeof g.state.onbeforeupdate&& +(A=e.call(g.state.onbeforeupdate,g,c));void 0===t&&void 0===A||t||A?t=!1:(g.dom=c.dom,g.domSize=c.domSize,g.instance=c.instance,t=!0);if(!t)if("string"===typeof q)switch(null!=g.attrs&&J(g.attrs,g,d),q){case "#":c.children.toString()!==g.children.toString()&&(c.dom.nodeValue=g.children);g.dom=c.dom;break;case "<":c.children!==g.children?(u(c),l(a,g,r,f)):(g.dom=c.dom,g.domSize=c.domSize);break;case "[":m(a,c.children,g.children,d,f,r);c=0;d=g.children;g.dom=null;if(null!=d){for(var n=0;n`-separated string showing the structure of the test specification. +In the below example, `result.context` would be `testing > rocks`. + +```javascript +o.spec("testing", function() { + o.spec("rocks", function() { + o(false).equals(true) + }) +}) +``` + + + --- ## Goals @@ -433,8 +547,8 @@ $o.run() - Do the most common things that the mocha/chai/sinon triad does without having to install 3 different libraries and several dozen dependencies - Disallow configuration in test-space: - Disallow ability to pick between API styles (BDD/TDD/Qunit, assert/should/expect, etc) - - Disallow ability to pick between different reporters - Disallow ability to add custom assertion types + - Provide a default simple reporter - Make assertion code terse, readable and self-descriptive - Have as few assertion types as possible for a workable usage pattern diff --git a/ospec/bin/ospec b/ospec/bin/ospec index 11777a09..00ab7fab 100644 --- a/ospec/bin/ospec +++ b/ospec/bin/ospec @@ -17,6 +17,7 @@ function traverseDirectory(pathname, callback) { var promises = [] for (var i = 0; i < pathnames.length; i++) { if (pathnames[i] === "node_modules") continue + if (pathnames[i][0] === ".") continue pathnames[i] = path.join(pathname, pathnames[i]) promises.push(traverseDirectory(pathnames[i], callback)) } diff --git a/ospec/ospec.js b/ospec/ospec.js index 1424851b..c0bf2b60 100644 --- a/ospec/ospec.js +++ b/ospec/ospec.js @@ -1,9 +1,16 @@ -/* eslint-disable no-bitwise, no-process-exit */ +/* eslint-disable global-require, no-bitwise, no-process-exit */ "use strict" - -module.exports = new function init() { +;(function(m) { +if (typeof module !== "undefined") module["exports"] = m() +else window.o = m() +})(function init(name) { var spec = {}, subjects = [], results, only = null, ctx = spec, start, stack = 0, nextTickish, hasProcess = typeof process === "object", hasOwn = ({}).hasOwnProperty + if (name != null) spec[name] = ctx = {} + + try {throw new Error} catch (e) { + var ospecFileName = e.stack && (/[\/\\](.*?):\d+:\d+/).test(e.stack) ? e.stack.match(/[\/\\](.*?):\d+:\d+/)[1] : null + } function o(subject, predicate) { if (predicate === undefined) { if (results == null) throw new Error("Assertions should not occur outside test definitions") @@ -47,10 +54,35 @@ module.exports = new function init() { spy.callCount = 0 return spy } - o.run = function() { + o.cleanStackTrace = function(error) { + // For IE 10+ in quirks mode, and IE 9- in any mode, errors don't have a stack + if (error.stack == null) return "" + var i = 0, header = error.message ? error.name + ": " + error.message : error.name, stack + // some environments add the name and message to the stack trace + if (error.stack.indexOf(header) === 0) { + stack = error.stack.slice(header.length).split(/\r?\n/) + stack.shift() // drop the initial empty string + } else { + stack = error.stack.split(/\r?\n/) + } + if (ospecFileName == null) return stack.join("\n") + // skip ospec-related entries on the stack + while (stack[i].indexOf(ospecFileName) !== -1) i++ + // now we're in user code + return stack[i] + } + o.run = function(reporter) { results = [] start = new Date - test(spec, [], [], report) + test(spec, [], [], function() { + setTimeout(function () { + if (typeof reporter === "function") reporter(results) + else { + var errCount = o.report(results) + if (hasProcess && errCount !== 0) process.exit(1) + } + }) + }) function test(spec, pre, post, finalize) { pre = [].concat(pre, spec["__beforeEach"] || []) @@ -82,41 +114,56 @@ module.exports = new function init() { if (cursor === fns.length) return var fn = fns[cursor++] + var timeout = 0, delay = 200, s = new Date + var isDone = false + + function done(err) { + if (err) { + if (err instanceof Error) record(err.message, err) + else record(String(err)) + subjects.pop() + next() + } + if (timeout !== undefined) { + timeout = clearTimeout(timeout) + if (delay !== Infinity) record(null) + if (!isDone) next() + else throw new Error("`" + arg + "()` should only be called once") + isDone = true + } + else console.log("# elapsed: " + Math.round(new Date - s) + "ms, expected under " + delay + "ms") + } + + function startTimer() { + timeout = setTimeout(function() { + timeout = undefined + record("async test timed out") + next() + }, Math.min(delay, 2147483647)) + } + if (fn.length > 0) { - var timeout = 0, delay = 200, s = new Date - var isDone = false var body = fn.toString() var arg = (body.match(/\(([\w$]+)/) || body.match(/([\w$]+)\s*=>/) || []).pop() if (body.indexOf(arg) === body.lastIndexOf(arg)) throw new Error("`" + arg + "()` should be called at least once") try { - fn(function done() { - if (timeout !== undefined) { - timeout = clearTimeout(timeout) - if (delay !== Infinity) record(null) - if (!isDone) next() - else throw new Error("`" + arg + "()` should only be called once") - isDone = true - } - else console.log("# elapsed: " + Math.round(new Date - s) + "ms, expected under " + delay + "ms") - }, function(t) {delay = t}) + fn(done, function(t) {delay = t}) } catch (e) { - if (e instanceof Error) record(e.message, e) - else record(String(e)) - subjects.pop() - next() + done(e) } if (timeout === 0) { - timeout = setTimeout(function() { - timeout = undefined - record("async test timed out") - next() - }, Math.min(delay, 2147483647)) + startTimer() } } else { - fn() - nextTickish(next) + var p = fn() + if (p && p.then) { + startTimer() + p.then(function() { done() }, done) + } else { + nextTickish(next) + } } } } @@ -200,11 +247,13 @@ module.exports = new function init() { } result.context = subjects.join(" > ") result.message = message - result.error = error.stack + result.error = error + } results.push(result) } function serialize(value) { + if (hasProcess) return require("util").inspect(value) if (value === null || (typeof value === "object" && !(value instanceof Array)) || typeof value === "number") return String(value) else if (typeof value === "function") return value.name || "" try {return JSON.stringify(value)} catch (e) {return String(value)} @@ -213,23 +262,24 @@ module.exports = new function init() { return hasProcess ? "\x1b[31m" + message + "\x1b[0m" : "%c" + message + "%c " } - function report() { - var status = 0 + o.report = function (results) { + var errCount = 0 for (var i = 0, r; r = results[i]; i++) { if (!r.pass) { - var stackTrace = r.error.match(/^(?:(?!Error|[\/\\]ospec[\/\\]ospec\.js).)*$/m) + var stackTrace = o.cleanStackTrace(r.error) console.error(r.context + ":\n" + highlight(r.message) + (stackTrace ? "\n\n" + stackTrace + "\n\n" : ""), hasProcess ? "" : "color:red", hasProcess ? "" : "color:black") - status = 1 + errCount++ } } console.log( + (name ? name + ": " : "") + results.length + " assertions completed in " + Math.round(new Date - start) + "ms, " + "of which " + results.filter(function(result){return result.error}).length + " failed" ) - if (hasProcess && status === 1) process.exit(1) + return errCount } - if(hasProcess) { + if (hasProcess) { nextTickish = process.nextTick } else { nextTickish = function fakeFastNextTick(next) { @@ -239,4 +289,4 @@ module.exports = new function init() { } return o -} +}) diff --git a/ospec/package.json b/ospec/package.json index fdda49db..acc0ae47 100644 --- a/ospec/package.json +++ b/ospec/package.json @@ -1,6 +1,6 @@ { "name": "ospec", - "version": "1.2.3", + "version": "1.4.0", "description": "Noiseless testing framework", "main": "ospec.js", "directories": { @@ -12,5 +12,5 @@ "bin": { "ospec": "./bin/ospec" }, - "repository": "lhorie/mithril.js#rewrite" + "repository": "MithrilJS/mithril.js" } diff --git a/ospec/tests/test-ospec.js b/ospec/tests/test-ospec.js index 0367e6ec..526671c6 100644 --- a/ospec/tests/test-ospec.js +++ b/ospec/tests/test-ospec.js @@ -18,6 +18,77 @@ new function(o) { o.run() }(o) +new function(o) { + var clone = o.new() + + clone.spec("clone", function() { + clone("fail", function() { + clone(true).equals(false) + }) + + clone("pass", function() { + clone(true).equals(true) + }) + }) + + // Predicate test passing on clone results + o.spec("reporting", function() { + o("reports per instance", function(done, timeout) { + timeout(100) // Waiting on clone + + clone.run(function(results) { + o(typeof results).equals("object") + o("length" in results).equals(true) + o(results.length).equals(2)("Two results") + + o("error" in results[0] && "pass" in results[0]).equals(true)("error and pass keys present in failing result") + o(!("error" in results[1]) && "pass" in results[1]).equals(true)("only pass key present in passing result") + o(results[0].pass).equals(false)("Test meant to fail has failed") + o(results[1].pass).equals(true)("Test meant to pass has passed") + + done() + }) + }) + o("o.report() returns the number of failures", function () { + var log = console.log, error = console.error + console.log = o.spy() + console.error = o.spy() + + function makeError(msg) {try{throw msg ? new Error(msg) : new Error} catch(e){return e}} + try { + var errCount = o.report([{pass: true}, {pass: true}]) + + o(errCount).equals(0) + o(console.log.callCount).equals(1) + o(console.error.callCount).equals(0) + + errCount = o.report([ + {pass: false, error: makeError("hey"), message: "hey"} + ]) + + o(errCount).equals(1) + o(console.log.callCount).equals(2) + o(console.error.callCount).equals(1) + + errCount = o.report([ + {pass: false, error: makeError("hey"), message: "hey"}, + {pass: true}, + {pass: false, error: makeError("ho"), message: "ho"} + ]) + + o(errCount).equals(2) + o(console.log.callCount).equals(3) + o(console.error.callCount).equals(3) + } catch (e) { + o(1).equals(0)("Error while testing the reporter") + } + + console.log = log + console.error = error + }) + }) +}(o) + o.spec("ospec", function() { o.spec("sync", function() { var a = 0, b = 0, illegalAssertionThrows = false @@ -107,7 +178,7 @@ o.spec("ospec", function() { o(output).deepEquals({tag: "div", children: children}) }) }) - o.spec("async", function() { + o.spec("async callback", function() { var a = 0, b = 0 o.before(function(done) { @@ -148,4 +219,63 @@ o.spec("ospec", function() { }) }) }) + + o.spec("stack trace cleaner", function() { + o("handles line breaks", function() { + try { + throw new Error("line\nbreak") + } catch(error) { + var trace = o.cleanStackTrace(error) + o(trace).notEquals("break") + o(trace.includes("test-ospec.js")).equals(true) + } + }) + }) + + o.spec("async promise", function() { + var a = 0, b = 0 + + function wrapPromise(fn) { + return new Promise((resolve, reject) => { + callAsync(() => { + try { + fn() + resolve() + } catch(e) { + reject(e) + } + }) + }) + } + + o.before(function() { + return wrapPromise(() => { + a = 1 + }) + }) + + o.after(function() { + return wrapPromise(function() { + a = 0 + }) + }) + + o.beforeEach(function() { + return wrapPromise(function() { + b = 1 + }) + }) + o.afterEach(function() { + return wrapPromise(function() { + b = 0 + }) + }) + + o("promise functions", function() { + return wrapPromise(function() { + o(a).equals(b) + o(a).equals(1)("a and b should be initialized") + }) + }) + }) }) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..8dd68afc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4310 @@ +{ + "name": "mithril", + "version": "1.1.3", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@alrra/travis-scripts": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@alrra/travis-scripts/-/travis-scripts-3.0.1.tgz", + "integrity": "sha1-RdW5NXMXtsxVU9/ZmTGEOuCw5To=", + "dev": true + }, + "abab": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.3.tgz", + "integrity": "sha1-uB3l9ydOxOdW15fNg08wNkJyTl0=", + "dev": true + }, + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "dev": true + }, + "acorn-globals": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", + "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", + "dev": true, + "requires": { + "acorn": "4.0.13" + } + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "3.3.0" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "agent-base": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-2.1.1.tgz", + "integrity": "sha1-1t4Q1a9hMtW9aSQn1G/FOFOQlMc=", + "dev": true, + "requires": { + "extend": "3.0.1", + "semver": "5.0.3" + } + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "anymatch": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz", + "integrity": "sha1-o+Uvo5FoyCX/V7AkgSbOWo/5VQc=", + "dev": true, + "requires": { + "arrify": "1.0.1", + "micromatch": "2.3.11" + } + }, + "app-root-path": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.0.1.tgz", + "integrity": "sha1-zWLc+OT9WkF+/GZNLlsQZTxlG0Y=", + "dev": true + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true + }, + "async": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/async/-/async-2.1.2.tgz", + "integrity": "sha1-YSpKtF70KnDN6Aa62G7m2wR+g4U=", + "dev": true, + "requires": { + "lodash": "4.17.4" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true + }, + "babel-code-frame": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", + "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babel-core": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.25.0.tgz", + "integrity": "sha1-fdQrBGPHQunVKW3rPsZ6kyLa1yk=", + "dev": true, + "requires": { + "babel-code-frame": "6.22.0", + "babel-generator": "6.25.0", + "babel-helpers": "6.24.1", + "babel-messages": "6.23.0", + "babel-register": "6.24.1", + "babel-runtime": "6.23.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.17.4", + "convert-source-map": "1.5.0", + "debug": "2.6.8", + "json5": "0.5.1", + "lodash": "4.17.4", + "minimatch": "3.0.4", + "path-is-absolute": "1.0.1", + "private": "0.1.7", + "slash": "1.0.0", + "source-map": "0.5.6" + }, + "dependencies": { + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", + "dev": true + } + } + }, + "babel-generator": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.25.0.tgz", + "integrity": "sha1-M6GvcNXyiQrrRlpKd5PB32qeqfw=", + "dev": true, + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.23.0", + "babel-types": "6.25.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.4", + "source-map": "0.5.6", + "trim-right": "1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", + "dev": true + } + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-template": "6.25.0" + } + }, + "babel-jest": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-20.0.3.tgz", + "integrity": "sha1-5KA7E9wQOJ4UD8ZF0J/8TO0wFnE=", + "dev": true, + "requires": { + "babel-core": "6.25.0", + "babel-plugin-istanbul": "4.1.4", + "babel-preset-jest": "20.0.3" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0" + } + }, + "babel-plugin-istanbul": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.4.tgz", + "integrity": "sha1-GN3oS/POMp/d8/QQP66SFFbY5Yc=", + "dev": true, + "requires": { + "find-up": "2.1.0", + "istanbul-lib-instrument": "1.7.4", + "test-exclude": "4.1.1" + } + }, + "babel-plugin-jest-hoist": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-20.0.3.tgz", + "integrity": "sha1-r+3IU70/jcNUjqZx++adA8wsF2c=", + "dev": true + }, + "babel-polyfill": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.23.0.tgz", + "integrity": "sha1-g2TKYt+Or7gwSZ9pkXdGbDsDSZ0=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "core-js": "2.4.1", + "regenerator-runtime": "0.10.5" + } + }, + "babel-preset-jest": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-20.0.3.tgz", + "integrity": "sha1-y6yq3stdaJyh4d4TYOv8ZoYsF4o=", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "20.0.3" + } + }, + "babel-register": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.24.1.tgz", + "integrity": "sha1-fhDhOi9xBlvfrVoXh7pFvKbe118=", + "dev": true, + "requires": { + "babel-core": "6.25.0", + "babel-runtime": "6.23.0", + "core-js": "2.4.1", + "home-or-tmp": "2.0.0", + "lodash": "4.17.4", + "mkdirp": "0.5.1", + "source-map-support": "0.4.15" + } + }, + "babel-runtime": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.23.0.tgz", + "integrity": "sha1-CpSJ8UTecO+zzkMArM2zKeL8VDs=", + "dev": true, + "requires": { + "core-js": "2.4.1", + "regenerator-runtime": "0.10.5" + } + }, + "babel-template": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.25.0.tgz", + "integrity": "sha1-ZlJBFmt8KqTGGdceGSlpVSsQwHE=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.17.4", + "lodash": "4.17.4" + } + }, + "babel-traverse": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.25.0.tgz", + "integrity": "sha1-IldJfi/NGbie3BPEyROB+VEklvE=", + "dev": true, + "requires": { + "babel-code-frame": "6.22.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.23.0", + "babel-types": "6.25.0", + "babylon": "6.17.4", + "debug": "2.6.8", + "globals": "9.18.0", + "invariant": "2.2.2", + "lodash": "4.17.4" + } + }, + "babel-types": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.25.0.tgz", + "integrity": "sha1-cK+ySNVmDl0Y+BHZHIMDtUE0oY4=", + "dev": true, + "requires": { + "babel-runtime": "6.23.0", + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.17.4", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.17.4.tgz", + "integrity": "sha512-kChlV+0SXkjE0vUn9OZ7pBMWRFd8uq3mZe8x1K6jhuNcAFAtEnjchFAqB+dYEXKyd+JpT6eppRR78QAr5gTsUw==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "benchmark": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", + "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", + "dev": true, + "requires": { + "lodash": "4.17.4", + "platform": "1.3.4" + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "browser-resolve": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", + "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "bser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", + "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", + "dev": true, + "requires": { + "node-int64": "0.4.0" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "circular-json": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.1.tgz", + "integrity": "sha1-vos2rvzN6LPKeqLWr8B6NyQsDS0=", + "dev": true + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "1.0.1" + } + }, + "cli-spinners": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-0.1.2.tgz", + "integrity": "sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw=", + "dev": true + }, + "cli-truncate": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", + "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", + "dev": true, + "requires": { + "slice-ansi": "0.0.4", + "string-width": "1.0.2" + } + }, + "cli-width": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz", + "integrity": "sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "collections": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/collections/-/collections-0.2.2.tgz", + "integrity": "sha1-HyMCay7zb5J+7MkB6ZxfDUj6M04=", + "dev": true, + "requires": { + "weak-map": "1.0.0" + } + }, + "color-convert": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", + "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "typedarray": "0.0.6" + } + }, + "content-type-parser": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-type-parser/-/content-type-parser-1.0.1.tgz", + "integrity": "sha1-w+VpiMU8ZRJ/tG1AMqOpACRv3JQ=", + "dev": true + }, + "convert-source-map": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz", + "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=", + "dev": true + }, + "core-js": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", + "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cosmiconfig": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-1.1.0.tgz", + "integrity": "sha1-DeoPmATv37kp+7GxiOJVU+oFPTc=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "js-yaml": "3.9.0", + "minimist": "1.2.0", + "object-assign": "4.1.1", + "os-homedir": "1.0.2", + "parse-json": "2.2.0", + "pinkie-promise": "2.0.1", + "require-from-string": "1.2.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.1", + "shebang-command": "1.2.0", + "which": "1.2.14" + } + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.10.1" + } + }, + "cssom": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", + "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=", + "dev": true + }, + "cssstyle": { + "version": "0.2.37", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", + "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", + "dev": true, + "requires": { + "cssom": "0.3.2" + } + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "0.10.24" + } + }, + "danger": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/danger/-/danger-1.2.0.tgz", + "integrity": "sha512-uFZb/QIGMnuHgireqI1Nu0pLoYGOtCj7E5DD8wUkNyRq0LjDiz3P2M5SMjbgparl8BG+suqmWc+fo3VksSJJnw==", + "dev": true, + "requires": { + "babel-polyfill": "6.23.0", + "chalk": "2.1.0", + "commander": "2.11.0", + "debug": "2.6.8", + "github": "9.2.0", + "jest-config": "20.0.4", + "jest-environment-node": "20.0.3", + "jest-runtime": "20.0.4", + "jsome": "2.3.26", + "jsonpointer": "4.0.1", + "lodash.find": "4.6.0", + "lodash.includes": "4.3.0", + "lodash.isobject": "2.4.1", + "lodash.keys": "4.2.0", + "node-fetch": "1.7.1", + "parse-diff": "0.4.0", + "parse-link-header": "1.0.1", + "rfc6902": "1.3.0", + "voca": "1.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.0" + } + }, + "chalk": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", + "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.2.1" + } + }, + "supports-color": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz", + "integrity": "sha512-qxzYsob3yv6U+xMzPrv170y8AwGP7i74g+pbixCfD6rgso8BscLT2qXIuz6TpOaiJZ3mFgT5O9lyT9nMU4LfaA==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "date-fns": { + "version": "1.28.5", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.28.5.tgz", + "integrity": "sha1-JXz8RdMi30XvVlhmWWfuhBzXP68=", + "dev": true + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "5.0.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.0", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "rimraf": "2.6.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "diff": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.0.tgz", + "integrity": "sha512-w0XZubFWn0Adlsapj9EAWX0FqWdO4tz8kc3RiYdWLh4k/V8PTb6i0SMgXt0vRM3zyKnT8tKO7mUlieRQHIjMNg==", + "dev": true + }, + "doctrine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", + "integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=", + "dev": true, + "requires": { + "esutils": "2.0.2", + "isarray": "1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "elegant-spinner": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", + "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", + "dev": true + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "dev": true, + "requires": { + "iconv-lite": "0.4.13" + } + }, + "errno": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", + "integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=", + "dev": true, + "requires": { + "prr": "0.0.0" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "es5-ext": { + "version": "0.10.24", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.24.tgz", + "integrity": "sha1-pVh3yZJLwMjZvTwsvhdJWsFwmxQ=", + "dev": true, + "requires": { + "es6-iterator": "2.0.1", + "es6-symbol": "3.1.1" + } + }, + "es6-iterator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz", + "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.24", + "es6-symbol": "3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.24", + "es6-iterator": "2.0.1", + "es6-set": "0.1.5", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.24", + "es6-iterator": "2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.24" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.24", + "es6-iterator": "2.0.1", + "es6-symbol": "3.1.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "2.7.3", + "estraverse": "1.9.3", + "esutils": "2.0.2", + "optionator": "0.8.2", + "source-map": "0.2.0" + } + }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true, + "requires": { + "es6-map": "0.1.5", + "es6-weak-map": "2.0.2", + "esrecurse": "4.2.0", + "estraverse": "4.2.0" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "eslint": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", + "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", + "dev": true, + "requires": { + "babel-code-frame": "6.22.0", + "chalk": "1.1.3", + "concat-stream": "1.6.0", + "debug": "2.6.8", + "doctrine": "2.0.0", + "escope": "3.6.0", + "espree": "3.4.3", + "esquery": "1.0.0", + "estraverse": "4.2.0", + "esutils": "2.0.2", + "file-entry-cache": "2.0.0", + "glob": "7.1.2", + "globals": "9.18.0", + "ignore": "3.3.3", + "imurmurhash": "0.1.4", + "inquirer": "0.12.0", + "is-my-json-valid": "2.16.0", + "is-resolvable": "1.0.0", + "js-yaml": "3.9.0", + "json-stable-stringify": "1.0.1", + "levn": "0.3.0", + "lodash": "4.17.4", + "mkdirp": "0.5.1", + "natural-compare": "1.4.0", + "optionator": "0.8.2", + "path-is-inside": "1.0.2", + "pluralize": "1.2.1", + "progress": "1.1.8", + "require-uncached": "1.0.3", + "shelljs": "0.7.8", + "strip-bom": "3.0.0", + "strip-json-comments": "2.0.1", + "table": "3.8.3", + "text-table": "0.2.0", + "user-home": "2.0.0" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "espree": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.4.3.tgz", + "integrity": "sha1-KRC1zNSc6JPC//+qtP2LOjG4I3Q=", + "dev": true, + "requires": { + "acorn": "5.1.1", + "acorn-jsx": "3.0.1" + }, + "dependencies": { + "acorn": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.1.tgz", + "integrity": "sha512-vOk6uEMctu0vQrvuSqFdJyqj1Q0S5VTDL79qtjo+DhRr+1mmaD+tluFSCZqhvi/JUhXSzoZN2BhtstaPEeE8cw==", + "dev": true + } + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "esquery": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", + "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", + "dev": true, + "requires": { + "estraverse": "4.2.0" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", + "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", + "dev": true, + "requires": { + "estraverse": "4.2.0", + "object-assign": "4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.24" + } + }, + "exec-sh": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.0.tgz", + "integrity": "sha1-FPdd4/INKG75MwmbLOUKkDWc7xA=", + "dev": true, + "requires": { + "merge": "1.2.0" + } + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.3" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "extsprintf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fb-watchman": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", + "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", + "dev": true, + "requires": { + "bser": "2.0.0" + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5", + "object-assign": "4.1.1" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "1.2.2", + "object-assign": "4.1.1" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "flat-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.2.tgz", + "integrity": "sha1-+oZxTnLCHbiGAXYezy9VXRq8a5Y=", + "dev": true, + "requires": { + "circular-json": "0.3.1", + "del": "2.2.2", + "graceful-fs": "4.1.11", + "write": "0.2.1" + } + }, + "follow-redirects": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz", + "integrity": "sha1-NLkLqyqRGqNHVx2pDyK9NuzYqRk=", + "dev": true, + "requires": { + "debug": "2.6.8", + "stream-consume": "0.1.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "1.0.2" + } + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "gh-pages": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-0.12.0.tgz", + "integrity": "sha1-2VHj7Zi4VpnUsEGOsaFbGgSYjcE=", + "dev": true, + "requires": { + "async": "2.1.2", + "commander": "2.9.0", + "globby": "6.1.0", + "graceful-fs": "4.1.10", + "q": "1.4.1", + "q-io": "1.13.2", + "rimraf": "2.6.1" + }, + "dependencies": { + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "requires": { + "graceful-readlink": "1.0.1" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "graceful-fs": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.10.tgz", + "integrity": "sha1-8tcgwiCS90Mih3XHXjYSYyUB8TE=", + "dev": true + } + } + }, + "github": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/github/-/github-9.2.0.tgz", + "integrity": "sha1-iohtxA3WNjZwfcr5nfPfJsWfFvw=", + "dev": true, + "requires": { + "follow-redirects": "0.0.7", + "https-proxy-agent": "1.0.0", + "mime": "1.3.6", + "netrc": "0.1.4" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "arrify": "1.0.1", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "handlebars": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.10.tgz", + "integrity": "sha1-PTDHGLCaPZbyPqTMH0A8TTup/08=", + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", + "dev": true + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "dev": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "dev": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.1.tgz", + "integrity": "sha1-eb96eF6klf5mFl5zQVPzY/9UN9o=", + "dev": true, + "requires": { + "whatwg-encoding": "1.0.1" + } + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.0", + "sshpk": "1.13.1" + } + }, + "https-proxy-agent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz", + "integrity": "sha1-NffabEjOTdv6JkiRrFk+5f+GceY=", + "dev": true, + "requires": { + "agent-base": "2.1.1", + "debug": "2.6.8", + "extend": "3.0.1" + } + }, + "iconv-lite": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", + "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=", + "dev": true + }, + "ignore": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz", + "integrity": "sha1-QyNS5XrM2HqzEQ6C0/6g5HgSFW0=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "inquirer": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", + "dev": true, + "requires": { + "ansi-escapes": "1.4.0", + "ansi-regex": "2.1.1", + "chalk": "1.1.3", + "cli-cursor": "1.0.2", + "cli-width": "2.1.0", + "figures": "1.7.0", + "lodash": "4.17.4", + "readline2": "1.0.1", + "run-async": "0.1.0", + "rx-lite": "3.1.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "through": "2.3.8" + } + }, + "interpret": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", + "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=", + "dev": true + }, + "invariant": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", + "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", + "dev": true, + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-my-json-valid": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", + "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=", + "dev": true, + "requires": { + "generate-function": "2.0.0", + "generate-object-property": "1.2.0", + "jsonpointer": "4.0.1", + "xtend": "4.0.1" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "dev": true, + "requires": { + "is-path-inside": "1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", + "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-resolvable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", + "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", + "dev": true, + "requires": { + "tryit": "1.0.3" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.9", + "async": "1.5.2", + "escodegen": "1.8.1", + "esprima": "2.7.3", + "glob": "5.0.15", + "handlebars": "4.0.10", + "js-yaml": "3.9.0", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "once": "1.4.0", + "resolve": "1.1.7", + "supports-color": "3.2.3", + "which": "1.2.14", + "wordwrap": "1.0.0" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + }, + "dependencies": { + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + } + } + } + } + }, + "istanbul-lib-coverage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz", + "integrity": "sha512-0+1vDkmzxqJIn5rcoEqapSB4DmPxE31EtI2dF2aCkV5esN9EWHxZ0dwgDClivMXJqE7zaYQxq30hj5L0nlTN5Q==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.4.tgz", + "integrity": "sha1-6f2SDkdn89Ge3HZeLWs/XMvQ7qg=", + "dev": true, + "requires": { + "babel-generator": "6.25.0", + "babel-template": "6.25.0", + "babel-traverse": "6.25.0", + "babel-types": "6.25.0", + "babylon": "6.17.4", + "istanbul-lib-coverage": "1.1.1", + "semver": "5.3.0" + }, + "dependencies": { + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + } + } + }, + "jest-config": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-20.0.4.tgz", + "integrity": "sha1-43kwqyIXyRNgXv8T5712PsSPruo=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "glob": "7.1.2", + "jest-environment-jsdom": "20.0.3", + "jest-environment-node": "20.0.3", + "jest-jasmine2": "20.0.4", + "jest-matcher-utils": "20.0.3", + "jest-regex-util": "20.0.3", + "jest-resolve": "20.0.4", + "jest-validate": "20.0.3", + "pretty-format": "20.0.3" + } + }, + "jest-diff": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-20.0.3.tgz", + "integrity": "sha1-gfKI/Z5nXw+yPHXxwrGURf5YZhc=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "diff": "3.3.0", + "jest-matcher-utils": "20.0.3", + "pretty-format": "20.0.3" + } + }, + "jest-docblock": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-20.0.3.tgz", + "integrity": "sha1-F76phDQswz2DxQ++FUXqDvqkRxI=", + "dev": true + }, + "jest-environment-jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-20.0.3.tgz", + "integrity": "sha1-BIqKwS7iJfcZBBdxODS7mZeH3pk=", + "dev": true, + "requires": { + "jest-mock": "20.0.3", + "jest-util": "20.0.3", + "jsdom": "9.12.0" + } + }, + "jest-environment-node": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-20.0.3.tgz", + "integrity": "sha1-1Ii8RhKvLCRumG6K52caCZFj1AM=", + "dev": true, + "requires": { + "jest-mock": "20.0.3", + "jest-util": "20.0.3" + } + }, + "jest-haste-map": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-20.0.4.tgz", + "integrity": "sha1-ZT61XIic48Ah97lGk/IKQVm63wM=", + "dev": true, + "requires": { + "fb-watchman": "2.0.0", + "graceful-fs": "4.1.11", + "jest-docblock": "20.0.3", + "micromatch": "2.3.11", + "sane": "1.6.0", + "worker-farm": "1.4.1" + } + }, + "jest-jasmine2": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-20.0.4.tgz", + "integrity": "sha1-/MWxQReA2RHQQpAu8YWehS5g1eE=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "graceful-fs": "4.1.11", + "jest-diff": "20.0.3", + "jest-matcher-utils": "20.0.3", + "jest-matchers": "20.0.3", + "jest-message-util": "20.0.3", + "jest-snapshot": "20.0.3", + "once": "1.4.0", + "p-map": "1.1.1" + } + }, + "jest-matcher-utils": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-20.0.3.tgz", + "integrity": "sha1-s6a443yld4A7CDKpixZPRLeBVhI=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "pretty-format": "20.0.3" + } + }, + "jest-matchers": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-matchers/-/jest-matchers-20.0.3.tgz", + "integrity": "sha1-ymnbHDLbWm9wf6XgQBq7VXAN/WA=", + "dev": true, + "requires": { + "jest-diff": "20.0.3", + "jest-matcher-utils": "20.0.3", + "jest-message-util": "20.0.3", + "jest-regex-util": "20.0.3" + } + }, + "jest-message-util": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-20.0.3.tgz", + "integrity": "sha1-auwoRDBvyw5udNV5bBAG2W/dgxw=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "micromatch": "2.3.11", + "slash": "1.0.0" + } + }, + "jest-mock": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-20.0.3.tgz", + "integrity": "sha1-i8Bw6QQUqhVcEajWTIaaDVxx2lk=", + "dev": true + }, + "jest-regex-util": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-20.0.3.tgz", + "integrity": "sha1-hburXRM+RGJbGfr4xqpRItCF12I=", + "dev": true + }, + "jest-resolve": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-20.0.4.tgz", + "integrity": "sha1-lEiz6La6/BVHlETGSZBFt//ll6U=", + "dev": true, + "requires": { + "browser-resolve": "1.11.2", + "is-builtin-module": "1.0.0", + "resolve": "1.3.3" + } + }, + "jest-runtime": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-20.0.4.tgz", + "integrity": "sha1-osgCIZxCA/dU3xQE5JAYYWnRJNg=", + "dev": true, + "requires": { + "babel-core": "6.25.0", + "babel-jest": "20.0.3", + "babel-plugin-istanbul": "4.1.4", + "chalk": "1.1.3", + "convert-source-map": "1.5.0", + "graceful-fs": "4.1.11", + "jest-config": "20.0.4", + "jest-haste-map": "20.0.4", + "jest-regex-util": "20.0.3", + "jest-resolve": "20.0.4", + "jest-util": "20.0.3", + "json-stable-stringify": "1.0.1", + "micromatch": "2.3.11", + "strip-bom": "3.0.0", + "yargs": "7.1.0" + } + }, + "jest-snapshot": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-20.0.3.tgz", + "integrity": "sha1-W4R+GtsaTZCFKn+fElCG4YfHZWY=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "jest-diff": "20.0.3", + "jest-matcher-utils": "20.0.3", + "jest-util": "20.0.3", + "natural-compare": "1.4.0", + "pretty-format": "20.0.3" + } + }, + "jest-util": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-20.0.3.tgz", + "integrity": "sha1-DAf32A2C9OWmfG+LnD/n9lz9Mq0=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "graceful-fs": "4.1.11", + "jest-message-util": "20.0.3", + "jest-mock": "20.0.3", + "jest-validate": "20.0.3", + "leven": "2.1.0", + "mkdirp": "0.5.1" + } + }, + "jest-validate": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-20.0.3.tgz", + "integrity": "sha1-0M/R3k9XnymEhJJcKA+PHZTsPKs=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "jest-matcher-utils": "20.0.3", + "leven": "2.1.0", + "pretty-format": "20.0.3" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.9.0.tgz", + "integrity": "sha512-0LoUNELX4S+iofCT8f4uEHIiRBR+c2AINyC8qRWfC6QNruLtxVZRJaPcu/xwMgFIgDxF25tGHaDjvxzJCNE9yw==", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "jsdom": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-9.12.0.tgz", + "integrity": "sha1-6MVG//ywbADUgzyoRBD+1/igl9Q=", + "dev": true, + "requires": { + "abab": "1.0.3", + "acorn": "4.0.13", + "acorn-globals": "3.1.0", + "array-equal": "1.0.0", + "content-type-parser": "1.0.1", + "cssom": "0.3.2", + "cssstyle": "0.2.37", + "escodegen": "1.8.1", + "html-encoding-sniffer": "1.0.1", + "nwmatcher": "1.4.1", + "parse5": "1.5.1", + "request": "2.81.0", + "sax": "1.2.4", + "symbol-tree": "3.2.2", + "tough-cookie": "2.3.2", + "webidl-conversions": "4.0.1", + "whatwg-encoding": "1.0.1", + "whatwg-url": "4.8.0", + "xml-name-validator": "2.0.1" + } + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "jsome": { + "version": "2.3.26", + "resolved": "https://registry.npmjs.org/jsome/-/jsome-2.3.26.tgz", + "integrity": "sha1-jLRDiSTSyd1SlMkK3wPzVBT7PKk=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "json-stringify-safe": "5.0.1", + "yargs": "4.8.1" + }, + "dependencies": { + "yargs": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz", + "integrity": "sha1-wMQpJMpKqmsObaFznfshZDn53cA=", + "dev": true, + "requires": { + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "lodash.assign": "4.2.0", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "window-size": "0.2.0", + "y18n": "3.2.1", + "yargs-parser": "2.4.1" + } + }, + "yargs-parser": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", + "integrity": "sha1-hVaN488VD/SfpRgl8DqMiA3cxcQ=", + "dev": true, + "requires": { + "camelcase": "3.0.0", + "lodash.assign": "4.2.0" + } + } + } + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", + "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "optional": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "lint-staged": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-4.0.4.tgz", + "integrity": "sha1-nKaWizDfv+gTZbenY81PSZKJZVM=", + "dev": true, + "requires": { + "app-root-path": "2.0.1", + "cosmiconfig": "1.1.0", + "execa": "0.8.0", + "listr": "0.12.0", + "lodash.chunk": "4.2.0", + "minimatch": "3.0.4", + "npm-which": "3.0.1", + "p-map": "1.1.1", + "staged-git-files": "0.0.4" + }, + "dependencies": { + "execa": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", + "integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + } + } + }, + "listr": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/listr/-/listr-0.12.0.tgz", + "integrity": "sha1-a84sD1YD+klYDqF81qAMwOX6RRo=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "cli-truncate": "0.2.1", + "figures": "1.7.0", + "indent-string": "2.1.0", + "is-promise": "2.1.0", + "is-stream": "1.1.0", + "listr-silent-renderer": "1.1.1", + "listr-update-renderer": "0.2.0", + "listr-verbose-renderer": "0.4.0", + "log-symbols": "1.0.2", + "log-update": "1.0.2", + "ora": "0.2.3", + "p-map": "1.1.1", + "rxjs": "5.4.2", + "stream-to-observable": "0.1.0", + "strip-ansi": "3.0.1" + } + }, + "listr-silent-renderer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", + "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=", + "dev": true + }, + "listr-update-renderer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz", + "integrity": "sha1-yoDhd5tOcCZoB+ju0a1qvjmFUPk=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "cli-truncate": "0.2.1", + "elegant-spinner": "1.0.1", + "figures": "1.7.0", + "indent-string": "3.1.0", + "log-symbols": "1.0.2", + "log-update": "1.0.2", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "indent-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.1.0.tgz", + "integrity": "sha1-CP9DNGAziDmbMp5rlTjcejz13n0=", + "dev": true + } + } + }, + "listr-verbose-renderer": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.4.0.tgz", + "integrity": "sha1-RNwBuww0oDxXIVTU0Izemx3FYg8=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "cli-cursor": "1.0.2", + "date-fns": "1.28.5", + "figures": "1.7.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + } + }, + "locater": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/locater/-/locater-1.3.0.tgz", + "integrity": "sha1-3HPcjgytwGQOP0IXIHtCZ7BEFOs=", + "dev": true + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + }, + "lodash._objecttypes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", + "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=", + "dev": true + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", + "dev": true + }, + "lodash.chunk": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.chunk/-/lodash.chunk-4.2.0.tgz", + "integrity": "sha1-ZuXOH3btJ7QwPYxlEujRIW6BBrw=", + "dev": true + }, + "lodash.find": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", + "integrity": "sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=", + "dev": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=", + "dev": true + }, + "lodash.isobject": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", + "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", + "dev": true, + "requires": { + "lodash._objecttypes": "2.4.1" + } + }, + "lodash.keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-4.2.0.tgz", + "integrity": "sha1-oIYCrBLk+4P5H8H7ejYKTZujUgU=", + "dev": true + }, + "log-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", + "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", + "dev": true, + "requires": { + "chalk": "1.1.3" + } + }, + "log-update": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz", + "integrity": "sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE=", + "dev": true, + "requires": { + "ansi-escapes": "1.4.0", + "cli-cursor": "1.0.2" + } + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "dev": true, + "requires": { + "js-tokens": "3.0.2" + } + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "dev": true, + "requires": { + "tmpl": "1.0.4" + } + }, + "marked": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", + "integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=", + "dev": true + }, + "merge": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", + "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=", + "dev": true + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.3" + } + }, + "mime": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.6.tgz", + "integrity": "sha1-WR2E02U6awtKO5343lqoEI5y5eA=", + "dev": true + }, + "mime-db": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=", + "dev": true + }, + "mime-types": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", + "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", + "dev": true, + "requires": { + "mime-db": "1.27.0" + } + }, + "mimeparse": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/mimeparse/-/mimeparse-0.1.4.tgz", + "integrity": "sha1-2vsCdSNw/SJgk64xUsJxrwGsJUo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "mute-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "netrc": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/netrc/-/netrc-0.1.4.tgz", + "integrity": "sha1-a+lPysqNd63gqWcNxGCRTJRHJEQ=", + "dev": true + }, + "node-fetch": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.1.tgz", + "integrity": "sha512-j8XsFGCLw79vWXkZtMSmmLaOk9z5SQ9bV/tkbZVCqvgwzrjAGq66igobLofHtF63NvMTp2WjytpsNTGKa+XRIQ==", + "dev": true, + "requires": { + "encoding": "0.1.12", + "is-stream": "1.1.0" + } + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1.0.9" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.0.3", + "validate-npm-package-license": "3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.0.2" + } + }, + "npm-path": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/npm-path/-/npm-path-2.0.3.tgz", + "integrity": "sha1-Fc/04ciaONp39W9gVbJPl137K74=", + "dev": true, + "requires": { + "which": "1.2.14" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "npm-which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-which/-/npm-which-3.0.1.tgz", + "integrity": "sha1-kiXybsOihcIJyuZ8OxGmtKtxQKo=", + "dev": true, + "requires": { + "commander": "2.11.0", + "npm-path": "2.0.3", + "which": "1.2.14" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "nwmatcher": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.1.tgz", + "integrity": "sha1-eumwew6oBNt+JfBctf5Al9TklJ8=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.8", + "wordwrap": "0.0.3" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + } + }, + "ora": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/ora/-/ora-0.2.3.tgz", + "integrity": "sha1-N1J9Igrc1Tw5tzVx11QVbV22V6Q=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "cli-cursor": "1.0.2", + "cli-spinners": "0.1.2", + "object-assign": "4.1.1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", + "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=", + "dev": true + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "1.1.0" + } + }, + "p-map": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.1.1.tgz", + "integrity": "sha1-BfXkrpegaDcbwqXMhr+9vBnErno=", + "dev": true + }, + "parse-diff": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/parse-diff/-/parse-diff-0.4.0.tgz", + "integrity": "sha1-nONbzOj8C3xY9G1xETOU/AtJgt0=", + "dev": true + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "parse-link-header": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-link-header/-/parse-link-header-1.0.1.tgz", + "integrity": "sha1-vt/g0hGK64S+deewJUGeyKYRQKc=", + "dev": true, + "requires": { + "xtend": "4.0.1" + } + }, + "parse5": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", + "integrity": "sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pinpoint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pinpoint/-/pinpoint-1.1.0.tgz", + "integrity": "sha1-DPd1eml38b9/ajIge3CeN3OI6HQ=", + "dev": true + }, + "platform": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.4.tgz", + "integrity": "sha1-bw+xftqqSPIUQrOpdcBjEw8cPr0=", + "dev": true + }, + "pluralize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "pretty-format": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-20.0.3.tgz", + "integrity": "sha1-Ag41ClYKH+GpjcO+tsz/s4beixQ=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1", + "ansi-styles": "3.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.1.0.tgz", + "integrity": "sha1-CcIC1ckX7CMYjKpcnLkXnNlUd1A=", + "dev": true, + "requires": { + "color-convert": "1.9.0" + } + } + } + }, + "private": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.7.tgz", + "integrity": "sha1-aM5eih7woju1cMwoU3tTMqumPvE=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "prr": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", + "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "q": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", + "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", + "dev": true + }, + "q-io": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/q-io/-/q-io-1.13.2.tgz", + "integrity": "sha1-7qEw1IHdteGqG8WmaFX3OR0G8AM=", + "dev": true, + "requires": { + "collections": "0.2.2", + "mime": "1.3.6", + "mimeparse": "0.1.4", + "q": "1.4.1", + "qs": "1.2.2", + "url2": "0.0.0" + }, + "dependencies": { + "qs": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-1.2.2.tgz", + "integrity": "sha1-GbV/8k3CqZzh+L32r82ln472H4g=", + "dev": true + } + } + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + } + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "readline2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "mute-stream": "0.0.5" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "1.3.3" + } + }, + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", + "dev": true + }, + "regex-cache": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz", + "integrity": "sha1-mxpsNdTQ3871cRrmUejp09cRQUU=", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3", + "is-primitive": "2.0.0" + } + }, + "remove-trailing-separator": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz", + "integrity": "sha1-abBi2XhyetFNxrVrpKt3L9jXBRE=", + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "dev": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.15", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-from-string": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", + "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "0.1.0", + "resolve-from": "1.0.1" + } + }, + "resolve": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz", + "integrity": "sha1-ZVkHw0aahoDcLeOidaj91paR8OU=", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "1.1.1", + "onetime": "1.1.0" + } + }, + "rfc6902": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfc6902/-/rfc6902-1.3.0.tgz", + "integrity": "sha1-hbLGnELc8RYIJDe5gpqWJEa0xKU=", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "run-async": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "dev": true, + "requires": { + "once": "1.4.0" + } + }, + "rx-lite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", + "dev": true + }, + "rxjs": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.4.2.tgz", + "integrity": "sha1-KjI2/L8D31e64G/Wly/ZnlwI/Pc=", + "dev": true, + "requires": { + "symbol-observable": "1.0.4" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + }, + "sane": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-1.6.0.tgz", + "integrity": "sha1-lhDEUjB6E10pwf3+JUcDQYDEZ3U=", + "dev": true, + "requires": { + "anymatch": "1.3.0", + "exec-sh": "0.2.0", + "fb-watchman": "1.9.2", + "minimatch": "3.0.4", + "minimist": "1.2.0", + "walker": "1.0.7", + "watch": "0.10.0" + }, + "dependencies": { + "bser": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bser/-/bser-1.0.2.tgz", + "integrity": "sha1-OBEWlwsqbe6lZG3RXdcnhES1YWk=", + "dev": true, + "requires": { + "node-int64": "0.4.0" + } + }, + "fb-watchman": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-1.9.2.tgz", + "integrity": "sha1-okz0eCf4LTj7Waaa1wt247auc4M=", + "dev": true, + "requires": { + "bser": "1.0.2" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "semver": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz", + "integrity": "sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no=", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shelljs": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", + "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", + "dev": true, + "requires": { + "glob": "7.1.2", + "interpret": "1.0.3", + "rechoir": "0.6.2" + } + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": "1.0.1" + } + }, + "source-map-support": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz", + "integrity": "sha1-AyAt9lwG0r2MfsI2KhkwVv7407E=", + "dev": true, + "requires": { + "source-map": "0.5.6" + }, + "dependencies": { + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", + "dev": true + } + } + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "dev": true, + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", + "dev": true + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dev": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "staged-git-files": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/staged-git-files/-/staged-git-files-0.0.4.tgz", + "integrity": "sha1-15fhtVHKemOd7AI33G60u5vhfTU=", + "dev": true + }, + "stream-consume": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.0.tgz", + "integrity": "sha1-pB6tGm1ggc63n2WwYZAbbY89HQ8=", + "dev": true + }, + "stream-to-observable": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stream-to-observable/-/stream-to-observable-0.1.0.tgz", + "integrity": "sha1-Rb8dny19wJvtgfHDB8Qw5ouEz/4=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "symbol-observable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz", + "integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0=", + "dev": true + }, + "symbol-tree": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", + "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", + "dev": true + }, + "table": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "dev": true, + "requires": { + "ajv": "4.11.8", + "ajv-keywords": "1.5.1", + "chalk": "1.1.3", + "lodash": "4.17.4", + "slice-ansi": "0.0.4", + "string-width": "2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "test-exclude": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.1.1.tgz", + "integrity": "sha512-35+Asrsk3XHJDBgf/VRFexPgh3UyETv8IAn/LRTiZjVy6rjPVqdEk8dJcJYBzl1w0XCJM48lvTy8SfEsCWS4nA==", + "dev": true, + "requires": { + "arrify": "1.0.1", + "micromatch": "2.3.11", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "require-main-filename": "1.0.1" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "tough-cookie": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", + "dev": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "tryit": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", + "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "0.5.6", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + } + }, + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", + "dev": true, + "optional": true + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true, + "optional": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "url2": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/url2/-/url2-0.0.0.tgz", + "integrity": "sha1-Tqq9HVw6yQ1iq0SFyZhCKGWgSxo=", + "dev": true + }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true, + "requires": { + "os-homedir": "1.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "dev": true, + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "verror": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", + "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "dev": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "voca": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/voca/-/voca-1.3.0.tgz", + "integrity": "sha1-AnUayDm/DJLiz+iOScOTyU3VCsM=", + "dev": true + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "dev": true, + "requires": { + "makeerror": "1.0.11" + } + }, + "watch": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/watch/-/watch-0.10.0.tgz", + "integrity": "sha1-d3mLLaD5kQ1ZXxrOWwwiWFIfIdw=", + "dev": true + }, + "weak-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.0.tgz", + "integrity": "sha1-tm5Wqd8L0lp2u/G1FNsSkIBhSjc=", + "dev": true + }, + "webidl-conversions": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.1.tgz", + "integrity": "sha1-gBWherg+fhsxFjhIas6B2mziBqA=", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.1.tgz", + "integrity": "sha1-PGxFGhmO567FWx7GHQkgxngBpfQ=", + "dev": true, + "requires": { + "iconv-lite": "0.4.13" + } + }, + "whatwg-url": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-4.8.0.tgz", + "integrity": "sha1-0pgaqRSMHgCkHFphMRZqtGg7vMA=", + "dev": true, + "requires": { + "tr46": "0.0.3", + "webidl-conversions": "3.0.1" + }, + "dependencies": { + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true + } + } + }, + "which": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", + "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true + }, + "window-size": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", + "integrity": "sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=", + "dev": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "worker-farm": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.4.1.tgz", + "integrity": "sha512-tgFAtgOYLPutkAyzgpS6VJFL5HY+0ui1Tvua+fITgz8ByaJTMFGtazR6xxQfwfiAcbwE+2fLG/K49wc2TfwCNw==", + "dev": true, + "requires": { + "errno": "0.1.4", + "xtend": "4.0.1" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "0.5.1" + } + }, + "xml-name-validator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", + "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "dev": true, + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "y18n": "3.2.1", + "yargs-parser": "5.0.0" + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "dev": true, + "requires": { + "camelcase": "3.0.0" + } + } + } +} diff --git a/package.json b/package.json index b9b1a2bc..6597d49e 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,17 @@ { "name": "mithril", - "version": "1.1.1", + "version": "1.1.3", "description": "A framework for building brilliant applications", "author": "Leo Horie", "license": "MIT", "main": "mithril.js", - "repository": "lhorie/mithril.js", + "repository": "MithrilJS/mithril.js", "scripts": { - "dev": "node bundler/cli browser.js -o mithril.js -w", + "dev": "node bundler/cli browser.js -output mithril.js -watch", "build": "npm run build-browser & npm run build-min", - "build-browser": "node bundler/cli browser.js -o mithril.js", - "build-min": "node bundler/cli browser.js -o mithril.min.js -m", + "build-browser": "node bundler/cli browser.js -output mithril.js", + "build-min": "node bundler/cli browser.js -output mithril.min.js -minify", + "precommit": "lint-staged", "lintdocs": "node docs/lint", "gendocs": "node docs/generate", "lint": "eslint . || true", @@ -25,13 +26,26 @@ "postversion": "git push --follow-tags" }, "devDependencies": { + "@alrra/travis-scripts": "^3.0.1", "benchmark": "^2.1.4", + "danger": "^1.2.0", + "dedent": "^0.7.0", "eslint": "^3.19.0", + "gh-pages": "^0.12.0", "istanbul": "^0.4.5", - "marked": "^0.3.6" + "lint-staged": "^4.0.4", + "locater": "^1.3.0", + "marked": "^0.3.6", + "pinpoint": "^1.1.0" }, "bin": { - "ospec": "./ospec/bin/ospec", - "bundle": "./bundler/bin/bundle" - } + "ospec": "./ospec/bin/ospec" + }, + "lint-staged": { + "*.js": [ + "eslint . --fix", + "git add" + ] + }, + "dependencies": {} } diff --git a/performance/test-perf.js b/performance/test-perf.js index 2ba9b622..a2b79421 100644 --- a/performance/test-perf.js +++ b/performance/test-perf.js @@ -70,7 +70,7 @@ suite.on("complete", function() { suite.on("error", console.error.bind(console)) suite.add({ - name : "rerender without changes", + name : "rerender identical vnode", onStart : function() { this.vdom = m("div", {class: "foo bar", "data-foo": "bar", p: 2}, m("header", @@ -119,6 +119,53 @@ suite.add({ } }) +suite.add({ + name : "rerender same tree", + fn : function() { + m.render(scratch, m("div", {class: "foo bar", "data-foo": "bar", p: 2}, + m("header", + m("h1", {class: "asdf"}, "a ", "b", " c ", 0, " d"), + m("nav", + m("a", {href: "/foo"}, "Foo"), + m("a", {href: "/bar"}, "Bar") + ) + ), + m("main", + m("form", {onSubmit: function onSubmit() {}}, + m("input", {type: "checkbox", checked: true}), + m("input", {type: "checkbox", checked: false}), + m("fieldset", + m("label", + m("input", {type: "radio", checked: true}) + ), + m("label", + m("input", {type: "radio"}) + ) + ), + m("button-bar", + m("button", + {style: "width:10px; height:10px; border:1px solid #FFF;"}, + "Normal CSS" + ), + m("button", + {style: "top:0 ; right: 20"}, + "Poor CSS" + ), + m("button", + {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true}, + "Poorer CSS" + ), + m("button", + {style: {margin: 0, padding: "10px", overflow: "visible"}}, + "Object CSS" + ) + ) + ) + ) + )) + } +}) + suite.add({ name : "construct large VDOM tree", @@ -311,16 +358,9 @@ var Root = { } } -suite.add({ - name : "repeated trees (recycling)", - fn : function () { - m.render(scratch, [m(Root)]) - m.render(scratch, []) - } -}) suite.add({ - name : "repeated trees (no recycling)", + name : "repeated trees", fn : function () { m.render(scratch, [m(Root)]) m.render(scratch, []) diff --git a/promise/polyfill.js b/promise/polyfill.js new file mode 100644 index 00000000..68cd60e1 --- /dev/null +++ b/promise/polyfill.js @@ -0,0 +1,112 @@ +"use strict" +/** @constructor */ +var PromisePolyfill = function(executor) { + if (!(this instanceof PromisePolyfill)) throw new Error("Promise must be called with `new`") + if (typeof executor !== "function") throw new TypeError("executor must be a function") + + var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false) + var instance = self._instance = {resolvers: resolvers, rejectors: rejectors} + var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout + function handler(list, shouldAbsorb) { + return function execute(value) { + var then + try { + if (shouldAbsorb && value != null && (typeof value === "object" || typeof value === "function") && typeof (then = value.then) === "function") { + if (value === self) throw new TypeError("Promise can't be resolved w/ itself") + executeOnce(then.bind(value)) + } + else { + callAsync(function() { + if (!shouldAbsorb && list.length === 0) console.error("Possible unhandled promise rejection:", value) + for (var i = 0; i < list.length; i++) list[i](value) + resolvers.length = 0, rejectors.length = 0 + instance.state = shouldAbsorb + instance.retry = function() {execute(value)} + }) + } + } + catch (e) { + rejectCurrent(e) + } + } + } + function executeOnce(then) { + var runs = 0 + function run(fn) { + return function(value) { + if (runs++ > 0) return + fn(value) + } + } + var onerror = run(rejectCurrent) + try {then(run(resolveCurrent), onerror)} catch (e) {onerror(e)} + } + + executeOnce(executor) +} +PromisePolyfill.prototype.then = function(onFulfilled, onRejection) { + var self = this, instance = self._instance + function handle(callback, list, next, state) { + list.push(function(value) { + if (typeof callback !== "function") next(value) + else try {resolveNext(callback(value))} catch (e) {if (rejectNext) rejectNext(e)} + }) + if (typeof instance.retry === "function" && state === instance.state) instance.retry() + } + var resolveNext, rejectNext + var promise = new PromisePolyfill(function(resolve, reject) {resolveNext = resolve, rejectNext = reject}) + handle(onFulfilled, instance.resolvers, resolveNext, true), handle(onRejection, instance.rejectors, rejectNext, false) + return promise +} +PromisePolyfill.prototype.catch = function(onRejection) { + return this.then(null, onRejection) +} +PromisePolyfill.prototype.finally = function(callback) { + return this.then( + function(value) { + return PromisePolyfill.resolve(callback()).then(function() { + return value + }) + }, + function(reason) { + return PromisePolyfill.resolve(callback()).then(function() { + return PromisePolyfill.reject(reason); + }) + } + ) +} +PromisePolyfill.resolve = function(value) { + if (value instanceof PromisePolyfill) return value + return new PromisePolyfill(function(resolve) {resolve(value)}) +} +PromisePolyfill.reject = function(value) { + return new PromisePolyfill(function(resolve, reject) {reject(value)}) +} +PromisePolyfill.all = function(list) { + return new PromisePolyfill(function(resolve, reject) { + var total = list.length, count = 0, values = [] + if (list.length === 0) resolve([]) + else for (var i = 0; i < list.length; i++) { + (function(i) { + function consume(value) { + count++ + values[i] = value + if (count === total) resolve(values) + } + if (list[i] != null && (typeof list[i] === "object" || typeof list[i] === "function") && typeof list[i].then === "function") { + list[i].then(consume, reject) + } + else consume(list[i]) + })(i) + } + }) +} +PromisePolyfill.race = function(list) { + return new PromisePolyfill(function(resolve, reject) { + for (var i = 0; i < list.length; i++) { + list[i].then(resolve, reject) + } + }) +} + +module.exports = PromisePolyfill diff --git a/promise/promise.js b/promise/promise.js index 53aefecc..7c6a2d11 100644 --- a/promise/promise.js +++ b/promise/promise.js @@ -1,105 +1,20 @@ "use strict" -/** @constructor */ -var PromisePolyfill = function(executor) { - if (!(this instanceof PromisePolyfill)) throw new Error("Promise must be called with `new`") - if (typeof executor !== "function") throw new TypeError("executor must be a function") - var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false) - var instance = self._instance = {resolvers: resolvers, rejectors: rejectors} - var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout - function handler(list, shouldAbsorb) { - return function execute(value) { - var then - try { - if (shouldAbsorb && value != null && (typeof value === "object" || typeof value === "function") && typeof (then = value.then) === "function") { - if (value === self) throw new TypeError("Promise can't be resolved w/ itself") - executeOnce(then.bind(value)) - } - else { - callAsync(function() { - if (!shouldAbsorb && list.length === 0) console.error("Possible unhandled promise rejection:", value) - for (var i = 0; i < list.length; i++) list[i](value) - resolvers.length = 0, rejectors.length = 0 - instance.state = shouldAbsorb - instance.retry = function() {execute(value)} - }) - } - } - catch (e) { - rejectCurrent(e) - } - } - } - function executeOnce(then) { - var runs = 0 - function run(fn) { - return function(value) { - if (runs++ > 0) return - fn(value) - } - } - var onerror = run(rejectCurrent) - try {then(run(resolveCurrent), onerror)} catch (e) {onerror(e)} - } - - executeOnce(executor) -} -PromisePolyfill.prototype.then = function(onFulfilled, onRejection) { - var self = this, instance = self._instance - function handle(callback, list, next, state) { - list.push(function(value) { - if (typeof callback !== "function") next(value) - else try {resolveNext(callback(value))} catch (e) {if (rejectNext) rejectNext(e)} - }) - if (typeof instance.retry === "function" && state === instance.state) instance.retry() - } - var resolveNext, rejectNext - var promise = new PromisePolyfill(function(resolve, reject) {resolveNext = resolve, rejectNext = reject}) - handle(onFulfilled, instance.resolvers, resolveNext, true), handle(onRejection, instance.rejectors, rejectNext, false) - return promise -} -PromisePolyfill.prototype.catch = function(onRejection) { - return this.then(null, onRejection) -} -PromisePolyfill.resolve = function(value) { - if (value instanceof PromisePolyfill) return value - return new PromisePolyfill(function(resolve) {resolve(value)}) -} -PromisePolyfill.reject = function(value) { - return new PromisePolyfill(function(resolve, reject) {reject(value)}) -} -PromisePolyfill.all = function(list) { - return new PromisePolyfill(function(resolve, reject) { - var total = list.length, count = 0, values = [] - if (list.length === 0) resolve([]) - else for (var i = 0; i < list.length; i++) { - (function(i) { - function consume(value) { - count++ - values[i] = value - if (count === total) resolve(values) - } - if (list[i] != null && (typeof list[i] === "object" || typeof list[i] === "function") && typeof list[i].then === "function") { - list[i].then(consume, reject) - } - else consume(list[i]) - })(i) - } - }) -} -PromisePolyfill.race = function(list) { - return new PromisePolyfill(function(resolve, reject) { - for (var i = 0; i < list.length; i++) { - list[i].then(resolve, reject) - } - }) -} +var PromisePolyfill = require("./polyfill") if (typeof window !== "undefined") { - if (typeof window.Promise === "undefined") window.Promise = PromisePolyfill + if (typeof window.Promise === "undefined") { + window.Promise = PromisePolyfill + } else if (!window.Promise.prototype.finally) { + window.Promise.prototype.finally = PromisePolyfill.prototype.finally + } module.exports = window.Promise } else if (typeof global !== "undefined") { - if (typeof global.Promise === "undefined") global.Promise = PromisePolyfill + if (typeof global.Promise === "undefined") { + global.Promise = PromisePolyfill + } else if (!global.Promise.prototype.finally) { + global.Promise.prototype.finally = PromisePolyfill.prototype.finally + } module.exports = global.Promise } else { module.exports = PromisePolyfill diff --git a/promise/tests/test-promise.js b/promise/tests/test-promise.js index 48a9fac3..7dcc21f6 100644 --- a/promise/tests/test-promise.js +++ b/promise/tests/test-promise.js @@ -2,7 +2,7 @@ var o = require("../../ospec/ospec") var callAsync = require("../../test-utils/callAsync") -var Promise = require("../../promise/promise") +var Promise = require("../../promise/polyfill") o.spec("promise", function() { o.spec("constructor", function() { @@ -15,6 +15,7 @@ o.spec("promise", function() { o("constructor has correct methods", function() { o(typeof Promise.prototype.then).equals("function") o(typeof Promise.prototype.catch).equals("function") + o(typeof Promise.prototype.finally).equals("function") o(typeof Promise.resolve).equals("function") o(typeof Promise.reject).equals("function") o(typeof Promise.race).equals("function") @@ -53,6 +54,78 @@ o.spec("promise", function() { o(value).equals(1) }).then(done) }) + o("finally lets a fulfilled value pass though", function(done) { + var promise = Promise.resolve(1) + var spy = o.spy(function(){return 2}) + + promise.finally(spy).then(function(value){ + o(value).equals(1) + o(spy.callCount).equals(1) + o(spy.args.length).equals(0) + o(spy.this).equals(undefined) + done() + }) + }) + o("finally lets a rejected reason pass though", function(done) { + var promise = Promise.reject(1) + var spy = o.spy(function(){return 2}) + + promise.finally(spy).catch(function(reason){ + o(reason).equals(1) + o(spy.callCount).equals(1) + o(spy.args.length).equals(0) + o(spy.this).equals(undefined) + done() + }) + }) + o("finally overrrides a fulfilled value when it throws", function(done) { + var promise = Promise.resolve(1) + var spy = o.spy(function(){throw 2}) + + promise.finally(spy).catch(function(reason){ + o(reason).equals(2) + o(spy.callCount).equals(1) + o(spy.args.length).equals(0) + o(spy.this).equals(undefined) + done() + }) + }) + o("finally overrrides a fulfilled value when it returns a rejected Promise", function(done) { + var promise = Promise.resolve(1) + var spy = o.spy(function(){return Promise.reject(2)}) + + promise.finally(spy).catch(function(reason){ + o(reason).equals(2) + o(spy.callCount).equals(1) + o(spy.args.length).equals(0) + o(spy.this).equals(undefined) + done() + }) + }) + o("finally overrrides a rejected reason when it throws", function(done) { + var promise = Promise.reject(1) + var spy = o.spy(function(){throw 2}) + + promise.finally(spy).catch(function(reason){ + o(reason).equals(2) + o(spy.callCount).equals(1) + o(spy.args.length).equals(0) + o(spy.this).equals(undefined) + done() + }) + }) + o("finally overrrides a rejected reason when it returns a rejected Promise", function(done) { + var promise = Promise.reject(1) + var spy = o.spy(function(){return Promise.reject(2)}) + + promise.finally(spy).catch(function(reason){ + o(reason).equals(2) + o(spy.callCount).equals(1) + o(spy.args.length).equals(0) + o(spy.this).equals(undefined) + done() + }) + }) }) o.spec("resolve", function() { o("resolves once", function(done) { @@ -131,7 +204,7 @@ o.spec("promise", function() { callAsync(function() {resolve(promise)}) }) - promise.then(null, done) + promise.then(null, () => done()) }) o("non-function onFulfilled is ignored", function(done) { var promise = Promise.resolve(1) diff --git a/render/hyperscript.js b/render/hyperscript.js index 3d4b21e4..00c438c9 100644 --- a/render/hyperscript.js +++ b/render/hyperscript.js @@ -6,6 +6,11 @@ var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\[" var selectorCache = {} var hasOwn = {}.hasOwnProperty +function isEmpty(object) { + for (var key in object) if (hasOwn.call(object, key)) return false + return true +} + function compileSelector(selector) { var match, tag = "div", classes = [], attrs = {} while (match = selectorParser.exec(selector)) { @@ -17,7 +22,7 @@ function compileSelector(selector) { var attrValue = match[6] if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, "$1").replace(/\\\\/g, "\\") if (match[4] === "class") classes.push(attrValue) - else attrs[match[4]] = attrValue || true + else attrs[match[4]] = attrValue === "" ? attrValue : attrValue || true } } if (classes.length > 0) attrs.className = classes.join(" ") @@ -28,6 +33,18 @@ function execSelector(state, attrs, children) { var hasAttrs = false, childList, text var className = attrs.className || attrs.class + if (!isEmpty(state.attrs) && !isEmpty(attrs)) { + var newAttrs = {} + + for(var key in attrs) { + if (hasOwn.call(attrs, key)) { + newAttrs[key] = attrs[key] + } + } + + attrs = newAttrs + } + for (var key in state.attrs) { if (hasOwn.call(state.attrs, key)) { attrs[key] = state.attrs[key] diff --git a/render/render.js b/render/render.js index df26a1df..b9b6411a 100644 --- a/render/render.js +++ b/render/render.js @@ -6,9 +6,36 @@ module.exports = function($window) { var $doc = $window.document var $emptyFragment = $doc.createDocumentFragment() + var nameSpace = { + svg: "http://www.w3.org/2000/svg", + math: "http://www.w3.org/1998/Math/MathML" + } + var onevent function setEventCallback(callback) {return onevent = callback} + function getNameSpace(vnode) { + return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag] + } + + //sanity check to discourage people from doing `vnode.state = ...` + function checkState(vnode, original) { + if (vnode.state !== original) throw new Error("`vnode.state` must not be modified") + } + + //Note: the hook is passed as the `this` argument to allow proxying the + //arguments without requiring a full array allocation to do so. It also + //takes advantage of the fact the current `vnode` is the first argument in + //all lifecycle methods. + function callHook(vnode) { + var original = vnode.state + try { + return this.apply(original, arguments) + } finally { + checkState(vnode, original) + } + } + //create function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) { for (var i = start; i < end; i++) { @@ -24,25 +51,33 @@ module.exports = function($window) { vnode.state = {} if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) switch (tag) { - case "#": return createText(parent, vnode, nextSibling) - case "<": return createHTML(parent, vnode, nextSibling) - case "[": return createFragment(parent, vnode, hooks, ns, nextSibling) - default: return createElement(parent, vnode, hooks, ns, nextSibling) + case "#": createText(parent, vnode, nextSibling); break + case "<": createHTML(parent, vnode, ns, nextSibling); break + case "[": createFragment(parent, vnode, hooks, ns, nextSibling); break + default: createElement(parent, vnode, hooks, ns, nextSibling) } } - else return createComponent(parent, vnode, hooks, ns, nextSibling) + else createComponent(parent, vnode, hooks, ns, nextSibling) } function createText(parent, vnode, nextSibling) { vnode.dom = $doc.createTextNode(vnode.children) insertNode(parent, vnode.dom, nextSibling) - return vnode.dom } - function createHTML(parent, vnode, nextSibling) { + var possibleParents = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"} + function createHTML(parent, vnode, ns, nextSibling) { var match = vnode.children.match(/^\s*?<(\w+)/im) || [] - var parent1 = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"}[match[1]] || "div" - var temp = $doc.createElement(parent1) - - temp.innerHTML = vnode.children + // not using the proper parent makes the child element(s) vanish. + // var div = document.createElement("div") + // div.innerHTML = "ij" + // console.log(div.innerHTML) + // --> "ij", no in sight. + var temp = $doc.createElement(possibleParents[match[1]] || "div") + if (ns === "http://www.w3.org/2000/svg") { + temp.innerHTML = "" + vnode.children + "" + temp = temp.firstChild + } else { + temp.innerHTML = vnode.children + } vnode.dom = temp.firstChild vnode.domSize = temp.childNodes.length var fragment = $doc.createDocumentFragment() @@ -51,7 +86,6 @@ module.exports = function($window) { fragment.appendChild(child) } insertNode(parent, fragment, nextSibling) - return fragment } function createFragment(parent, vnode, hooks, ns, nextSibling) { var fragment = $doc.createDocumentFragment() @@ -62,18 +96,14 @@ module.exports = function($window) { vnode.dom = fragment.firstChild vnode.domSize = fragment.childNodes.length insertNode(parent, fragment, nextSibling) - return fragment } function createElement(parent, vnode, hooks, ns, nextSibling) { var tag = vnode.tag - switch (vnode.tag) { - case "svg": ns = "http://www.w3.org/2000/svg"; break - case "math": ns = "http://www.w3.org/1998/Math/MathML"; break - } - var attrs = vnode.attrs var is = attrs && attrs.is + ns = getNameSpace(vnode) || ns + var element = ns ? is ? $doc.createElementNS(ns, tag, {is: is}) : $doc.createElementNS(ns, tag) : is ? $doc.createElement(tag, {is: is}) : $doc.createElement(tag) @@ -99,7 +129,6 @@ module.exports = function($window) { setLateAttrs(vnode) } } - return element } function initComponent(vnode, hooks) { var sentinel @@ -115,147 +144,250 @@ module.exports = function($window) { sentinel.$$reentrantLock$$ = true vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode) } - vnode._state = vnode.state if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks) - initLifecycle(vnode._state, vnode, hooks) - vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode)) + initLifecycle(vnode.state, vnode, hooks) + vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") sentinel.$$reentrantLock$$ = null } function createComponent(parent, vnode, hooks, ns, nextSibling) { initComponent(vnode, hooks) if (vnode.instance != null) { - var element = createNode(parent, vnode.instance, hooks, ns, nextSibling) + createNode(parent, vnode.instance, hooks, ns, nextSibling) vnode.dom = vnode.instance.dom vnode.domSize = vnode.dom != null ? vnode.instance.domSize : 0 - insertNode(parent, element, nextSibling) - return element } else { vnode.domSize = 0 - return $emptyFragment } } //update - function updateNodes(parent, old, vnodes, recycling, hooks, nextSibling, ns) { - if (old === vnodes || old == null && vnodes == null) return - else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, undefined) - else if (vnodes == null) removeNodes(old, 0, old.length, vnodes) - else { - if (old.length === vnodes.length) { - var isUnkeyed = false - for (var i = 0; i < vnodes.length; i++) { - if (vnodes[i] != null && old[i] != null) { - isUnkeyed = vnodes[i].key == null && old[i].key == null - break - } - } - if (isUnkeyed) { - for (var i = 0; i < old.length; i++) { - if (old[i] === vnodes[i]) continue - else if (old[i] == null && vnodes[i] != null) createNode(parent, vnodes[i], hooks, ns, getNextSibling(old, i + 1, nextSibling)) - else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes) - else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), recycling, ns) - } - return - } - } - recycling = recycling || isRecyclable(old, vnodes) - if (recycling) { - var pool = old.pool - old = old.concat(old.pool) - } + /** + * @param {Element|Fragment} parent - the parent element + * @param {Vnode[] | null} old - the list of vnodes of the last `render()` call for + * this part of the tree + * @param {Vnode[] | null} vnodes - as above, but for the current `render()` call. + * @param {Function[]} hooks - an accumulator of post-render hooks (oncreate/onupdate) + * @param {Element | null} nextSibling - the next DOM node if we're dealing with a + * fragment that is not the last item in its + * parent + * @param {'svg' | 'math' | String | null} ns) - the current XML namespace, if any + * @returns void + */ + // This function diffs and patches lists of vnodes, both keyed and unkeyed. + // + // We will: + // + // 1. describe its general structure + // 2. focus on the diff algorithm optimizations + // 3. discuss DOM node operations. + + // ## Overview: + // + // The updateNodes() function: + // - deals with trivial cases + // - determines whether the lists are keyed or unkeyed based on the first non-null node + // of each list. + // - diffs them and patches the DOM if needed (that's the brunt of the code) + // - manages the leftovers: after diffing, are there: + // - old nodes left to remove? + // - new nodes to insert? + // deal with them! + // + // The lists are only iterated over once, with an exception for the nodes in `old` that + // are visited in the fourth part of the diff and in the `removeNodes` loop. + + // ## Diffing + // + // If one list is keyed and the other is unkeyed, the old is removed, and the new one is + // inserted (since the keys are guaranteed to differ). + // + // Then comes the unkeyed diff algo, and at last, the keyed diff algorithm that is split + // in four parts (simplifying a bit). + // + // The first part goes through both lists top-down as long as the nodes at each level have + // the same key. + // + // The second part deals with lists reversals, and traverses one list top-down and the other + // bottom-up (as long as the keys match). + // + // The third part goes through both lists bottom up as long as the keys match. + // + // The first and third sections allow us to deal efficiently with situations where one or + // more contiguous nodes were either inserted into, removed from or re-ordered in an otherwise + // sorted list. They may reduce the number of nodes to be processed in the fourth section. + // + // The fourth section does keyed diff for the situations not covered by the other three. It + // builds a {key: oldIndex} dictionary and uses it to find old nodes that match the keys of + // new ones. + // The nodes from the `old` array that have a match in the new `vnodes` one are marked as + // `vnode.skip: true`. + // + // If there are still nodes in the new `vnodes` array that haven't been matched to old ones, + // they are created. + // The range of old nodes that wasn't covered by the first three sections is passed to + // `removeNodes()`. Those nodes are removed unless marked as `.skip: true`. + // + // It should be noted that the description of the four sections above is not perfect, because those + // parts are actually implemented as only two loops, one for the first two parts, and one for + // the other two. I'm not sure it wins us anything except maybe a few bytes of file size. + + // ## Finding the next sibling. + // + // `updateNode()` and `createNode()` expect a nextSibling parameter to perform DOM operations. + // When the list is being traversed top-down, at any index, the DOM nodes up to the previous + // vnode reflect the content of the new list, whereas the rest of the DOM nodes reflect the old + // list. The next sibling must be looked for in the old list using `getNextSibling(... oldStart + 1 ...)`. + // + // In the other scenarios (swaps, upwards traversal, map-based diff), + // the new vnodes list is traversed upwards. The DOM nodes at the bottom of the list reflect the + // bottom part of the new vnodes list, and we can use the `v.dom` value of the previous node + // as the next sibling (cached in the `nextSibling` variable). + + + // ## DOM node moves + // + // In most scenarios `updateNode()` and `createNode()` perform the DOM operations. However, + // this is not the case if the node moved (second and fourth part of the diff algo). We move + // the old DOM nodes before updateNode runs because it enables us to use the cached `nextSibling` + // variable rather than fetching it using `getNextSibling()`. + // + // The fourth part of the diff currently inserts nodes unconditionally, leading to issues + // like #1791 and #1999. We need to be smarter about those situations where adjascent old + // nodes remain together in the new list in a way that isn't covered by parts one and + // three of the diff algo. + + function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { + if (old === vnodes || old == null && vnodes == null) return + else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns) + else if (vnodes == null) removeNodes(old, 0, old.length) + else { + // default to keyed because, when either list is full of null nodes, it has fewer branches + var start = 0, oldStart = 0, isOldKeyed = true, isKeyed = true + for (; oldStart < old.length; oldStart++) { + if (old[oldStart] != null) { + isOldKeyed = old[oldStart].key != null + break + } + } + for (; start < vnodes.length; start++) { + if (vnodes[start] != null) { + isKeyed = vnodes[start].key != null + break + } + } + if (isOldKeyed !== isKeyed) { + removeNodes(old, oldStart, old.length) + createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + return + } + if (!isKeyed) { + // Don't index past the end of either list (causes deopts). + var commonLength = old.length < vnodes.length ? old.length : vnodes.length + // Rewind if necessary to the first non-null index on either side. + // We could alternatively either explicitly create or remove nodes when `start !== oldStart` + // but that would be optimizing for sparse lists which are more rare than dense ones. + start = start < oldStart ? start : oldStart + for (; start < commonLength; start++) { + o = old[start] + v = vnodes[start] + if (o === v || o == null && v == null) continue + else if (o == null) createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling)) + else if (v == null) removeNode(o) + else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns) + } + if (old.length > commonLength) removeNodes(old, start, old.length) + if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns) + return + } + // keyed diff + var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v - var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map while (oldEnd >= oldStart && end >= start) { - var o = old[oldStart], v = vnodes[start] - if (o === v && !recycling) oldStart++, start++ - else if (o == null) oldStart++ + // both top-down + o = old[oldStart] + v = vnodes[start] + if (o == null) oldStart++ else if (v == null) start++ else if (o.key === v.key) { - var shouldRecycle = (pool != null && oldStart >= old.length - pool.length) || ((pool == null) && recycling) oldStart++, start++ - updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), shouldRecycle, ns) - if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) - } - else { - var o = old[oldEnd] - if (o === v && !recycling) oldEnd--, start++ - else if (o == null) oldEnd-- - else if (v == null) start++ + if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns) + } else { + // reversal: old top-down, new bottom-up + v = vnodes[end] + if (o == null) oldStart++ + else if (v == null) end-- else if (o.key === v.key) { - var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling) - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) - if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling)) - oldEnd--, start++ + oldStart++ + if (start < end--) insertNode(parent, toFragment(o), nextSibling) + if (o !== v) updateNode(parent, o, v, hooks, nextSibling, ns) + if (v.dom != null) nextSibling = v.dom } else break } } while (oldEnd >= oldStart && end >= start) { - var o = old[oldEnd], v = vnodes[end] - if (o === v && !recycling) oldEnd--, end-- - else if (o == null) oldEnd-- + // both bottom-up + o = old[oldEnd] + v = vnodes[end] + if (o == null) oldEnd-- else if (v == null) end-- else if (o.key === v.key) { - var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling) - updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns) - if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) - if (o.dom != null) nextSibling = o.dom + if (o !== v) updateNode(parent, o, v, hooks, nextSibling, ns) + if (v.dom != null) nextSibling = v.dom oldEnd--, end-- - } - else { - if (!map) map = getKeyMap(old, oldEnd) + } else { + // old map-based, new bottom-up + if (map == null) { + // the last node can be left out of the map because it will be caught by the + // bottom-up part of the diff loop. If we were to refactor this to use distinct + // loops, we'd have to pass `oldEnd + 1` (or change `start < end` to `<=` in getKeyMap). + map = getKeyMap(old, oldStart, oldEnd) + } if (v != null) { var oldIndex = map[v.key] - if (oldIndex != null) { - var movable = old[oldIndex] - var shouldRecycle = (pool != null && oldIndex >= old.length - pool.length) || ((pool == null) && recycling) - updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) - insertNode(parent, toFragment(movable), nextSibling) - old[oldIndex].skip = true - if (movable.dom != null) nextSibling = movable.dom - } - else { - var dom = createNode(parent, v, hooks, undefined, nextSibling) - nextSibling = dom + if (oldIndex == null) { + createNode(parent, v, hooks, ns, nextSibling) + if (v.dom != null) nextSibling = v.dom + } else { + o = old[oldIndex] + insertNode(parent, toFragment(o), nextSibling) + if (o !== v) updateNode(parent, o, v, hooks, nextSibling, ns) + o.skip = true + if (v.dom != null) nextSibling = v.dom } } end-- } if (end < start) break } + // deal with the leftovers. createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns) - removeNodes(old, oldStart, oldEnd + 1, vnodes) + removeNodes(old, oldStart, oldEnd + 1) } } - function updateNode(parent, old, vnode, hooks, nextSibling, recycling, ns) { + function updateNode(parent, old, vnode, hooks, nextSibling, ns) { var oldTag = old.tag, tag = vnode.tag if (oldTag === tag) { vnode.state = old.state - vnode._state = old._state vnode.events = old.events - if (!recycling && shouldNotUpdate(vnode, old)) return + if (shouldNotUpdate(vnode, old)) return if (typeof oldTag === "string") { if (vnode.attrs != null) { - if (recycling) { - vnode.state = {} - initLifecycle(vnode.attrs, vnode, hooks) - } - else updateLifecycle(vnode.attrs, vnode, hooks) + updateLifecycle(vnode.attrs, vnode, hooks) } switch (oldTag) { case "#": updateText(old, vnode); break - case "<": updateHTML(parent, old, vnode, nextSibling); break - case "[": updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns); break - default: updateElement(old, vnode, recycling, hooks, ns) + case "<": updateHTML(parent, old, vnode, ns, nextSibling); break + case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns); break + default: updateElement(old, vnode, hooks, ns) } } - else updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) + else updateComponent(parent, old, vnode, hooks, nextSibling, ns) } else { - removeNode(old, null) + removeNode(old) createNode(parent, vnode, hooks, ns, nextSibling) } } @@ -265,15 +397,15 @@ module.exports = function($window) { } vnode.dom = old.dom } - function updateHTML(parent, old, vnode, nextSibling) { + function updateHTML(parent, old, vnode, ns, nextSibling) { if (old.children !== vnode.children) { toFragment(old) - createHTML(parent, vnode, nextSibling) + createHTML(parent, vnode, ns, nextSibling) } else vnode.dom = old.dom, vnode.domSize = old.domSize } - function updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns) { - updateNodes(parent, old.children, vnode.children, recycling, hooks, nextSibling, ns) + function updateFragment(parent, old, vnode, hooks, nextSibling, ns) { + updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns) var domSize = 0, children = vnode.children vnode.dom = null if (children != null) { @@ -287,12 +419,10 @@ module.exports = function($window) { if (domSize !== 1) vnode.domSize = domSize } } - function updateElement(old, vnode, recycling, hooks, ns) { + function updateElement(old, vnode, hooks, ns) { var element = vnode.dom = old.dom - switch (vnode.tag) { - case "svg": ns = "http://www.w3.org/2000/svg"; break - case "math": ns = "http://www.w3.org/1998/Math/MathML"; break - } + ns = getNameSpace(vnode) || ns + if (vnode.tag === "textarea") { if (vnode.attrs == null) vnode.attrs = {} if (vnode.text != null) { @@ -310,26 +440,22 @@ module.exports = function($window) { else { if (old.text != null) old.children = [Vnode("#", undefined, undefined, old.text, undefined, old.dom.firstChild)] if (vnode.text != null) vnode.children = [Vnode("#", undefined, undefined, vnode.text, undefined, undefined)] - updateNodes(element, old.children, vnode.children, recycling, hooks, null, ns) + updateNodes(element, old.children, vnode.children, hooks, null, ns) } } - function updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) { - if (recycling) { - initComponent(vnode, hooks) - } else { - vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode)) - if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") - if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) - updateLifecycle(vnode._state, vnode, hooks) - } + function updateComponent(parent, old, vnode, hooks, nextSibling, ns) { + vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode)) + if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument") + if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks) + updateLifecycle(vnode.state, vnode, hooks) if (vnode.instance != null) { if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling) - else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, recycling, ns) + else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns) vnode.dom = vnode.instance.dom vnode.domSize = vnode.instance.domSize } else if (old.instance != null) { - removeNode(old.instance, null) + removeNode(old.instance) vnode.dom = undefined vnode.domSize = 0 } @@ -338,24 +464,13 @@ module.exports = function($window) { vnode.domSize = old.domSize } } - function isRecyclable(old, vnodes) { - if (old.pool != null && Math.abs(old.pool.length - vnodes.length) <= Math.abs(old.length - vnodes.length)) { - var oldChildrenLength = old[0] && old[0].children && old[0].children.length || 0 - var poolChildrenLength = old.pool[0] && old.pool[0].children && old.pool[0].children.length || 0 - var vnodesChildrenLength = vnodes[0] && vnodes[0].children && vnodes[0].children.length || 0 - if (Math.abs(poolChildrenLength - vnodesChildrenLength) <= Math.abs(oldChildrenLength - vnodesChildrenLength)) { - return true - } - } - return false - } - function getKeyMap(vnodes, end) { - var map = {}, i = 0 - for (var i = 0; i < end; i++) { - var vnode = vnodes[i] + function getKeyMap(vnodes, start, end) { + var map = {} + for (; start < end; start++) { + var vnode = vnodes[start] if (vnode != null) { var key = vnode.key - if (key != null) map[key] = i + if (key != null) map[key] = start } } return map @@ -381,7 +496,7 @@ module.exports = function($window) { } function insertNode(parent, dom, nextSibling) { - if (nextSibling && nextSibling.parentNode) parent.insertBefore(dom, nextSibling) + if (nextSibling != null) parent.insertBefore(dom, nextSibling) else parent.appendChild(dom) } @@ -395,26 +510,27 @@ module.exports = function($window) { } //remove - function removeNodes(vnodes, start, end, context) { + function removeNodes(vnodes, start, end) { for (var i = start; i < end; i++) { var vnode = vnodes[i] if (vnode != null) { if (vnode.skip) vnode.skip = false - else removeNode(vnode, context) + else removeNode(vnode) } } } - function removeNode(vnode, context) { + function removeNode(vnode) { var expected = 1, called = 0 + var original = vnode.state if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") { - var result = vnode.attrs.onbeforeremove.call(vnode.state, vnode) + var result = callHook.call(vnode.attrs.onbeforeremove, vnode) if (result != null && typeof result.then === "function") { expected++ result.then(continuation, continuation) } } - if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeremove === "function") { - var result = vnode._state.onbeforeremove.call(vnode.state, vnode) + if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") { + var result = callHook.call(vnode.state.onbeforeremove, vnode) if (result != null && typeof result.then === "function") { expected++ result.then(continuation, continuation) @@ -423,6 +539,7 @@ module.exports = function($window) { continuation() function continuation() { if (++called === expected) { + checkState(vnode, original) onremove(vnode) if (vnode.dom) { var count = vnode.domSize || 1 @@ -433,10 +550,6 @@ module.exports = function($window) { } } removeNodeFromDOM(vnode.dom) - if (context != null && vnode.domSize == null && !hasIntegrationMethods(vnode.attrs) && typeof vnode.tag === "string") { //TODO test custom elements - if (!context.pool) context.pool = [vnode] - else context.pool.push(vnode) - } } } } @@ -446,10 +559,11 @@ module.exports = function($window) { if (parent != null) parent.removeChild(node) } function onremove(vnode) { - if (vnode.attrs && typeof vnode.attrs.onremove === "function") vnode.attrs.onremove.call(vnode.state, vnode) - if (typeof vnode.tag !== "string" && typeof vnode._state.onremove === "function") vnode._state.onremove.call(vnode.state, vnode) - if (vnode.instance != null) onremove(vnode.instance) - else { + if (vnode.attrs && typeof vnode.attrs.onremove === "function") callHook.call(vnode.attrs.onremove, vnode) + if (typeof vnode.tag !== "string") { + if (typeof vnode.state.onremove === "function") callHook.call(vnode.state.onremove, vnode) + if (vnode.instance != null) onremove(vnode.instance) + } else { var children = vnode.children if (Array.isArray(children)) { for (var i = 0; i < children.length; i++) { @@ -467,21 +581,32 @@ module.exports = function($window) { } } function setAttr(vnode, key, old, value, ns) { - var element = vnode.dom - if (key === "key" || key === "is" || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key)) return - var nsLastIndex = key.indexOf(":") - if (nsLastIndex > -1 && key.substr(0, nsLastIndex) === "xlink") { - element.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(nsLastIndex + 1), value) + if (key === "key" || key === "is" || isLifecycleMethod(key)) return + if (key[0] === "o" && key[1] === "n") return updateEvent(vnode, key, value) + if (typeof value === "undefined" && key === "value" && old !== value) { + vnode.dom.value = "" + return } - else if (key[0] === "o" && key[1] === "n" && typeof value === "function") updateEvent(vnode, key, value) + if ((old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || value === undefined) return + var element = vnode.dom + if (key.slice(0, 6) === "xlink:") element.setAttributeNS("http://www.w3.org/1999/xlink", key, value) else if (key === "style") updateStyle(element, old, value) else if (key in element && !isAttribute(key) && ns === undefined && !isCustomElement(vnode)) { - //setting input[value] to same value by typing on focused element moves cursor to end in Chrome - if (vnode.tag === "input" && key === "value" && vnode.dom.value == value && vnode.dom === $doc.activeElement) return - //setting select[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "select" && key === "value" && vnode.dom.value == value && vnode.dom === $doc.activeElement) return - //setting option[value] to same value while having select open blinks select dropdown in Chrome - if (vnode.tag === "option" && key === "value" && vnode.dom.value == value) return + if (key === "value") { + var normalized = "" + value // eslint-disable-line no-implicit-coercion + //setting input[value] to same value by typing on focused element moves cursor to end in Chrome + if ((vnode.tag === "input" || vnode.tag === "textarea") && vnode.dom.value === normalized && vnode.dom === $doc.activeElement) return + //setting select[value] to same value while having select open blinks select dropdown in Chrome + if (vnode.tag === "select") { + if (value === null) { + if (vnode.dom.selectedIndex === -1 && vnode.dom === $doc.activeElement) return + } else { + if (old !== null && vnode.dom.value === normalized && vnode.dom === $doc.activeElement) return + } + } + //setting option[value] to same value while having select open blinks select dropdown in Chrome + if (vnode.tag === "option" && old != null && vnode.dom.value === normalized) return + } // If you assign an input type that is not supported by IE 11 with an assignment expression, an error will occur. if (vnode.tag === "input" && key === "type") { element.setAttribute(key, value) @@ -521,7 +646,7 @@ module.exports = function($window) { } } function isFormAttribute(vnode, attr) { - return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement + return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement || vnode.tag === "option" && vnode.dom.parentNode === $doc.activeElement } function isLifecycleMethod(attr) { return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate" @@ -532,12 +657,21 @@ module.exports = function($window) { function isCustomElement(vnode){ return vnode.attrs.is || vnode.tag.indexOf("-") > -1 } - function hasIntegrationMethods(source) { - return source != null && (source.oncreate || source.onupdate || source.onbeforeremove || source.onremove) - } //style function updateStyle(element, old, style) { + if (old != null && style != null && typeof old === "object" && typeof style === "object" && style !== old) { + // Both old & new are (different) objects. + // Update style properties that have changed + for (var key in style) { + if (style[key] !== old[key]) element.style[key] = style[key] + } + // Remove style properties that no longer exist + for (var key in old) { + if (!(key in style)) element.style[key] = "" + } + return + } if (old === style) element.style.cssText = "", old = null if (style == null) element.style.cssText = "" else if (typeof style === "string") element.style.cssText = style @@ -546,47 +680,61 @@ module.exports = function($window) { for (var key in style) { element.style[key] = style[key] } - if (old != null && typeof old !== "string") { - for (var key in old) { - if (!(key in style)) element.style[key] = "" - } - } } } + // Here's an explanation of how this works: + // 1. The event names are always (by design) prefixed by `on`. + // 2. The EventListener interface accepts either a function or an object + // with a `handleEvent` method. + // 3. The object does not inherit from `Object.prototype`, to avoid + // any potential interference with that (e.g. setters). + // 4. The event name is remapped to the handler before calling it. + // 5. In function-based event handlers, `ev.target === this`. We replicate + // that below. + function EventDict() {} + EventDict.prototype = Object.create(null) + EventDict.prototype.handleEvent = function (ev) { + var handler = this["on" + ev.type] + if (typeof handler === "function") handler.call(ev.target, ev) + else if (typeof handler.handleEvent === "function") handler.handleEvent(ev) + if (typeof onevent === "function") onevent.call(ev.target, ev) + } + //event function updateEvent(vnode, key, value) { - var element = vnode.dom - var callback = typeof onevent !== "function" ? value : function(e) { - var result = value.call(element, e) - onevent.call(element, e) - return result - } - if (key in element) element[key] = typeof value === "function" ? callback : null - else { - var eventName = key.slice(2) - if (vnode.events === undefined) vnode.events = {} - if (vnode.events[key] === callback) return - if (vnode.events[key] != null) element.removeEventListener(eventName, vnode.events[key], false) - if (typeof value === "function") { - vnode.events[key] = callback - element.addEventListener(eventName, vnode.events[key], false) + if (vnode.events != null) { + if (vnode.events[key] === value) return + if (value != null && (typeof value === "function" || typeof value === "object")) { + if (vnode.events[key] == null) vnode.dom.addEventListener(key.slice(2), vnode.events, false) + vnode.events[key] = value + } else { + if (vnode.events[key] != null) vnode.dom.removeEventListener(key.slice(2), vnode.events, false) + vnode.events[key] = undefined } + } else if (value != null && (typeof value === "function" || typeof value === "object")) { + vnode.events = new EventDict() + vnode.dom.addEventListener(key.slice(2), vnode.events, false) + vnode.events[key] = value } } //lifecycle function initLifecycle(source, vnode, hooks) { - if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode) - if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode)) + if (typeof source.oninit === "function") callHook.call(source.oninit, vnode) + if (typeof source.oncreate === "function") hooks.push(callHook.bind(source.oncreate, vnode)) } function updateLifecycle(source, vnode, hooks) { - if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode)) + if (typeof source.onupdate === "function") hooks.push(callHook.bind(source.onupdate, vnode)) } function shouldNotUpdate(vnode, old) { var forceVnodeUpdate, forceComponentUpdate - if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") forceVnodeUpdate = vnode.attrs.onbeforeupdate.call(vnode.state, vnode, old) - if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeupdate === "function") forceComponentUpdate = vnode._state.onbeforeupdate.call(vnode.state, vnode, old) + if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") { + forceVnodeUpdate = callHook.call(vnode.attrs.onbeforeupdate, vnode, old) + } + if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") { + forceComponentUpdate = callHook.call(vnode.state.onbeforeupdate, vnode, old) + } if (!(forceVnodeUpdate === undefined && forceComponentUpdate === undefined) && !forceVnodeUpdate && !forceComponentUpdate) { vnode.dom = old.dom vnode.domSize = old.domSize @@ -600,15 +748,17 @@ module.exports = function($window) { if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.") var hooks = [] var active = $doc.activeElement + var namespace = dom.namespaceURI // First time rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" if (!Array.isArray(vnodes)) vnodes = [vnodes] - updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), false, hooks, null, undefined) + updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace) dom.vnodes = vnodes + // document.activeElement can return null in IE https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement + if (active != null && $doc.activeElement !== active) active.focus() for (var i = 0; i < hooks.length; i++) hooks[i]() - if ($doc.activeElement !== active) active.focus() } return {render: render, setEventCallback: setEventCallback} diff --git a/render/tests/test-attributes.js b/render/tests/test-attributes.js index c6a3df31..62cb4196 100644 --- a/render/tests/test-attributes.js +++ b/render/tests/test-attributes.js @@ -11,6 +11,46 @@ o.spec("attributes", function() { root = $window.document.body render = vdom($window).render }) + o.spec("basics", function() { + o("works (create/update/remove)", function() { + + var a = {tag: "div", attrs: {}} + var b = {tag: "div", attrs: {id: "test"}} + var c = {tag: "div", attrs: {}} + + render(root, [a]); + + o(a.dom.hasAttribute("id")).equals(false) + + render(root, [b]); + + o(b.dom.getAttribute("id")).equals("test") + + render(root, [c]); + + o(c.dom.hasAttribute("id")).equals(false) + }) + o("undefined attr is equivalent to a lack of attr", function() { + var a = {tag: "div", attrs: {id: undefined}} + var b = {tag: "div", attrs: {id: "test"}} + var c = {tag: "div", attrs: {id: undefined}} + + render(root, [a]); + + o(a.dom.hasAttribute("id")).equals(false) + + render(root, [b]); + + o(b.dom.hasAttribute("id")).equals(true) + o(b.dom.getAttribute("id")).equals("test") + + render(root, [c]); + + // #1804 + // TODO: uncomment + // o(c.dom.hasAttribute("id")).equals(false) + }) + }) o.spec("customElements", function(){ o("when vnode is customElement, custom setAttribute called", function(){ @@ -54,7 +94,7 @@ o.spec("attributes", function() { render(root, [a]) - o(a.dom.attributes["readonly"].nodeValue).equals("") + o(a.dom.attributes["readonly"].value).equals("") }) o("when input readonly is false, attribute is not present", function() { var a = {tag: "input", attrs: {readonly: false}} @@ -96,6 +136,196 @@ o.spec("attributes", function() { o(a.dom.attributes["checked"]).equals(undefined) }) }) + o.spec("input.value", function() { + o("can be set as text", function() { + var a = {tag: "input", attrs: {value: "test"}} + + render(root, [a]); + + o(a.dom.value).equals("test") + }) + o("a lack of attribute removes `value`", function() { + var a = {tag: "input", attrs: {}} + var b = {tag: "input", attrs: {value: "test"}} + // var c = {tag: "input", attrs: {}} + + render(root, [a]) + + o(a.dom.value).equals("") + + render(root, [b]) + + o(a.dom.value).equals("test") + + // https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235 + // TODO: Uncomment + // render(root, [c]) + + // o(a.dom.value).equals("") + }) + o("can be set as number", function() { + var a = {tag: "input", attrs: {value: 1}} + + render(root, [a]); + + o(a.dom.value).equals("1") + }) + o("null becomes the empty string", function() { + var a = {tag: "input", attrs: {value: null}} + var b = {tag: "input", attrs: {value: "test"}} + var c = {tag: "input", attrs: {value: null}} + + render(root, [a]); + + o(a.dom.value).equals("") + o(a.dom.getAttribute("value")).equals(null) + + render(root, [b]); + + o(b.dom.value).equals("test") + o(b.dom.getAttribute("value")).equals(null) + + render(root, [c]); + + o(c.dom.value).equals("") + o(c.dom.getAttribute("value")).equals(null) + }) + o("'' and 0 are different values", function() { + var a = {tag: "input", attrs: {value: 0}, children:[{tag:"#", children:""}]} + var b = {tag: "input", attrs: {value: ""}, children:[{tag:"#", children:""}]} + var c = {tag: "input", attrs: {value: 0}, children:[{tag:"#", children:""}]} + + render(root, [a]); + + o(a.dom.value).equals("0") + + render(root, [b]); + + o(b.dom.value).equals("") + + // #1595 redux + render(root, [c]); + + o(c.dom.value).equals("0") + }) + o("isn't set when equivalent to the previous value and focused", function() { + var $window = domMock({spy: o.spy}) + var root = $window.document.body + var render = vdom($window).render + + var a = {tag: "input"} + var b = {tag: "input", attrs: {value: "1"}} + var c = {tag: "input", attrs: {value: "1"}} + var d = {tag: "input", attrs: {value: 1}} + var e = {tag: "input", attrs: {value: 2}} + + render(root, [a]) + var spies = $window.__getSpies(a.dom) + a.dom.focus() + + o(spies.valueSetter.callCount).equals(0) + + render(root, [b]) + + o(b.dom.value).equals("1") + o(spies.valueSetter.callCount).equals(1) + + render(root, [c]) + + o(c.dom.value).equals("1") + o(spies.valueSetter.callCount).equals(1) + + render(root, [d]) + + o(d.dom.value).equals("1") + o(spies.valueSetter.callCount).equals(1) + + render(root, [e]) + + o(d.dom.value).equals("2") + o(spies.valueSetter.callCount).equals(2) + }) + }) + o.spec("input.type", function() { + o("the input.type setter is never used", function() { + var $window = domMock({spy: o.spy}) + var root = $window.document.body + var render = vdom($window).render + + var a = {tag: "input", attrs: {type: "radio"}} + var b = {tag: "input", attrs: {type: "text"}} + var c = {tag: "input", attrs: {}} + + render(root, [a]) + var spies = $window.__getSpies(a.dom) + + o(spies.typeSetter.callCount).equals(0) + o(a.dom.getAttribute("type")).equals("radio") + + render(root, [b]) + + o(spies.typeSetter.callCount).equals(0) + o(b.dom.getAttribute("type")).equals("text") + + render(root, [c]) + + o(spies.typeSetter.callCount).equals(0) + o(c.dom.hasAttribute("type")).equals(false) + }) + }) + o.spec("textarea.value", function() { + o("can be removed by not passing a value", function() { + var a = {tag: "textarea", attrs: {value:"x"}} + // var b = {tag: "textarea", attrs: {}} + + render(root, [a]) + + o(a.dom.value).equals("x") + + // https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235 + // TODO: Uncomment + // render(root, [b]) + + // o(b.dom.value).equals("") + }) + o("isn't set when equivalent to the previous value and focused", function() { + var $window = domMock({spy: o.spy}) + var root = $window.document.body + var render = vdom($window).render + + var a = {tag: "textarea"} + var b = {tag: "textarea", attrs: {value: "1"}} + var c = {tag: "textarea", attrs: {value: "1"}} + var d = {tag: "textarea", attrs: {value: 1}} + var e = {tag: "textarea", attrs: {value: 2}} + + render(root, [a]) + var spies = $window.__getSpies(a.dom) + a.dom.focus() + + o(spies.valueSetter.callCount).equals(0) + + render(root, [b]) + + o(b.dom.value).equals("1") + o(spies.valueSetter.callCount).equals(1) + + render(root, [c]) + + o(c.dom.value).equals("1") + o(spies.valueSetter.callCount).equals(1) + + render(root, [d]) + + o(d.dom.value).equals("1") + o(spies.valueSetter.callCount).equals(1) + + render(root, [e]) + + o(d.dom.value).equals("2") + o(spies.valueSetter.callCount).equals(2) + }) + }) o.spec("link href", function() { o("when link href is true, attribute is present", function() { var a = {tag: "a", attrs: {href: true}} @@ -118,7 +348,7 @@ o.spec("attributes", function() { render(root, canvas) - o(canvas.dom.attributes["width"].nodeValue).equals("100%") + o(canvas.dom.attributes["width"].value).equals("100%") o(canvas.dom.width).equals(100) }) }) @@ -128,7 +358,231 @@ o.spec("attributes", function() { render(root, [a]); - o(a.dom.attributes["class"].nodeValue).equals("test") + o(a.dom.attributes["class"].value).equals("test") + }) + }) + o.spec("option.value", function() { + o("can be set as text", function() { + var a = {tag: "option", attrs: {value: "test"}} + + render(root, [a]); + + o(a.dom.value).equals("test") + }) + o("can be set as number", function() { + var a = {tag: "option", attrs: {value: 1}} + + render(root, [a]); + + o(a.dom.value).equals("1") + }) + o("null becomes 'null'", function() { + var a = {tag: "option", attrs: {value: null}} + var b = {tag: "option", attrs: {value: "test"}} + var c = {tag: "option", attrs: {value: null}} + + render(root, [a]); + + o(a.dom.value).equals("null") + o(a.dom.getAttribute("value")).equals("null") + + render(root, [b]); + + o(b.dom.value).equals("test") + o(b.dom.getAttribute("value")).equals("test") + + render(root, [c]); + + o(c.dom.value).equals("null") + o(c.dom.getAttribute("value")).equals("null") + }) + o("'' and 0 are different values", function() { + var a = {tag: "option", attrs: {value: 0}, children:[{tag:"#", children:""}]} + var b = {tag: "option", attrs: {value: ""}, children:[{tag:"#", children:""}]} + var c = {tag: "option", attrs: {value: 0}, children:[{tag:"#", children:""}]} + + render(root, [a]); + + o(a.dom.value).equals("0") + + render(root, [b]); + + o(a.dom.value).equals("") + + // #1595 redux + render(root, [c]); + + o(c.dom.value).equals("0") + }) + o("isn't set when equivalent to the previous value", function() { + var $window = domMock({spy: o.spy}) + var root = $window.document.body + var render = vdom($window).render + + var a = {tag: "option"} + var b = {tag: "option", attrs: {value: "1"}} + var c = {tag: "option", attrs: {value: "1"}} + var d = {tag: "option", attrs: {value: 1}} + var e = {tag: "option", attrs: {value: 2}} + + render(root, [a]) + var spies = $window.__getSpies(a.dom) + + o(spies.valueSetter.callCount).equals(0) + + render(root, [b]) + + o(b.dom.value).equals("1") + o(spies.valueSetter.callCount).equals(1) + + render(root, [c]) + + o(c.dom.value).equals("1") + o(spies.valueSetter.callCount).equals(1) + + render(root, [d]) + + o(d.dom.value).equals("1") + o(spies.valueSetter.callCount).equals(1) + + render(root, [e]) + + o(d.dom.value).equals("2") + o(spies.valueSetter.callCount).equals(2) + }) + }) + o.spec("select.value", function() { + function makeSelect(value) { + var attrs = (arguments.length === 0) ? {} : {value: value} + return {tag: "select", attrs: attrs, children: [ + {tag:"option", attrs: {value: "1"}}, + {tag:"option", attrs: {value: "2"}}, + {tag:"option", attrs: {value: "a"}}, + {tag:"option", attrs: {value: "0"}}, + {tag:"option", attrs: {value: ""}} + ]} + } + /* FIXME + This incomplete test is meant for testing #1916. + However it cannot be completed until #1978 is addressed + which is a lack a working select.selected / option.selected + attribute. Ask isiahmeadows. + + o("render select options", function() { + var select = {tag: "select", selectedIndex: 0, children: [ + {tag:"option", attrs: {value: "1", selected: ""}} + ]} + render(root, select) + }) + */ + o("can be set as text", function() { + var a = makeSelect() + var b = makeSelect("2") + var c = makeSelect("a") + + render(root, [a]) + + o(a.dom.value).equals("1") + o(a.dom.selectedIndex).equals(0) + + render(root, [b]) + + o(b.dom.value).equals("2") + o(b.dom.selectedIndex).equals(1) + + render(root, [c]) + + o(c.dom.value).equals("a") + o(c.dom.selectedIndex).equals(2) + }) + o("setting null unsets the value", function() { + var a = makeSelect(null) + + render(root, [a]) + + o(a.dom.value).equals("") + o(a.dom.selectedIndex).equals(-1) + }) + o("values are type converted", function() { + var a = makeSelect(1) + var b = makeSelect(2) + + render(root, [a]) + + o(a.dom.value).equals("1") + o(a.dom.selectedIndex).equals(0) + + render(root, [b]) + + o(b.dom.value).equals("2") + o(b.dom.selectedIndex).equals(1) + }) + o("'' and 0 are different values when focused", function() { + var a = makeSelect("") + var b = makeSelect(0) + + render(root, [a]) + a.dom.focus() + + o(a.dom.value).equals("") + + // #1595 redux + render(root, [b]) + + o(b.dom.value).equals("0") + }) + o("'' and null are different values when focused", function() { + var a = makeSelect("") + var b = makeSelect(null) + var c = makeSelect("") + + render(root, [a]) + a.dom.focus() + + o(a.dom.value).equals("") + o(a.dom.selectedIndex).equals(4) + + render(root, [b]) + + o(b.dom.value).equals("") + o(b.dom.selectedIndex).equals(-1) + + render(root, [c]) + + o(c.dom.value).equals("") + o(c.dom.selectedIndex).equals(4) + }) + o("updates with the same value do not re-set the attribute if the select has focus", function() { + var $window = domMock({spy: o.spy}) + var root = $window.document.body + var render = vdom($window).render + + var a = makeSelect() + var b = makeSelect("1") + var c = makeSelect(1) + var d = makeSelect("2") + + render(root, [a]) + var spies = $window.__getSpies(a.dom) + a.dom.focus() + + o(spies.valueSetter.callCount).equals(0) + o(a.dom.value).equals("1") + + render(root, [b]) + + o(spies.valueSetter.callCount).equals(0) + o(b.dom.value).equals("1") + + render(root, [c]) + + o(spies.valueSetter.callCount).equals(0) + o(c.dom.value).equals("1") + + render(root, [d]) + + o(spies.valueSetter.callCount).equals(1) + o(d.dom.value).equals("2") }) }) o.spec("contenteditable throws on untrusted children", function() { diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 94863236..dd27ef10 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -30,7 +30,7 @@ o.spec("component", function() { render(root, [node]) o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.attributes["id"].value).equals("a") o(root.firstChild.firstChild.nodeValue).equals("b") }) o("receives arguments", function() { @@ -44,7 +44,7 @@ o.spec("component", function() { render(root, [node]) o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.attributes["id"].value).equals("a") o(root.firstChild.firstChild.nodeValue).equals("b") }) o("updates", function() { @@ -57,7 +57,7 @@ o.spec("component", function() { render(root, [{tag: component, attrs: {id: "c"}, text: "d"}]) o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("c") + o(root.firstChild.attributes["id"].value).equals("c") o(root.firstChild.firstChild.nodeValue).equals("d") }) o("updates root from null", function() { @@ -400,7 +400,7 @@ o.spec("component", function() { o(called).equals(1) o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.attributes["id"].value).equals("a") o(root.firstChild.firstChild.nodeValue).equals("b") }) o("calls oninit when returning fragment", function() { @@ -423,7 +423,7 @@ o.spec("component", function() { o(called).equals(1) o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.attributes["id"].value).equals("a") o(root.firstChild.firstChild.nodeValue).equals("b") }) o("calls oninit before view", function() { @@ -479,7 +479,7 @@ o.spec("component", function() { o(called).equals(1) o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.attributes["id"].value).equals("a") o(root.firstChild.firstChild.nodeValue).equals("b") }) o("does not calls oncreate on redraw", function() { @@ -520,7 +520,7 @@ o.spec("component", function() { o(called).equals(1) o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.attributes["id"].value).equals("a") o(root.firstChild.firstChild.nodeValue).equals("b") }) o("calls onupdate", function() { @@ -546,7 +546,7 @@ o.spec("component", function() { o(called).equals(1) o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.attributes["id"].value).equals("a") o(root.firstChild.firstChild.nodeValue).equals("b") }) o("calls onupdate when returning fragment", function() { @@ -572,7 +572,7 @@ o.spec("component", function() { o(called).equals(1) o(root.firstChild.nodeName).equals("DIV") - o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.attributes["id"].value).equals("a") o(root.firstChild.firstChild.nodeValue).equals("b") }) o("calls onremove", function() { @@ -764,97 +764,6 @@ o.spec("component", function() { o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) }) }) - o("lifecycle timing megatest (for a single component with the state overwritten)", function() { - var methods = { - view: o.spy(function(vnode) { - o(vnode.state).equals(1) - return "" - }) - } - var attrs = {} - var hooks = [ - "oninit", "oncreate", "onbeforeupdate", - "onupdate", "onbeforeremove", "onremove" - ] - hooks.forEach(function(hook) { - // the `attrs` hooks are called before the component ones - attrs[hook] = o.spy(function(vnode) { - o(vnode.state).equals(1) - o(attrs[hook].callCount).equals(methods[hook].callCount + 1) - }) - methods[hook] = o.spy(function(vnode) { - o(vnode.state).equals(1) - o(attrs[hook].callCount).equals(methods[hook].callCount) - }) - }) - - var attrsOninit = attrs.oninit - var methodsOninit = methods.oninit - attrs.oninit = o.spy(function(vnode){ - vnode.state = 1 - return attrsOninit.call(this, vnode) - }) - methods.oninit = o.spy(function(vnode){ - vnode.state = 1 - return methodsOninit.call(this, vnode) - }) - - var component = createComponent(methods) - - o(methods.view.callCount).equals(0) - o(methods.oninit.callCount).equals(0) - o(methods.oncreate.callCount).equals(0) - o(methods.onbeforeupdate.callCount).equals(0) - o(methods.onupdate.callCount).equals(0) - o(methods.onbeforeremove.callCount).equals(0) - o(methods.onremove.callCount).equals(0) - - hooks.forEach(function(hook) { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) - }) - - render(root, [{tag: component, attrs: attrs}]) - - o(methods.view.callCount).equals(1) - o(methods.oninit.callCount).equals(1) - o(methods.oncreate.callCount).equals(1) - o(methods.onbeforeupdate.callCount).equals(0) - o(methods.onupdate.callCount).equals(0) - o(methods.onbeforeremove.callCount).equals(0) - o(methods.onremove.callCount).equals(0) - - hooks.forEach(function(hook) { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) - }) - - render(root, [{tag: component, attrs: attrs}]) - - o(methods.view.callCount).equals(2) - o(methods.oninit.callCount).equals(1) - o(methods.oncreate.callCount).equals(1) - o(methods.onbeforeupdate.callCount).equals(1) - o(methods.onupdate.callCount).equals(1) - o(methods.onbeforeremove.callCount).equals(0) - o(methods.onremove.callCount).equals(0) - - hooks.forEach(function(hook) { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) - }) - - render(root, []) - - o(methods.view.callCount).equals(2) - o(methods.oninit.callCount).equals(1) - o(methods.oncreate.callCount).equals(1) - o(methods.onbeforeupdate.callCount).equals(1) - o(methods.onupdate.callCount).equals(1) - o(methods.onbeforeremove.callCount).equals(1) - o(methods.onremove.callCount).equals(1) - - hooks.forEach(function(hook) { - o(attrs[hook].callCount).equals(methods[hook].callCount)(hook) - }) - }) o("hook state and arguments validation", function(){ var methods = { view: o.spy(function(vnode) { @@ -899,7 +808,7 @@ o.spec("component", function() { o(methods[hook].args.length).equals(attrs[hook].args.length)(hook) }) }) - o("recycled components get a fresh state", function() { + o("no recycling occurs (was: recycled components get a fresh state)", function() { var step = 0 var firstState var view = o.spy(function(vnode) { @@ -918,7 +827,7 @@ o.spec("component", function() { step = 1 render(root, [{tag: "div", children: [{tag: component, key: 1}]}]) - o(child).equals(root.firstChild.firstChild) + o(child).notEquals(root.firstChild.firstChild) // this used to be a recycling pool test o(view.callCount).equals(2) }) }) diff --git a/render/tests/test-createElement.js b/render/tests/test-createElement.js index 9e2827cf..c2dc1d90 100644 --- a/render/tests/test-createElement.js +++ b/render/tests/test-createElement.js @@ -24,8 +24,8 @@ o.spec("createElement", function() { render(root, [vnode]) o(vnode.dom.nodeName).equals("DIV") - o(vnode.dom.attributes["id"].nodeValue).equals("a") - o(vnode.dom.attributes["title"].nodeValue).equals("b") + o(vnode.dom.attributes["id"].value).equals("a") + o(vnode.dom.attributes["title"].value).equals("b") }) o("creates style", function() { var vnode = {tag: "div", attrs: {style: {backgroundColor: "red"}}} @@ -48,28 +48,52 @@ o.spec("createElement", function() { render(root, [vnode]) o(vnode.dom.nodeName).equals("DIV") - o(vnode.dom.attributes["id"].nodeValue).equals("a") - o(vnode.dom.attributes["title"].nodeValue).equals("b") + o(vnode.dom.attributes["id"].value).equals("a") + o(vnode.dom.attributes["title"].value).equals("b") o(vnode.dom.childNodes.length).equals(2) o(vnode.dom.childNodes[0].nodeName).equals("A") o(vnode.dom.childNodes[1].nodeName).equals("B") }) o("creates svg", function() { - var vnode = {tag: "svg", ns: "http://www.w3.org/2000/svg", children: [{tag: "a", ns: "http://www.w3.org/2000/svg", attrs: {"xlink:href": "javascript:;"}}]} + var vnode = {tag: "svg", ns: "http://www.w3.org/2000/svg", children: [ + {tag: "a", ns: "http://www.w3.org/2000/svg", attrs: {"xlink:href": "javascript:;"}}, + {tag: "foreignObject", children: [{tag: "body", attrs: {xmlns: "http://www.w3.org/1999/xhtml"}}]} + ]} render(root, [vnode]) o(vnode.dom.nodeName).equals("svg") o(vnode.dom.namespaceURI).equals("http://www.w3.org/2000/svg") o(vnode.dom.firstChild.nodeName).equals("a") o(vnode.dom.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") - o(vnode.dom.firstChild.attributes["href"].nodeValue).equals("javascript:;") - o(vnode.dom.firstChild.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") + o(vnode.dom.firstChild.attributes["xlink:href"].value).equals("javascript:;") + o(vnode.dom.firstChild.attributes["xlink:href"].namespaceURI).equals("http://www.w3.org/1999/xlink") + o(vnode.dom.childNodes[1].nodeName).equals("foreignObject") + o(vnode.dom.childNodes[1].firstChild.nodeName).equals("body") + o(vnode.dom.childNodes[1].firstChild.namespaceURI).equals("http://www.w3.org/1999/xhtml") }) o("sets attributes correctly for svg", function() { var vnode = {tag: "svg", ns: "http://www.w3.org/2000/svg", attrs: {viewBox: "0 0 100 100"}} render(root, [vnode]) - o(vnode.dom.attributes["viewBox"].nodeValue).equals("0 0 100 100") + o(vnode.dom.attributes["viewBox"].value).equals("0 0 100 100") + }) + o("removes xlink:href", function() { + var vnode = {tag: "svg", ns: "http://www.w3.org/2000/svg", children: [ + {tag: "a", ns: "http://www.w3.org/2000/svg", attrs: {"xlink:href": "javascript:;"}} + ]} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("svg") + o(vnode.dom.firstChild.attributes["xlink:href"].value).equals("javascript:;") + o(vnode.dom.firstChild.attributes["xlink:href"].namespaceURI).equals("http://www.w3.org/1999/xlink") + + vnode = {tag: "svg", ns: "http://www.w3.org/2000/svg", children: [ + {tag: "a", ns: "http://www.w3.org/2000/svg", attrs: {}} + ]} + render(root, [vnode]) + + o(vnode.dom.nodeName).equals("svg") + o(vnode.dom.firstChild.attributes["xlink:href"]).equals(undefined) }) o("creates mathml", function() { var vnode = {tag: "math", ns: "http://www.w3.org/1998/Math/MathML", children: [{tag: "mrow", ns: "http://www.w3.org/1998/Math/MathML"}]} diff --git a/render/tests/test-createHTML.js b/render/tests/test-createHTML.js index bb0d508c..a337213b 100644 --- a/render/tests/test-createHTML.js +++ b/render/tests/test-createHTML.js @@ -31,7 +31,7 @@ o.spec("createHTML", function() { o(vnode.dom).equals(null) o(vnode.domSize).equals(0) }) - o("handles multiple children", function() { + o("handles multiple children in HTML", function() { var vnode = {tag: "<", children: "
"} render(root, [vnode]) @@ -51,4 +51,34 @@ o.spec("createHTML", function() { o(vnode.dom.nodeName).equals(tag.toUpperCase()) }) }) + o("creates SVG", function() { + var vnode = {tag: "<", children: ""} + render(root, [{tag:"svg", children: [vnode]}]) + + o(vnode.dom.nodeName).equals("g") + o(vnode.dom.namespaceURI).equals("http://www.w3.org/2000/svg") + }) + o("creates text SVG", function() { + var vnode = {tag: "<", children: "a"} + render(root, [{tag:"svg", children: [vnode]}]) + + o(vnode.dom.nodeValue).equals("a") + }) + o("handles empty SVG", function() { + var vnode = {tag: "<", children: ""} + render(root, [{tag:"svg", children: [vnode]}]) + + o(vnode.dom).equals(null) + o(vnode.domSize).equals(0) + }) + o("handles multiple children in SVG", function() { + var vnode = {tag: "<", children: ""} + render(root, [{tag:"svg", children: [vnode]}]) + + o(vnode.domSize).equals(2) + o(vnode.dom.nodeName).equals("g") + o(vnode.dom.namespaceURI).equals("http://www.w3.org/2000/svg") + o(vnode.dom.nextSibling.nodeName).equals("text") + o(vnode.dom.nextSibling.namespaceURI).equals("http://www.w3.org/2000/svg") + }) }) diff --git a/render/tests/test-event.js b/render/tests/test-event.js index 31d2d012..b6cb1540 100644 --- a/render/tests/test-event.js +++ b/render/tests/test-event.js @@ -33,7 +33,27 @@ o.spec("event", function() { o(onevent.args[0].type).equals("click") o(onevent.args[0].target).equals(div.dom) }) - + + o("handles click EventListener object", function() { + var spy = o.spy() + var listener = {handleEvent: spy} + var div = {tag: "div", attrs: {onclick: listener}} + var e = $window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + render(root, [div]) + div.dom.dispatchEvent(e) + + o(spy.callCount).equals(1) + o(spy.this).equals(listener) + o(spy.args[0].type).equals("click") + o(spy.args[0].target).equals(div.dom) + o(onevent.callCount).equals(1) + o(onevent.this).equals(div.dom) + o(onevent.args[0].type).equals("click") + o(onevent.args[0].target).equals(div.dom) + }) + o("removes event", function() { var spy = o.spy() var vnode = {tag: "a", attrs: {onclick: spy}} @@ -45,7 +65,130 @@ o.spec("event", function() { var e = $window.document.createEvent("MouseEvents") e.initEvent("click", true, true) vnode.dom.dispatchEvent(e) - + + o(spy.callCount).equals(0) + }) + + o("removes event when null", function() { + var spy = o.spy() + var vnode = {tag: "a", attrs: {onclick: spy}} + var updated = {tag: "a", attrs: {onclick: null}} + + render(root, [vnode]) + render(root, [updated]) + + var e = $window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + vnode.dom.dispatchEvent(e) + + o(spy.callCount).equals(0) + }) + + o("removes event when undefined", function() { + var spy = o.spy() + var vnode = {tag: "a", attrs: {onclick: spy}} + var updated = {tag: "a", attrs: {onclick: undefined}} + + render(root, [vnode]) + render(root, [updated]) + + var e = $window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + vnode.dom.dispatchEvent(e) + + o(spy.callCount).equals(0) + }) + + o("removes event added via addEventListener when null", function() { + var spy = o.spy() + var vnode = {tag: "a", attrs: {ontouchstart: spy}} + var updated = {tag: "a", attrs: {ontouchstart: null}} + + render(root, [vnode]) + render(root, [updated]) + + var e = $window.document.createEvent("TouchEvents") + e.initEvent("touchstart", true, true) + vnode.dom.dispatchEvent(e) + + o(spy.callCount).equals(0) + }) + + o("removes event added via addEventListener", function() { + var spy = o.spy() + var vnode = {tag: "a", attrs: {ontouchstart: spy}} + var updated = {tag: "a", attrs: {}} + + render(root, [vnode]) + render(root, [updated]) + + var e = $window.document.createEvent("TouchEvents") + e.initEvent("touchstart", true, true) + vnode.dom.dispatchEvent(e) + + o(spy.callCount).equals(0) + }) + + o("removes event added via addEventListener when undefined", function() { + var spy = o.spy() + var vnode = {tag: "a", attrs: {ontouchstart: spy}} + var updated = {tag: "a", attrs: {ontouchstart: undefined}} + + render(root, [vnode]) + render(root, [updated]) + + var e = $window.document.createEvent("TouchEvents") + e.initEvent("touchstart", true, true) + vnode.dom.dispatchEvent(e) + + o(spy.callCount).equals(0) + }) + + o("removes EventListener object", function() { + var spy = o.spy() + var listener = {handleEvent: spy} + var vnode = {tag: "a", attrs: {onclick: listener}} + var updated = {tag: "a", attrs: {}} + + render(root, [vnode]) + render(root, [updated]) + + var e = $window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + vnode.dom.dispatchEvent(e) + + o(spy.callCount).equals(0) + }) + + o("removes EventListener object when null", function() { + var spy = o.spy() + var listener = {handleEvent: spy} + var vnode = {tag: "a", attrs: {onclick: listener}} + var updated = {tag: "a", attrs: {onclick: null}} + + render(root, [vnode]) + render(root, [updated]) + + var e = $window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + vnode.dom.dispatchEvent(e) + + o(spy.callCount).equals(0) + }) + + o("removes EventListener object when undefined", function() { + var spy = o.spy() + var listener = {handleEvent: spy} + var vnode = {tag: "a", attrs: {onclick: listener}} + var updated = {tag: "a", attrs: {onclick: undefined}} + + render(root, [vnode]) + render(root, [updated]) + + var e = $window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + vnode.dom.dispatchEvent(e) + o(spy.callCount).equals(0) }) @@ -69,7 +212,31 @@ o.spec("event", function() { o(onevent.args[0].type).equals("click") o(onevent.args[0].target).equals(div.dom) o(div.dom).equals(updated.dom) - o(div.dom.attributes["id"].nodeValue).equals("b") + o(div.dom.attributes["id"].value).equals("b") + }) + + o("fires click EventListener object only once after redraw", function() { + var spy = o.spy() + var listener = {handleEvent: spy} + var div = {tag: "div", attrs: {id: "a", onclick: listener}} + var updated = {tag: "div", attrs: {id: "b", onclick: listener}} + var e = $window.document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + render(root, [div]) + render(root, [updated]) + div.dom.dispatchEvent(e) + + o(spy.callCount).equals(1) + o(spy.this).equals(listener) + o(spy.args[0].type).equals("click") + o(spy.args[0].target).equals(div.dom) + o(onevent.callCount).equals(1) + o(onevent.this).equals(div.dom) + o(onevent.args[0].type).equals("click") + o(onevent.args[0].target).equals(div.dom) + o(div.dom).equals(updated.dom) + o(div.dom.attributes["id"].value).equals("b") }) o("handles ontransitionend", function() { @@ -90,4 +257,24 @@ o.spec("event", function() { o(onevent.args[0].type).equals("transitionend") o(onevent.args[0].target).equals(div.dom) }) + + o("handles transitionend EventListener object", function() { + var spy = o.spy() + var listener = {handleEvent: spy} + var div = {tag: "div", attrs: {ontransitionend: listener}} + var e = $window.document.createEvent("HTMLEvents") + e.initEvent("transitionend", true, true) + + render(root, [div]) + div.dom.dispatchEvent(e) + + o(spy.callCount).equals(1) + o(spy.this).equals(listener) + o(spy.args[0].type).equals("transitionend") + o(spy.args[0].target).equals(div.dom) + o(onevent.callCount).equals(1) + o(onevent.this).equals(div.dom) + o(onevent.args[0].type).equals("transitionend") + o(onevent.args[0].target).equals(div.dom) + }) }) diff --git a/render/tests/test-hyperscript.js b/render/tests/test-hyperscript.js index 4f6c4548..f7c5f889 100644 --- a/render/tests/test-hyperscript.js +++ b/render/tests/test-hyperscript.js @@ -218,6 +218,18 @@ o.spec("hyperscript", function() { o(vnode.tag).equals("div") o(vnode.attrs.a).equals(true) }) + o("handles explicit empty string value for input", function() { + var vnode = m('input[value=""]') + + o(vnode.tag).equals("input") + o(vnode.attrs.value).equals("") + }) + o("handles explicit empty string value for option", function() { + var vnode = m('option[value=""]') + + o(vnode.tag).equals("option") + o(vnode.attrs.value).equals("") + }) }) o.spec("attrs", function() { o("handles string attr", function() { @@ -495,6 +507,23 @@ o.spec("hyperscript", function() { o(vnode.children[0].tag).equals("i") o(vnode.children[1].tag).equals("s") }) + o("handles shared attrs", function() { + var attrs = {a: "b"} + + var nodeA = m(".a", attrs) + var nodeB = m(".b", attrs) + + o(nodeA.attrs.className).equals("a") + o(nodeA.attrs.a).equals("b") + + o(nodeB.attrs.className).equals("b") + o(nodeB.attrs.a).equals("b") + }) + o("doesnt modify passed attributes object", function() { + var attrs = {a: "b"} + m(".a", attrs) + o(attrs).deepEquals({a: "b"}) + }) o("handles fragment children without attr unwrapped", function() { var vnode = m("div", [m("i")], [m("s")]) diff --git a/render/tests/test-input.js b/render/tests/test-input.js index 73eecf1e..c443db1a 100644 --- a/render/tests/test-input.js +++ b/render/tests/test-input.js @@ -30,6 +30,16 @@ o.spec("form inputs", function() { o($window.document.activeElement).equals(input.dom) }) + o("maintains focus when changed manually in hook", function() { + var input = {tag: "input", attrs: {oncreate: function() { + input.dom.focus(); + }}}; + + render(root, [input]) + + o($window.document.activeElement).equals(input.dom) + }) + o("syncs input value if DOM value differs from vdom value", function() { var input = {tag: "input", attrs: {value: "aaa", oninput: function() {}}} var updated = {tag: "input", attrs: {value: "aaa", oninput: function() {}}} @@ -49,6 +59,16 @@ o.spec("form inputs", function() { o(updated.dom.value).equals("aaa") }) + o("clear element value if vdom value is set to undefined (aka removed)", function() { + var input = {tag: "input", attrs: {value: "aaa", oninput: function() {}}} + var updated = {tag: "input", attrs: {value: undefined, oninput: function() {}}} + + render(root, [input]) + render(root, [updated]) + + o(updated.dom.value).equals("") + }) + o("syncs input checked attribute if DOM value differs from vdom value", function() { var input = {tag: "input", attrs: {type: "checkbox", checked: true, onclick: function() {}}} var updated = {tag: "input", attrs: {type: "checkbox", checked: true, onclick: function() {}}} @@ -80,6 +100,57 @@ o.spec("form inputs", function() { o(select.dom.selectedIndex).equals(0) }) + o("select option can have empty string value", function() { + var select = {tag: "select", children :[ + {tag: "option", attrs: {value: ""}, text: "aaa"} + ]} + + render(root, [select]) + + o(select.dom.firstChild.value).equals("") + }) + + o("option value defaults to textContent unless explicitly set", function() { + var select = {tag: "select", children :[ + {tag: "option", text: "aaa"} + ]} + + render(root, [select]) + + o(select.dom.firstChild.value).equals("aaa") + o(select.dom.value).equals("aaa") + + //test that value changes when content changes + select = {tag: "select", children :[ + {tag: "option", text: "bbb"} + ]} + + render(root, [select]) + + o(select.dom.firstChild.value).equals("bbb") + o(select.dom.value).equals("bbb") + + //test that value can be set to "" in subsequent render + select = {tag: "select", children :[ + {tag: "option", attrs: {value: ""}, text: "aaa"} + ]} + + render(root, [select]) + + o(select.dom.firstChild.value).equals("") + o(select.dom.value).equals("") + + //test that value reverts to textContent when value omitted + select = {tag: "select", children :[ + {tag: "option", text: "aaa"} + ]} + + render(root, [select]) + + o(select.dom.firstChild.value).equals("aaa") + o(select.dom.value).equals("aaa") + }) + o("select yields invalid value without children", function() { var select = {tag: "select", attrs: {value: "a"}} diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js index 0e83d4a0..834f7510 100644 --- a/render/tests/test-onbeforeremove.js +++ b/render/tests/test-onbeforeremove.js @@ -36,10 +36,7 @@ o.spec("onbeforeremove", function() { o(update.callCount).equals(0) }) o("calls onbeforeremove when removing element", function(done) { - var vnode = {tag: "div", attrs: { - oninit: function() {vnode.state = {}}, - onbeforeremove: remove - }} + var vnode = {tag: "div", attrs: {onbeforeremove: remove}} render(root, [vnode]) render(root, []) @@ -47,6 +44,7 @@ o.spec("onbeforeremove", function() { function remove(node) { o(node).equals(vnode) o(this).equals(vnode.state) + o(this != null && typeof this === "object").equals(true) o(root.childNodes.length).equals(1) o(root.firstChild).equals(vnode.dom) diff --git a/render/tests/test-onbeforeupdate.js b/render/tests/test-onbeforeupdate.js index 9814122b..d0b1ecab 100644 --- a/render/tests/test-onbeforeupdate.js +++ b/render/tests/test-onbeforeupdate.js @@ -21,7 +21,7 @@ o.spec("onbeforeupdate", function() { render(root, [vnode]) render(root, [updated]) - o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.attributes["id"].value).equals("a") }) o("prevents update in text", function() { @@ -65,7 +65,7 @@ o.spec("onbeforeupdate", function() { render(root, [vnode]) render(root, [updated]) - o(root.firstChild.attributes["id"].nodeValue).equals("b") + o(root.firstChild.attributes["id"].value).equals("b") }) o("accepts arguments for comparison", function() { @@ -86,7 +86,7 @@ o.spec("onbeforeupdate", function() { } o(count).equals(1) - o(root.firstChild.attributes["id"].nodeValue).equals("b") + o(root.firstChild.attributes["id"].value).equals("b") }) o("is not called on creation", function() { @@ -129,7 +129,7 @@ o.spec("onbeforeupdate", function() { render(root, temp) render(root, updated) - o(vnodes[0].dom).equals(updated[0].dom) + o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test o(updated[0].dom.nodeName).equals("DIV") o(onbeforeupdate.callCount).equals(0) }) @@ -167,7 +167,7 @@ o.spec("onbeforeupdate", function() { render(root, [vnode]) render(root, [updated]) - o(root.firstChild.attributes["id"].nodeValue).equals("a") + o(root.firstChild.attributes["id"].value).equals("a") }) o("does not prevent update if returning true in component and true in vnode", function() { @@ -183,7 +183,7 @@ o.spec("onbeforeupdate", function() { render(root, [vnode]) render(root, [updated]) - o(root.firstChild.attributes["id"].nodeValue).equals("b") + o(root.firstChild.attributes["id"].value).equals("b") }) o("does not prevent update if returning false in component but true in vnode", function() { @@ -199,7 +199,7 @@ o.spec("onbeforeupdate", function() { render(root, [vnode]) render(root, [updated]) - o(root.firstChild.attributes["id"].nodeValue).equals("b") + o(root.firstChild.attributes["id"].value).equals("b") }) o("does not prevent update if returning true in component but false in vnode", function() { @@ -215,7 +215,7 @@ o.spec("onbeforeupdate", function() { render(root, [vnode]) render(root, [updated]) - o(root.firstChild.attributes["id"].nodeValue).equals("b") + o(root.firstChild.attributes["id"].value).equals("b") }) o("does not prevent update if returning true from component", function() { @@ -231,7 +231,7 @@ o.spec("onbeforeupdate", function() { render(root, [vnode]) render(root, [updated]) - o(root.firstChild.attributes["id"].nodeValue).equals("b") + o(root.firstChild.attributes["id"].value).equals("b") }) o("accepts arguments for comparison in component", function() { @@ -258,7 +258,7 @@ o.spec("onbeforeupdate", function() { } o(count).equals(1) - o(root.firstChild.attributes["id"].nodeValue).equals("b") + o(root.firstChild.attributes["id"].value).equals("b") }) o("is not called on component creation", function() { diff --git a/render/tests/test-oninit.js b/render/tests/test-oninit.js index 4d94cae4..f6ffb873 100644 --- a/render/tests/test-oninit.js +++ b/render/tests/test-oninit.js @@ -199,4 +199,27 @@ o.spec("oninit", function() { o(vnode.dom.oninit).equals(undefined) o(vnode.dom.attributes["oninit"]).equals(undefined) }) + + o("No spurious oninit calls in mapped keyed diff when the pool is involved (#1992)", function () { + var oninit1 = o.spy() + var oninit2 = o.spy() + var oninit3 = o.spy() + + render(root, [ + {tag: "p", key: 1, attrs: {oninit: oninit1}}, + {tag: "p", key: 2, attrs: {oninit: oninit2}}, + {tag: "p", key: 3, attrs: {oninit: oninit3}}, + ]) + render(root, [ + {tag: "p", key: 1, attrs: {oninit: oninit1}}, + {tag: "p", key: 3, attrs: {oninit: oninit3}}, + ]) + render(root, [ + {tag: "p", key: 3, attrs: {oninit: oninit3}}, + ]) + + o(oninit1.callCount).equals(1) + o(oninit2.callCount).equals(1) + o(oninit3.callCount).equals(1) + }) }) diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index a7f88a6b..9b4c4e23 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -89,8 +89,9 @@ o.spec("onremove", function() { o(vnode.dom.onremove).equals(undefined) o(vnode.dom.attributes["onremove"]).equals(undefined) + o(vnode.events).equals(undefined) }) - o("calls onremove on recycle", function() { + o("calls onremove on keyed nodes", function() { var remove = o.spy() var vnodes = [{tag: "div", key: 1}] var temp = [{tag: "div", key: 2, attrs: {onremove: remove}}] @@ -100,6 +101,7 @@ o.spec("onremove", function() { render(root, temp) render(root, updated) + o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test o(remove.callCount).equals(1) }) o("does not recycle when there's an onremove", function() { @@ -131,7 +133,7 @@ o.spec("onremove", function() { }) render(root, {tag: comp}) render(root, null) - + o(spy.callCount).equals(1) }) o("calls onremove on nested component child", function() { @@ -147,9 +149,71 @@ o.spec("onremove", function() { }) render(root, {tag: comp}) render(root, null) - + o(spy.callCount).equals(1) }) + o("doesn't call onremove on children when the corresponding view returns null (after removing the parent)", function() { + var threw = false + var spy = o.spy() + var parent = createComponent({ + view: function() {} + }) + var child = createComponent({ + view: function() {}, + onremove: spy + }) + render(root, {tag: parent, children: [child]}) + try { + render(root, null) + } catch (e) { + threw = e + } + + o(spy.callCount).equals(0) + o(threw).equals(false) + }) + o("doesn't call onremove on children when the corresponding view returns null (after removing the children)", function() { + var threw = false + var spy = o.spy() + var parent = createComponent({ + view: function() {} + }) + var child = createComponent({ + view: function() {}, + onremove: spy + }) + render(root, {tag: parent, children: [child]}) + try { + render(root, {tag: parent}) + } catch (e) { + threw = true + } + + o(spy.callCount).equals(0) + o(threw).equals(false) + }) + o("onremove doesn't fire on nodes that go from pool to pool (#1990)", function() { + var onremove = o.spy(); + + render(root, [m("div", m("div")), m("div", m("div", {onremove: onremove}))]); + render(root, [m("div", m("div"))]); + render(root, []); + + o(onremove.callCount).equals(1) + }) + o("doesn't fire when removing the children of a node that's brought back from the pool (#1991 part 2)", function() { + var onremove = o.spy() + var vnode = {tag: "div", key: 1, children: [{tag: "div", attrs: {onremove: onremove}}]} + var temp = {tag: "div", key: 2} + var updated = {tag: "div", key: 1, children: [{tag: "p"}]} + + render(root, [vnode]) + render(root, [temp]) + render(root, [updated]) + + o(vnode.dom).notEquals(updated.dom) // this used to be a recycling pool test + o(onremove.callCount).equals(1) + }) }) }) }) \ No newline at end of file diff --git a/render/tests/test-onupdate.js b/render/tests/test-onupdate.js index 13f62c46..4b74d288 100644 --- a/render/tests/test-onupdate.js +++ b/render/tests/test-onupdate.js @@ -170,9 +170,9 @@ o.spec("onupdate", function() { function update(vnode) { called = true - o(vnode.dom.parentNode.attributes["id"].nodeValue).equals("11") - o(vnode.dom.attributes["id"].nodeValue).equals("22") - o(vnode.dom.childNodes[0].attributes["id"].nodeValue).equals("33") + o(vnode.dom.parentNode.attributes["id"].value).equals("11") + o(vnode.dom.attributes["id"].value).equals("22") + o(vnode.dom.childNodes[0].attributes["id"].value).equals("33") } o(called).equals(true) }) diff --git a/render/tests/test-render.js b/render/tests/test-render.js index 92bcd93c..292433a0 100644 --- a/render/tests/test-render.js +++ b/render/tests/test-render.js @@ -271,4 +271,32 @@ o.spec("render", function() { o(updateA.callCount).equals(2) o(removeA.callCount).equals(1) }) + o("svg namespace is preserved in keyed diff (#1820)", function(){ + // note that this only exerciese one branch of the keyed diff algo + var svg = {tag:"svg", children: [ + {tag:"g", key: 0}, + {tag:"g", key: 1} + ]} + render(root, [svg]) + + o(svg.dom.namespaceURI).equals("http://www.w3.org/2000/svg") + o(svg.dom.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") + o(svg.dom.childNodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") + + svg = {tag:"svg", children: [ + {tag:"g", key: 1, attrs: {x: 1}}, + {tag:"g", key: 2, attrs: {x: 2}} + ]} + render(root, [svg]) + + o(svg.dom.namespaceURI).equals("http://www.w3.org/2000/svg") + o(svg.dom.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") + o(svg.dom.childNodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") + }) + o("the namespace of the root is passed to children", function() { + render(root, [{tag: "svg"}]) + o(root.childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") + render(root.childNodes[0], [{tag: "g"}]) + o(root.childNodes[0].childNodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") + }) }) diff --git a/render/tests/test-updateElement.js b/render/tests/test-updateElement.js index 1774e61f..0e0332b7 100644 --- a/render/tests/test-updateElement.js +++ b/render/tests/test-updateElement.js @@ -21,7 +21,7 @@ o.spec("updateElement", function() { o(updated.dom).equals(vnode.dom) o(updated.dom).equals(root.firstChild) - o(updated.dom.attributes["id"].nodeValue).equals("c") + o(updated.dom.attributes["id"].value).equals("c") }) o("adds attr", function() { var vnode = {tag: "a", attrs: {id: "b"}} @@ -32,7 +32,7 @@ o.spec("updateElement", function() { o(updated.dom).equals(vnode.dom) o(updated.dom).equals(root.firstChild) - o(updated.dom.attributes["title"].nodeValue).equals("d") + o(updated.dom.attributes["title"].value).equals("d") }) o("adds attr from empty attrs", function() { var vnode = {tag: "a"} @@ -43,7 +43,7 @@ o.spec("updateElement", function() { o(updated.dom).equals(vnode.dom) o(updated.dom).equals(root.firstChild) - o(updated.dom.attributes["title"].nodeValue).equals("d") + o(updated.dom.attributes["title"].value).equals("d") }) o("removes attr", function() { var vnode = {tag: "a", attrs: {id: "b", title: "d"}} @@ -192,6 +192,19 @@ o.spec("updateElement", function() { o(updated.dom.style.backgroundColor).equals("") o(updated.dom.style.color).equals("gold") }) + o("does not re-render element styles for equivalent style objects", function() { + var style = {color: "gold"} + var vnode = {tag: "a", attrs: {style: style}} + + render(root, [vnode]) + + root.firstChild.style.color = "red" + style = {color: "gold"} + var updated = {tag: "a", attrs: {style: style}} + render(root, [updated]) + + o(updated.dom.style.color).equals("red") + }) o("replaces el", function() { var vnode = {tag: "a"} var updated = {tag: "b"} @@ -209,7 +222,7 @@ o.spec("updateElement", function() { render(root, [vnode]) render(root, [updated]) - o(updated.dom.attributes["class"].nodeValue).equals("b") + o(updated.dom.attributes["class"].value).equals("b") }) o("updates svg child", function() { var vnode = {tag: "svg", children: [{ @@ -224,7 +237,7 @@ o.spec("updateElement", function() { o(updated.dom.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") }) - o("restores correctly when recycling", function() { + o("doesn't restore since we're not recycling", function() { var vnode = {tag: "div", key: 1} var updated = {tag: "div", key: 2} @@ -237,9 +250,9 @@ o.spec("updateElement", function() { var c = vnode.dom o(root.childNodes.length).equals(1) - o(a).equals(c) + o(a).notEquals(c) // this used to be a recycling pool test }) - o("restores correctly when recycling via map", function() { + o("doesn't restore since we're not recycling (via map)", function() { var a = {tag: "div", key: 1} var b = {tag: "div", key: 2} var c = {tag: "div", key: 3} @@ -256,6 +269,6 @@ o.spec("updateElement", function() { var y = root.childNodes[1] o(root.childNodes.length).equals(3) - o(x).equals(y) + o(x).notEquals(y) // this used to be a recycling pool test }) }) diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index fa5231ea..85a5ffd3 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -614,6 +614,38 @@ o.spec("updateNodes", function() { o(updated[0].dom.nodeName).equals("I") o(updated[0].dom).equals(root.childNodes[0]) }) + o("cached keyed nodes move when the list is reversed", function(){ + var a = {tag: "a", key: "a"} + var b = {tag: "b", key: "b"} + var c = {tag: "c", key: "c"} + var d = {tag: "d", key: "d"} + + render(root, [a, b, c, d]) + render(root, [d, c, b, a]) + + o(root.childNodes.length).equals(4) + o(root.childNodes[0].nodeName).equals("D") + o(root.childNodes[1].nodeName).equals("C") + o(root.childNodes[2].nodeName).equals("B") + o(root.childNodes[3].nodeName).equals("A") + }) + o("cached keyed nodes move when diffed via the map", function() { + var onupdate = o.spy() + var a = {tag: "a", key: "a", attrs: {onupdate: onupdate}} + var b = {tag: "b", key: "b", attrs: {onupdate: onupdate}} + var c = {tag: "c", key: "c", attrs: {onupdate: onupdate}} + var d = {tag: "d", key: "d", attrs: {onupdate: onupdate}} + + render(root, [a, b, c, d]) + render(root, [b, d, a, c]) + + o(root.childNodes.length).equals(4) + o(root.childNodes[0].nodeName).equals("B") + o(root.childNodes[1].nodeName).equals("D") + o(root.childNodes[2].nodeName).equals("A") + o(root.childNodes[3].nodeName).equals("C") + o(onupdate.callCount).equals(0) + }) o("removes then create different bigger", function() { var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}] var temp = [] @@ -776,7 +808,7 @@ o.spec("updateNodes", function() { o(root.childNodes[0].childNodes[1].childNodes.length).equals(1) o(root.childNodes[1].childNodes.length).equals(0) }) - o("recycles", function() { + o("doesn't recycle", function() { var vnodes = [{tag: "div", key: 1}] var temp = [] var updated = [{tag: "div", key: 1}] @@ -785,10 +817,10 @@ o.spec("updateNodes", function() { render(root, temp) render(root, updated) - o(vnodes[0].dom).equals(updated[0].dom) + o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test o(updated[0].dom.nodeName).equals("DIV") }) - o("recycles when not keyed", function() { + o("doesn't recycle when not keyed", function() { var vnodes = [{tag: "div"}] var temp = [] var updated = [{tag: "div"}] @@ -798,19 +830,22 @@ o.spec("updateNodes", function() { render(root, updated) o(root.childNodes.length).equals(1) - o(vnodes[0].dom).equals(updated[0].dom) + o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test o(updated[0].dom.nodeName).equals("DIV") }) - o("recycles deep", function() { + o("doesn't recycle deep", function() { var vnodes = [{tag: "div", children: [{tag: "a", key: 1}]}] var temp = [{tag: "div"}] var updated = [{tag: "div", children: [{tag: "a", key: 1}]}] render(root, vnodes) + + var oldChild = vnodes[0].dom.firstChild + render(root, temp) render(root, updated) - o(vnodes[0].dom.firstChild).equals(updated[0].dom.firstChild) + o(oldChild).notEquals(updated[0].dom.firstChild) // this used to be a recycling pool test o(updated[0].dom.firstChild.nodeName).equals("A") }) o("mixed unkeyed tags are not broken by recycle", function() { @@ -839,6 +874,19 @@ o.spec("updateNodes", function() { o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[1].nodeName).equals("B") }) + o("onremove doesn't fire from nodes in the pool (#1990)", function () { + var onremove = o.spy() + render(root, [ + {tag: "div", children: [{tag: "div", attrs: {onremove: onremove}}]}, + {tag: "div", children: [{tag: "div", attrs: {onremove: onremove}}]} + ]) + render(root, [ + {tag: "div", children: [{tag: "div", attrs: {onremove: onremove}}]} + ]) + render(root,[]) + + o(onremove.callCount).equals(2) + }) o("cached, non-keyed nodes skip diff", function () { var onupdate = o.spy(); var cached = {tag:"a", attrs:{onupdate: onupdate}} @@ -857,6 +905,72 @@ o.spec("updateNodes", function() { o(onupdate.callCount).equals(0) }) + o("keyed cached elements are re-initialized when brought back from the pool (#2003)", function () { + var onupdate = o.spy() + var oncreate = o.spy() + var cached = { + tag: "B", key: 1, children: [ + {tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"} + ] + } + render(root, [{tag: "div", children: [cached]}]) + render(root, []) + render(root, [{tag: "div", children: [cached]}]) + + o(oncreate.callCount).equals(2) + o(onupdate.callCount).equals(0) + }) + + o("unkeyed cached elements are re-initialized when brought back from the pool (#2003)", function () { + var onupdate = o.spy() + var oncreate = o.spy() + var cached = { + tag: "B", children: [ + {tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"} + ] + } + render(root, [{tag: "div", children: [cached]}]) + render(root, []) + render(root, [{tag: "div", children: [cached]}]) + + o(oncreate.callCount).equals(2) + o(onupdate.callCount).equals(0) + }) + + o("keyed cached elements are re-initialized when brought back from nested pools (#2003)", function () { + var onupdate = o.spy() + var oncreate = o.spy() + var cached = { + tag: "B", key: 1, children: [ + {tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"} + ] + } + render(root, [{tag: "div", children: [cached]}]) + render(root, [{tag: "div", children: []}]) + render(root, []) + render(root, [{tag: "div", children: [cached]}]) + + o(oncreate.callCount).equals(2) + o(onupdate.callCount).equals(0) + }) + + o("unkeyed cached elements are re-initialized when brought back from nested pools (#2003)", function () { + var onupdate = o.spy() + var oncreate = o.spy() + var cached = { + tag: "B", children: [ + {tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"} + ] + } + render(root, [{tag: "div", children: [cached]}]) + render(root, [{tag: "div", children: []}]) + render(root, []) + render(root, [{tag: "div", children: [cached]}]) + + o(oncreate.callCount).equals(2) + o(onupdate.callCount).equals(0) + }) + o("null stays in place", function() { var create = o.spy() var update = o.spy() @@ -904,6 +1018,88 @@ o.spec("updateNodes", function() { o(vnode.dom).notEquals(updated.dom) }) + o("don't add back elements from fragments that are restored from the pool #1991", function() { + render(root, [ + {tag: "[", children: []}, + {tag: "[", children: []} + ]) + render(root, [ + {tag: "[", children: []}, + {tag: "[", children: [{tag: "div"}]} + ]) + render(root, [ + {tag: "[", children: [null]} + ]) + render(root, [ + {tag: "[", children: []}, + {tag: "[", children: []} + ]) + + o(root.childNodes.length).equals(0) + }) + o("don't add back elements from fragments that are being removed #1991", function() { + render(root, [ + {tag: "[", children: []}, + {tag: "p"}, + ]) + render(root, [ + {tag: "[", children: [{tag: "div", text: 5}]} + ]) + render(root, [ + {tag: "[", children: []}, + {tag: "[", children: []} + ]) + + o(root.childNodes.length).equals(0) + }) + o("handles null values in unkeyed lists of different length (#2003)", function() { + var oncreate = o.spy(); + var onremove = o.spy(); + var onupdate = o.spy(); + function attrs() { + return {oncreate: oncreate, onremove: onremove, onupdate: onupdate} + } + + render(root, [{tag: "div", attrs: attrs()}, null]); + render(root, [null, {tag: "div", attrs: attrs()}, null]); + + o(oncreate.callCount).equals(2) + o(onremove.callCount).equals(1) + o(onupdate.callCount).equals(0) + }) + o("supports changing the element of a keyed element in a list when traversed bottom-up", function() { + try { + render(root, [{tag: "a", key: 2}]) + render(root, [{tag: "b", key: 1}, {tag: "b", key: 2}]) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("B") + o(root.childNodes[1].nodeName).equals("B") + } catch (e) { + o(e).equals(null) + } + }) + o("supports changing the element of a keyed element in a list when looking up nodes using the map", function() { + try { + render(root, [{tag: "x", key: 1}, {tag: "y", key: 2}, {tag: "z", key: 3}]) + render(root, [{tag: "b", key: 2}, {tag: "c", key: 1}, {tag: "d", key: 4}, {tag: "e", key: 3}]) + + o(root.childNodes.length).equals(4) + o(root.childNodes[0].nodeName).equals("B") + o(root.childNodes[1].nodeName).equals("C") + o(root.childNodes[2].nodeName).equals("D") + o(root.childNodes[3].nodeName).equals("E") + } catch (e) { + o(e).equals(null) + } + }) + o("don't fetch the nextSibling from the pool", function() { + render(root, [{tag: "[", children: [{tag: "div", key: 1}, {tag: "div", key: 2}]}, {tag: "p"}]) + render(root, [{tag: "[", children: []}, {tag: "p"}]) + render(root, [{tag: "[", children: [{tag: "div", key: 2}, {tag: "div", key: 1}]}, {tag: "p"}]) + + o([].map.call(root.childNodes, function(el) {return el.nodeName})).deepEquals(["DIV", "DIV", "P"]) + }) components.forEach(function(cmp){ o.spec(cmp.kind, function(){ var createComponent = cmp.create @@ -940,6 +1136,19 @@ o.spec("updateNodes", function() { o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[1].nodeName).equals("S") }) + o("removing a component that returns a fragment doesn't throw (regression test for incidental bug introduced while debugging some Flems)", function() { + var component = createComponent({ + view: function() {return {tag: "[", children:[{tag: "a"}, {tag: "b"}]}} + }) + try { + render(root, [{tag: component}]) + render(root, []) + + o(root.childNodes.length).equals(0) + } catch (e) { + o(e).equals(null) + } + }) }) }) }) diff --git a/render/vnode.js b/render/vnode.js index ce137703..13ed393f 100644 --- a/render/vnode.js +++ b/render/vnode.js @@ -1,7 +1,7 @@ "use strict" function Vnode(tag, key, attrs, children, text, dom) { - return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: undefined, _state: undefined, events: undefined, instance: undefined, skip: false} + return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: undefined, events: undefined, instance: undefined, skip: false} } Vnode.normalize = function(node) { if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) diff --git a/request/request.js b/request/request.js index 76e218c9..8424f889 100644 --- a/request/request.js +++ b/request/request.js @@ -67,14 +67,16 @@ module.exports = function($window, Promise) { xhr.open(args.method, args.url, typeof args.async === "boolean" ? args.async : true, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined) - if (args.serialize === JSON.stringify && useBody) { + if (args.serialize === JSON.stringify && useBody && !(args.headers && args.headers.hasOwnProperty("Content-Type"))) { xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8") } - if (args.deserialize === deserialize) { + if (args.deserialize === deserialize && !(args.headers && args.headers.hasOwnProperty("Accept"))) { xhr.setRequestHeader("Accept", "application/json, text/*") } if (args.withCredentials) xhr.withCredentials = args.withCredentials + if (args.timeout) xhr.timeout = args.timeout + for (var key in args.headers) if ({}.hasOwnProperty.call(args.headers, key)) { xhr.setRequestHeader(key, args.headers[key]) } @@ -88,12 +90,13 @@ module.exports = function($window, Promise) { if (xhr.readyState === 4) { try { var response = (args.extract !== extract) ? args.extract(xhr, args) : args.deserialize(args.extract(xhr, args)) - if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) { + if (args.extract !== extract || (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) { resolve(cast(args.type, response)) } else { var error = new Error(xhr.responseText) - for (var key in response) error[key] = response[key] + error.code = xhr.status + error.response = response reject(error) } } diff --git a/request/tests/test-request.js b/request/tests/test-request.js index 7f965498..52f840ef 100644 --- a/request/tests/test-request.js +++ b/request/tests/test-request.js @@ -230,7 +230,7 @@ o.spec("xhr", function() { } }) xhr({method: "GET", url: "/item", deserialize: deserialize}).then(function(data) { - o(data).equals('{"test":123}') + o(data).equals("{\"test\":123}") }).then(done) }) o("deserialize parameter works in POST", function(done) { @@ -244,7 +244,7 @@ o.spec("xhr", function() { } }) xhr({method: "POST", url: "/item", deserialize: deserialize}).then(function(data) { - o(data).equals('{"test":123}') + o(data).equals("{\"test\":123}") }).then(done) }) o("extract parameter works in GET", function(done) { @@ -258,7 +258,7 @@ o.spec("xhr", function() { } }) xhr({method: "GET", url: "/item", extract: extract}).then(function(data) { - o(data).equals('{"test":123}') + o(data).equals("{\"test\":123}") }).then(done) }) o("extract parameter works in POST", function(done) { @@ -272,7 +272,7 @@ o.spec("xhr", function() { } }) xhr({method: "POST", url: "/item", extract: extract}).then(function(data) { - o(data).equals('{"test":123}') + o(data).equals("{\"test\":123}") }).then(done) }) o("ignores deserialize if extract is defined", function(done) { @@ -435,6 +435,20 @@ o.spec("xhr", function() { done() }) }) + o("set timeout to xhr instance", function() { + mock.$defineRoutes({ + "GET /item": function() { + return {status: 200, responseText: ""} + } + }) + return xhr({ + method: "GET", url: "/item", + timeout: 42, + config: function(xhr) { + o(xhr.timeout).equals(42) + } + }) + }) /*o("data maintains after interpolate", function() { mock.$defineRoutes({ "PUT /items/:x": function() { @@ -458,9 +472,10 @@ o.spec("xhr", function() { xhr({method: "GET", url: "/item"}).catch(function(e) { o(e instanceof Error).equals(true) o(e.message).equals(JSON.stringify({error: "error"})) + o(e.code).equals(500) }).then(done) }) - o("extends Error with JSON response", function(done) { + o("adds response to Error", function(done) { mock.$defineRoutes({ "GET /item": function() { return {status: 500, responseText: JSON.stringify({message: "error", stack: "error on line 1"})} @@ -468,8 +483,8 @@ o.spec("xhr", function() { }) xhr({method: "GET", url: "/item"}).catch(function(e) { o(e instanceof Error).equals(true) - o(e.message).equals("error") - o(e.stack).equals("error on line 1") + o(e.response.message).equals("error") + o(e.response.stack).equals("error on line 1") }).then(done) }) o("rejects on non-JSON server error", function(done) { @@ -518,5 +533,35 @@ o.spec("xhr", function() { o(e instanceof Error).equals(true) }).then(done) }) + o("does not reject on status error code when extract provided", function(done) { + mock.$defineRoutes({ + "GET /item": function() { + return {status: 500, responseText: JSON.stringify({message: "error"})} + } + }) + xhr({ + method: "GET", url: "/item", + extract: function(xhr) {return JSON.parse(xhr.responseText)} + }).then(function(data) { + o(data.message).equals("error") + done() + }) + }) + o("rejects on error in extract", function(done) { + mock.$defineRoutes({ + "GET /item": function() { + return {status: 200, responseText: JSON.stringify({a: 1})} + } + }) + xhr({ + method: "GET", url: "/item", + extract: function() {throw new Error("error")} + }).catch(function(e) { + o(e instanceof Error).equals(true) + o(e.message).equals("error") + }).then(function() { + done() + }) + }) }) }) diff --git a/stream/LICENSE b/stream/LICENSE new file mode 100644 index 00000000..2aae0f1e --- /dev/null +++ b/stream/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Leo Horie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/stream/README.md b/stream/README.md new file mode 100644 index 00000000..d4cb92c1 --- /dev/null +++ b/stream/README.md @@ -0,0 +1,6 @@ +mithril-stream [![NPM Version](https://img.shields.io/npm/v/mithril-stream.svg)](https://www.npmjs.com/package/mithril-stream) [![NPM License](https://img.shields.io/npm/l/mithril-stream.svg)](https://www.npmjs.com/package/mithril-stream) +============== + +Mithril's `m.stream` as a standalone module. + +See [mithril.js.org/stream.html](https://mithril.js.org/stream.html) for docs/usage. diff --git a/stream/package.json b/stream/package.json index f48972d5..f5f4f86c 100644 --- a/stream/package.json +++ b/stream/package.json @@ -1,6 +1,6 @@ { "name": "mithril-stream", - "version": "1.0.0", + "version": "1.1.0", "description": "Streaming data, mithril-style", "main": "stream.js", "directories": { @@ -9,5 +9,5 @@ "keywords": [ "stream", "reactive", "data" ], "author": "Leo Horie ", "license": "MIT", - "repository": "lhorie/mithril.js" + "repository": "MithrilJS/mithril.js" } diff --git a/stream/stream.js b/stream/stream.js index 18c7e608..e9438480 100644 --- a/stream/stream.js +++ b/stream/stream.js @@ -1,6 +1,7 @@ -"use strict" - +/* eslint-disable */ ;(function() { +"use strict" +/* eslint-enable */ var guid = 0, HALT = {} function createStream() { @@ -116,7 +117,9 @@ function merge(streams) { function scan(reducer, seed, stream) { var newStream = combine(function (s) { - return seed = reducer(seed, s._state.value) + var next = reducer(seed, s._state.value) + if (next !== HALT) return seed = next + return HALT }, [stream]) if (newStream._state.state === 0) newStream(seed) diff --git a/stream/tests/index.html b/stream/tests/index.html index 86cb8947..8db5634b 100644 --- a/stream/tests/index.html +++ b/stream/tests/index.html @@ -8,11 +8,8 @@ - - - diff --git a/stream/tests/test-scan.js b/stream/tests/test-scan.js index db5d885f..2f2c2bb2 100644 --- a/stream/tests/test-scan.js +++ b/stream/tests/test-scan.js @@ -30,4 +30,36 @@ o.spec("scan", function() { o(result[2]).equals(undefined) o(result[3]).deepEquals({a: 1}) }) + + o("reducer can return HALT to prevent child updates", function() { + var count = 0 + var action = stream() + var store = stream.scan(function (arr, value) { + switch (typeof value) { + case "number": + return arr.concat(value) + default: + return stream.HALT + } + }, [], action) + var child = store.map(function (p) { + count++ + return p + }) + var result + + action(7) + action("11") + action(undefined) + action({a: 1}) + + result = child() + + // check we got the expect result + o(result[0]).equals(7) + + // check child received minimum # of updates + o(count).equals(2) + }) + }) diff --git a/test-utils/domMock.js b/test-utils/domMock.js index a827070a..e5c1f68c 100644 --- a/test-utils/domMock.js +++ b/test-utils/domMock.js @@ -1,9 +1,66 @@ "use strict" -module.exports = function() { +/* +Known limitations: +- the innerHTML setter and the DOMParser only support a small subset of the true HTML/XML syntax. +- `option.selected` can't be set/read when the option doesn't have a `select` parent +- `element.attributes` is just a map of attribute names => Attr objects stubs +- ... + +*/ + +/* +options: +- spy:(f: Function) => Function +*/ + +module.exports = function(options) { + options = options || {} + var spy = options.spy || function(f){return f} + var spymap = [] + function registerSpies(element, spies) { + if(options.spy) { + var i = spymap.indexOf(element) + if (i === -1) { + spymap.push(element, spies) + } else { + var existing = spymap[i + 1] + for (var k in spies) existing[k] = spies[k] + } + } + } + function getSpies(element) { + if (element == null || typeof element !== "object") throw new Error("Element expected") + if(options.spy) return spymap[spymap.indexOf(element) + 1] + } + function isModernEvent(type) { return type === "transitionstart" || type === "transitionend" || type === "animationstart" || type === "animationend" } + function dispatchEvent(e) { + var stopped = false + e.stopImmediatePropagation = function() { + e.stopPropagation() + stopped = true + } + e.currentTarget = this + if (this._events[e.type] != null) { + for (var i = 0; i < this._events[e.type].handlers.length; i++) { + var useCapture = this._events[e.type].options[i].capture + if (useCapture && e.eventPhase < 3 || !useCapture && e.eventPhase > 1) { + var handler = this._events[e.type].handlers[i] + if (typeof handler === "function") try {handler.call(this, e)} catch(e) {setTimeout(function(){throw e})} + else try {handler.handleEvent(e)} catch(e) {setTimeout(function(){throw e})} + if (stopped) return + } + } + } + // this is inaccurate. Normally the event fires in definition order, including legacy events + // this would require getters/setters for each of them though and we haven't gotten around to + // adding them since it would be at a high perf cost or would entail some heavy refactoring of + // the mocks (prototypes instead of closures). + if (e.eventPhase > 1 && typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) try {this["on" + e.type](e)} catch(e) {setTimeout(function(){throw e})} + } function appendChild(child) { var ancestor = this while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode @@ -44,6 +101,7 @@ module.exports = function() { if (index > -1) this.childNodes.splice(index, 1) if (reference === null) this.appendChild(child) else { + if (index !== -1 && refIndex > index) refIndex-- if (child.nodeType === 11) { this.childNodes.splice.apply(this.childNodes, [refIndex, 0].concat(child.childNodes)) while (child.firstChild) { @@ -62,14 +120,26 @@ module.exports = function() { } function getAttribute(name) { if (this.attributes[name] == null) return null - return this.attributes[name].nodeValue + return this.attributes[name].value } function setAttribute(name, value) { - var nodeValue = String(value) + /*eslint-disable no-implicit-coercion*/ + // this is the correct kind of conversion, passing a Symbol throws in browsers too. + var nodeValue = "" + value + /*eslint-enable no-implicit-coercion*/ + this.attributes[name] = { namespaceURI: null, + get value() {return nodeValue}, + set value(value) { + /*eslint-disable no-implicit-coercion*/ + nodeValue = "" + value + /*eslint-enable no-implicit-coercion*/ + }, get nodeValue() {return nodeValue}, - set nodeValue(value) {nodeValue = String(value)}, + set nodeValue(value) { + this.value = value + } } } function setAttributeNS(ns, name, value) { @@ -79,6 +149,9 @@ module.exports = function() { function removeAttribute(name) { delete this.attributes[name] } + function hasAttribute(name) { + return name in this.attributes + } var declListTokenizer = /;|"(?:\\.|[^"\n])*"|'(?:\\.|[^'\n])*'/g /** * This will split a semicolon-separated CSS declaration list into an array of @@ -111,9 +184,43 @@ module.exports = function() { res.unshift(declList) return res } - + function parseMarkup(value, root, voidElements, xmlns) { + var depth = 0, stack = [root] + value.replace(/<([a-z0-9\-]+?)((?:\s+?[^=]+?=(?:"[^"]*?"|'[^']*?'|[^\s>]*))*?)(\s*\/)?>|<\/([a-z0-9\-]+?)>|([^<]+)/g, function(match, startTag, attrs, selfClosed, endTag, text) { + if (startTag) { + var element = xmlns == null ? $window.document.createElement(startTag) : $window.document.createElementNS(xmlns, startTag) + attrs.replace(/\s+?([^=]+?)=(?:"([^"]*?)"|'([^']*?)'|([^\s>]*))/g, function(match, key, doubleQuoted, singleQuoted, unquoted) { + var keyParts = key.split(":") + var name = keyParts.pop() + var ns = keyParts[0] + var value = doubleQuoted || singleQuoted || unquoted || "" + if (ns != null) element.setAttributeNS(ns, name, value) + else element.setAttribute(name, value) + }) + stack[depth].appendChild(element) + if (!selfClosed && voidElements.indexOf(startTag.toLowerCase()) < 0) stack[++depth] = element + } + else if (endTag) { + depth-- + } + else if (text) { + stack[depth].appendChild($window.document.createTextNode(text)) // FIXME handle html entities + } + }) + } + function DOMParser() {} + DOMParser.prototype.parseFromString = function(src, mime) { + if (mime !== "image/svg+xml") throw new Error("The DOMParser mock only supports the \"image/svg+xml\" MIME type") + var match = src.match(/^(.*)<\/svg>$/) + if (!match) throw new Error("Please provide a bare SVG tag with the xmlns as only attribute") + var value = match[1] + var root = $window.document.createElementNS("http://www.w3.org/2000/svg", "svg") + parseMarkup(value, root, [], "http://www.w3.org/2000/svg") + return {documentElement: root} + } var activeElement var $window = { + DOMParser: DOMParser, document: { createElement: function(tag) { var cssText = "" @@ -150,6 +257,7 @@ module.exports = function() { appendChild: appendChild, removeChild: removeChild, insertBefore: insertBefore, + hasAttribute: hasAttribute, getAttribute: getAttribute, setAttribute: setAttribute, setAttributeNS: setAttributeNS, @@ -171,30 +279,18 @@ module.exports = function() { if (value !== "") this.appendChild($window.document.createTextNode(value)) }, set innerHTML(value) { + var voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"] while (this.firstChild) this.removeChild(this.firstChild) - - var stack = [this], depth = 0, voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"] - value.replace(/<([a-z0-9\-]+?)((?:\s+?[^=]+?=(?:"[^"]*?"|'[^']*?'|[^\s>]*))*?)(\s*\/)?>|<\/([a-z0-9\-]+?)>|([^<]+)/g, function(match, startTag, attrs, selfClosed, endTag, text) { - if (startTag) { - var element = $window.document.createElement(startTag) - attrs.replace(/\s+?([^=]+?)=(?:"([^"]*?)"|'([^']*?)'|([^\s>]*))/g, function(match, key, doubleQuoted, singleQuoted, unquoted) { - var keyParts = key.split(":") - var name = keyParts.pop() - var ns = keyParts[0] - var value = doubleQuoted || singleQuoted || unquoted || "" - if (ns != null) element.setAttributeNS(ns, name, value) - else element.setAttribute(name, value) - }) - stack[depth].appendChild(element) - if (!selfClosed && voidElements.indexOf(startTag.toLowerCase()) < 0) stack[++depth] = element - } - else if (endTag) { - depth-- - } - else if (text) { - stack[depth].appendChild($window.document.createTextNode(text)) // FIXME handle html entities - } - }) + var match = value.match(/^(.*)<\/svg>$/), root, ns + if (match) { + var value = match[1] + root = $window.document.createElementNS("http://www.w3.org/2000/svg", "svg") + ns = "http://www.w3.org/2000/svg" + this.appendChild(root) + } else { + root = this + } + parseMarkup(value, root, voidElements, ns) }, get style() { return style @@ -204,40 +300,106 @@ module.exports = function() { throw new Error("setting element.style is not portable") }, get className() { - return this.attributes["class"] ? this.attributes["class"].nodeValue : "" + return this.attributes["class"] ? this.attributes["class"].value : "" }, set className(value) { if (this.namespaceURI === "http://www.w3.org/2000/svg") throw new Error("Cannot set property className of SVGElement") else this.setAttribute("class", value) }, focus: function() {activeElement = this}, - addEventListener: function(type, callback) { - if (events[type] == null) events[type] = [callback] - else events[type].push(callback) + addEventListener: function(type, handler, options) { + if (arguments.length > 2) { + if (typeof options === "object" && options != null) throw new TypeError("NYI: addEventListener options") + else if (typeof options !== "boolean") throw new TypeError("boolean expected for useCapture") + else options = {capture: options} + } else { + options = {capture: false} + } + if (events[type] == null) events[type] = {handlers: [handler], options: [options]} + else { + var found = false + for (var i = 0; i < events[type].handlers.length; i++) { + if (events[type].handlers[i] === handler && events[type].options[i].capture === options.capture) { + found = true + break + } + } + if (!found) { + events[type].handlers.push(handler) + events[type].options.push(options) + } + } }, - removeEventListener: function(type, callback) { + removeEventListener: function(type, handler, options) { + if (arguments.length > 2) { + if (typeof options === "object" && options != null) throw new TypeError("NYI: addEventListener options") + else if (typeof options !== "boolean") throw new TypeError("boolean expected for useCapture") + else options = {capture: options} + } else { + options = {capture: false} + } if (events[type] != null) { - var index = events[type].indexOf(callback) - if (index > -1) events[type].splice(index, 1) + for (var i = 0; i < events[type].handlers.length; i++) { + if (events[type].handlers[i] === handler && events[type].options[i].capture === options.capture) { + events[type].handlers.splice(i, 1) + events[type].options.splice(i, 1) + break; + } + } } }, dispatchEvent: function(e) { - if (this.nodeName === "INPUT" && this.attributes["type"] != null && this.attributes["type"].nodeValue === "checkbox" && e.type === "click") { - this.checked = !this.checked + var parents = [] + if (this.parentNode != null) { + var parent = this.parentNode + do { + parents.push(parent) + parent = parent.parentNode + } while (parent != null) } - e.target = this - if (events[e.type] != null) { - for (var i = 0; i < events[e.type].length; i++) { - events[e.type][i].call(this, e) + var prevented = false + e.preventDefault = function() { + prevented = true + } + var stopped = false + e.stopPropagation = function() { + stopped = true + } + e.eventPhase = 1 + try { + for (var i = parents.length - 1; 0 <= i; i--) { + dispatchEvent.call(parents[i], e) + if (stopped) { + return + } + } + e.eventPhase = 2 + dispatchEvent.call(this, e) + if (stopped) { + return + } + e.eventPhase = 3 + for (var i = 0; i < parents.length; i++) { + dispatchEvent.call(parents[i], e) + if (stopped) { + return + } + } + } catch(e) { + throw e + } finally { + e.eventPhase = 0 + if (!prevented) { + if (this.nodeName === "INPUT" && this.attributes["type"] != null && this.attributes["type"].value === "checkbox" && e.type === "click") { + this.checked = !this.checked + } } } - e.preventDefault = function() { - // TODO: should this do something? - } - if (typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) this["on" + e.type](e) + }, onclick: null, + _events: events } if (element.nodeName === "A") { @@ -256,30 +418,73 @@ module.exports = function() { enumerable: true, }) - element.value = "" - } - - if (element.nodeName === "TEXTAREA") { - var value + var value = "" + var valueSetter = spy(function(v) { + /*eslint-disable no-implicit-coercion*/ + value = v === null ? "" : "" + v + /*eslint-enable no-implicit-coercion*/ + }) Object.defineProperty(element, "value", { get: function() { - return value != null ? value : - this.firstChild ? this.firstChild.nodeValue : "" + return value }, - set: function(v) {value = v}, + set: valueSetter, enumerable: true, }) + + // we currently emulate the non-ie behavior, but emulating ie may be more useful (throw when an invalid type is set) + var typeSetter = spy(function(v) { + this.setAttribute("type", v) + }) + Object.defineProperty(element, "type", { + get: function() { + if (!this.hasAttribute("type")) return "text" + var type = this.getAttribute("type") + return (/^(?:radio|button|checkbox|color|date|datetime|datetime-local|email|file|hidden|month|number|password|range|research|search|submit|tel|text|url|week|image)$/) + .test(type) + ? type + : "text" + }, + set: typeSetter, + enumerable: true, + }) + registerSpies(element, { + valueSetter: valueSetter, + typeSetter: typeSetter + }) + } + + + if (element.nodeName === "TEXTAREA") { + var wasNeverSet = true + var value = "" + var valueSetter = spy(function(v) { + wasNeverSet = false + /*eslint-disable no-implicit-coercion*/ + value = v === null ? "" : "" + v + /*eslint-enable no-implicit-coercion*/ + }) + Object.defineProperty(element, "value", { + get: function() { + return wasNeverSet && this.firstChild ? this.firstChild.nodeValue : value + }, + set: valueSetter, + enumerable: true, + }) + registerSpies(element, { + valueSetter: valueSetter + }) } /* eslint-disable radix */ if (element.nodeName === "CANVAS") { Object.defineProperty(element, "width", { - get: function() {return this.attributes["width"] ? Math.floor(parseInt(this.attributes["width"].nodeValue) || 0) : 300}, + get: function() {return this.attributes["width"] ? Math.floor(parseInt(this.attributes["width"].value) || 0) : 300}, set: function(value) {this.setAttribute("width", Math.floor(Number(value) || 0).toString())}, }) Object.defineProperty(element, "height", { - get: function() {return this.attributes["height"] ? Math.floor(parseInt(this.attributes["height"].nodeValue) || 0) : 300}, + get: function() {return this.attributes["height"] ? Math.floor(parseInt(this.attributes["height"].value) || 0) : 300}, set: function(value) {this.setAttribute("height", Math.floor(Number(value) || 0).toString())}, }) } @@ -296,7 +501,7 @@ module.exports = function() { } function getOptionValue(element) { return element.attributes["value"] != null ? - element.attributes["value"].nodeValue : + element.attributes["value"].value : element.firstChild != null ? element.firstChild.nodeValue : "" } if (element.nodeName === "SELECT") { @@ -317,14 +522,14 @@ module.exports = function() { }, enumerable: true, }) - Object.defineProperty(element, "value", { - get: function() { - if (this.selectedIndex > -1) return getOptionValue(getOptions(this)[this.selectedIndex]) - return "" - }, - set: function(value) { + var valueSetter = spy(function(value) { + if (value === null) { + selectedIndex = -1 + } else { var options = getOptions(this) - var stringValue = String(value) + /*eslint-disable no-implicit-coercion*/ + var stringValue = "" + value + /*eslint-enable no-implicit-coercion*/ for (var i = 0; i < options.length; i++) { if (getOptionValue(options[i]) === stringValue) { // selectedValue = stringValue @@ -334,19 +539,37 @@ module.exports = function() { } // selectedValue = stringValue selectedIndex = -1 + } + }) + Object.defineProperty(element, "value", { + get: function() { + if (this.selectedIndex > -1) return getOptionValue(getOptions(this)[this.selectedIndex]) + return "" }, + set: valueSetter, enumerable: true, }) + registerSpies(element, { + valueSetter: valueSetter + }) } if (element.nodeName === "OPTION") { + var valueSetter = spy(function(value) { + /*eslint-disable no-implicit-coercion*/ + this.setAttribute("value", "" + value) + /*eslint-enable no-implicit-coercion*/ + }) Object.defineProperty(element, "value", { get: function() {return getOptionValue(this)}, - set: function(value) { - this.setAttribute("value", value) - }, + set: valueSetter, enumerable: true, }) + registerSpies(element, { + valueSetter: valueSetter + }) + Object.defineProperty(element, "selected", { + // TODO? handle `selected` without a parent (works in browsers) get: function() { var options = getOptions(this.parentNode) var index = options.indexOf(this) @@ -372,13 +595,19 @@ module.exports = function() { return element }, createTextNode: function(text) { - var nodeValue = String(text) + /*eslint-disable no-implicit-coercion*/ + var nodeValue = "" + text + /*eslint-enable no-implicit-coercion*/ return { nodeType: 3, nodeName: "#text", parentNode: null, get nodeValue() {return nodeValue}, - set nodeValue(value) {nodeValue = String(value)}, + set nodeValue(value) { + /*eslint-disable no-implicit-coercion*/ + nodeValue = "" + value + /*eslint-enable no-implicit-coercion*/ + }, } }, createDocumentFragment: function() { @@ -397,7 +626,8 @@ module.exports = function() { }, createEvent: function() { return { - initEvent: function(type) {this.type = type}, + eventPhase: 0, + initEvent: function(type) {this.type = type} } }, get activeElement() {return activeElement}, @@ -409,5 +639,7 @@ module.exports = function() { $window.document.documentElement.appendChild($window.document.body) activeElement = $window.document.body + if (options.spy) $window.__getSpies = getSpies + return $window } diff --git a/test-utils/tests/index.html b/test-utils/tests/index.html index 51b04d73..a0edd42a 100644 --- a/test-utils/tests/index.html +++ b/test-utils/tests/index.html @@ -7,21 +7,20 @@ - - + - + diff --git a/test-utils/tests/test-domMock.js b/test-utils/tests/test-domMock.js index aee68832..e27d0bee 100644 --- a/test-utils/tests/test-domMock.js +++ b/test-utils/tests/test-domMock.js @@ -4,9 +4,10 @@ var o = require("../../ospec/ospec") var domMock = require("../../test-utils/domMock") o.spec("domMock", function() { - var $document + var $document, $window o.beforeEach(function() { - $document = domMock().document + $window = domMock() + $document = $window.document }) o.spec("createElement", function() { @@ -77,6 +78,27 @@ o.spec("domMock", function() { o(node.nodeValue).equals("true") }) + if (typeof Symbol === "function") { + o("doesn't work with symbols", function(){ + var threw = false + try { + $document.createTextNode(Symbol("nono")) + } catch(e) { + threw = true + } + o(threw).equals(true) + }) + o("symbols can't be used as nodeValue", function(){ + var threw = false + try { + var node = $document.createTextNode("a") + node.nodeValue = Symbol("nono") + } catch(e) { + threw = true + } + o(threw).equals(true) + }) + } }) o.spec("createDocumentFragment", function() { @@ -220,6 +242,28 @@ o.spec("domMock", function() { o(a.parentNode).equals(parent) o(b.parentNode).equals(parent) }) + o("moves existing node forward but not at the end", function() { + var parent = $document.createElement("div") + var a = $document.createElement("a") + var b = $document.createElement("b") + var c = $document.createElement("c") + parent.appendChild(a) + parent.appendChild(b) + parent.appendChild(c) + parent.insertBefore(a, c) + + o(parent.childNodes.length).equals(3) + o(parent.childNodes[0]).equals(b) + o(parent.childNodes[1]).equals(a) + o(parent.childNodes[2]).equals(c) + o(parent.firstChild).equals(b) + o(parent.firstChild.nextSibling).equals(a) + o(parent.firstChild.nextSibling.nextSibling).equals(c) + o(a.parentNode).equals(parent) + o(b.parentNode).equals(parent) + o(c.parentNode).equals(parent) + + }) o("removes from old parent", function() { var parent = $document.createElement("div") var source = $document.createElement("span") @@ -327,6 +371,7 @@ o.spec("domMock", function() { var div = $document.createElement("div") div.setAttribute("id", "aaa") + o(div.attributes["id"].value).equals("aaa") o(div.attributes["id"].nodeValue).equals("aaa") o(div.attributes["id"].namespaceURI).equals(null) }) @@ -334,32 +379,51 @@ o.spec("domMock", function() { var div = $document.createElement("div") div.setAttribute("id", 123) - o(div.attributes["id"].nodeValue).equals("123") + o(div.attributes["id"].value).equals("123") }) o("works w/ null", function() { var div = $document.createElement("div") div.setAttribute("id", null) - o(div.attributes["id"].nodeValue).equals("null") + o(div.attributes["id"].value).equals("null") }) o("works w/ undefined", function() { var div = $document.createElement("div") div.setAttribute("id", undefined) - o(div.attributes["id"].nodeValue).equals("undefined") + o(div.attributes["id"].value).equals("undefined") }) o("works w/ object", function() { var div = $document.createElement("div") div.setAttribute("id", {}) - o(div.attributes["id"].nodeValue).equals("[object Object]") + o(div.attributes["id"].value).equals("[object Object]") }) o("setting via attributes map stringifies", function() { var div = $document.createElement("div") div.setAttribute("id", "a") - div.attributes["id"].nodeValue = 123 + div.attributes["id"].value = 123 - o(div.attributes["id"].nodeValue).equals("123") + o(div.attributes["id"].value).equals("123") + + div.attributes["id"].nodeValue = 456 + + o(div.attributes["id"].value).equals("456") + }) + }) + o.spec("hasAttribute", function() { + o("works", function() { + var div = $document.createElement("div") + + o(div.hasAttribute("id")).equals(false) + + div.setAttribute("id", "aaa") + + o(div.hasAttribute("id")).equals(true) + + div.removeAttribute("id") + + o(div.hasAttribute("id")).equals(false) }) }) @@ -368,14 +432,14 @@ o.spec("domMock", function() { var div = $document.createElement("div") div.setAttributeNS("http://www.w3.org/1999/xlink", "href", "aaa") - o(div.attributes["href"].nodeValue).equals("aaa") + o(div.attributes["href"].value).equals("aaa") o(div.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") }) o("works w/ number", function() { var div = $document.createElement("div") div.setAttributeNS("http://www.w3.org/1999/xlink", "href", 123) - o(div.attributes["href"].nodeValue).equals("123") + o(div.attributes["href"].value).equals("123") o(div.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink") }) }) @@ -416,18 +480,18 @@ o.spec("domMock", function() { o(div.childNodes[0].nodeName).equals("BR") o(div.childNodes[1].nodeType).equals(1) o(div.childNodes[1].nodeName).equals("A") - o(div.childNodes[1].attributes["class"].nodeValue).equals("aaa") - o(div.childNodes[1].attributes["id"].nodeValue).equals("xyz") + o(div.childNodes[1].attributes["class"].value).equals("aaa") + o(div.childNodes[1].attributes["id"].value).equals("xyz") o(div.childNodes[1].childNodes[0].nodeType).equals(3) o(div.childNodes[1].childNodes[0].nodeValue).equals("123") o(div.childNodes[1].childNodes[1].nodeType).equals(1) o(div.childNodes[1].childNodes[1].nodeName).equals("B") - o(div.childNodes[1].childNodes[1].attributes["class"].nodeValue).equals("bbb") + o(div.childNodes[1].childNodes[1].attributes["class"].value).equals("bbb") o(div.childNodes[1].childNodes[2].nodeType).equals(3) o(div.childNodes[1].childNodes[2].nodeValue).equals("234") o(div.childNodes[1].childNodes[3].nodeType).equals(1) o(div.childNodes[1].childNodes[3].nodeName).equals("BR") - o(div.childNodes[1].childNodes[3].attributes["class"].nodeValue).equals("ccc") + o(div.childNodes[1].childNodes[3].attributes["class"].value).equals("ccc") o(div.childNodes[1].childNodes[4].nodeType).equals(3) o(div.childNodes[1].childNodes[4].nodeValue).equals("345") }) @@ -456,6 +520,45 @@ o.spec("domMock", function() { o(a.parentNode).equals(null) }) + o("empty SVG document", function() { + var div = $document.createElement("div") + div.innerHTML = "" + + o(typeof div.firstChild).notEquals(undefined) + o(div.firstChild.nodeName).equals("svg") + o(div.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + o(div.firstChild.childNodes.length).equals(0) + }) + o("text elements", function() { + var div = $document.createElement("div") + div.innerHTML = + "" + + "hello" + + " " + + "world" + + "" + + o(div.firstChild.nodeName).equals("svg") + o(div.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg") + + var nodes = div.firstChild.childNodes + o(nodes.length).equals(3) + o(nodes[0].nodeName).equals("text") + o(nodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") + o(nodes[0].childNodes.length).equals(1) + o(nodes[0].childNodes[0].nodeName).equals("#text") + o(nodes[0].childNodes[0].nodeValue).equals("hello") + o(nodes[1].nodeName).equals("text") + o(nodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") + o(nodes[1].childNodes.length).equals(1) + o(nodes[1].childNodes[0].nodeName).equals("#text") + o(nodes[1].childNodes[0].nodeValue).equals(" ") + o(nodes[2].nodeName).equals("text") + o(nodes[2].namespaceURI).equals("http://www.w3.org/2000/svg") + o(nodes[2].childNodes.length).equals(1) + o(nodes[2].childNodes[0].nodeName).equals("#text") + o(nodes[2].childNodes[0].nodeValue).equals("world") + }) }) o.spec("focus", function() { o("body is active by default", function() { @@ -570,13 +673,57 @@ o.spec("domMock", function() { o(spy.args[0].type).equals("click") o(spy.args[0].target).equals(div) }) - o("removeEventListener works", function(done) { + o("removeEventListener works (bubbling phase)", function() { div.addEventListener("click", spy, false) div.removeEventListener("click", spy, false) div.dispatchEvent(e) o(spy.callCount).equals(0) - done() + }) + o("removeEventListener works (capture phase)", function() { + div.addEventListener("click", spy, true) + div.removeEventListener("click", spy, true) + div.dispatchEvent(e) + + o(spy.callCount).equals(0) + }) + o("removeEventListener is selective (bubbling phase)", function() { + var other = o.spy() + div.addEventListener("click", spy, false) + div.addEventListener("click", other, false) + div.removeEventListener("click", spy, false) + div.dispatchEvent(e) + + o(spy.callCount).equals(0) + o(other.callCount).equals(1) + }) + o("removeEventListener is selective (capture phase)", function() { + var other = o.spy() + div.addEventListener("click", spy, true) + div.addEventListener("click", other, true) + div.removeEventListener("click", spy, true) + div.dispatchEvent(e) + + o(spy.callCount).equals(0) + o(other.callCount).equals(1) + }) + o("removeEventListener only removes the handler related to a given phase (1/2)", function() { + spy = o.spy(function(e) {o(e.eventPhase).equals(3)}) + $document.body.addEventListener("click", spy, true) + $document.body.addEventListener("click", spy, false) + $document.body.removeEventListener("click", spy, true) + div.dispatchEvent(e) + + o(spy.callCount).equals(1) + }) + o("removeEventListener only removes the handler related to a given phase (2/2)", function() { + spy = o.spy(function(e) {o(e.eventPhase).equals(1)}) + $document.body.addEventListener("click", spy, true) + $document.body.addEventListener("click", spy, false) + $document.body.removeEventListener("click", spy, false) + div.dispatchEvent(e) + + o(spy.callCount).equals(1) }) o("click fires onclick", function() { div.onclick = spy @@ -614,6 +761,488 @@ o.spec("domMock", function() { done() }) }) + o.spec("capture and bubbling phases", function() { + var div, e + o.beforeEach(function() { + div = $document.createElement("div") + e = $document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + $document.body.appendChild(div) + }) + o.afterEach(function() { + $document.body.removeChild(div) + }) + o("capture and bubbling events both fire on the target in the order they were defined, regardless of the phase", function () { + var sequence = [] + var capture = o.spy(function(ev){ + sequence.push("capture") + + o(ev).equals(e) + o(ev.eventPhase).equals(2) + o(ev.target).equals(div) + o(ev.currentTarget).equals(div) + }) + var bubble = o.spy(function(ev){ + sequence.push("bubble") + + o(ev).equals(e) + o(ev.eventPhase).equals(2) + o(ev.target).equals(div) + o(ev.currentTarget).equals(div) + }) + + div.addEventListener("click", bubble, false) + div.addEventListener("click", capture, true) + div.dispatchEvent(e) + + o(capture.callCount).equals(1) + o(bubble.callCount).equals(1) + o(sequence).deepEquals(["bubble", "capture"]) + }) + o("capture and bubbling events both fire on the parent", function () { + var sequence = [] + var capture = o.spy(function(ev){ + sequence.push("capture") + + o(ev).equals(e) + o(ev.eventPhase).equals(1) + o(ev.target).equals(div) + o(ev.currentTarget).equals($document.body) + }) + var bubble = o.spy(function(ev){ + sequence.push("bubble") + + o(ev).equals(e) + o(ev.eventPhase).equals(3) + o(ev.target).equals(div) + o(ev.currentTarget).equals($document.body) + }) + + $document.body.addEventListener("click", bubble, false) + $document.body.addEventListener("click", capture, true) + div.dispatchEvent(e) + + o(capture.callCount).equals(1) + o(bubble.callCount).equals(1) + o(sequence).deepEquals(["capture", "bubble"]) + }) + o("useCapture defaults to false", function () { + var sequence = [] + var parent = o.spy(function(ev){ + sequence.push("parent") + + o(ev).equals(e) + o(ev.eventPhase).equals(3) + o(ev.target).equals(div) + o(ev.currentTarget).equals($document.body) + }) + var target = o.spy(function(ev){ + sequence.push("target") + + o(ev).equals(e) + o(ev.eventPhase).equals(2) + o(ev.target).equals(div) + o(ev.currentTarget).equals(div) + }) + + $document.body.addEventListener("click", parent) + div.addEventListener("click", target) + div.dispatchEvent(e) + + o(parent.callCount).equals(1) + o(target.callCount).equals(1) + o(sequence).deepEquals(["target", "parent"]) + }) + o("legacy handlers fire on the bubbling phase", function () { + var sequence = [] + var parent = o.spy(function(ev){ + sequence.push("parent") + + o(ev).equals(e) + o(ev.eventPhase).equals(3) + o(ev.target).equals(div) + o(ev.currentTarget).equals($document.body) + }) + var target = o.spy(function(ev){ + sequence.push("target") + + o(ev).equals(e) + o(ev.eventPhase).equals(2) + o(ev.target).equals(div) + o(ev.currentTarget).equals(div) + }) + + $document.body.addEventListener("click", parent) + $document.body.onclick = parent + div.addEventListener("click", target) + div.dispatchEvent(e) + + o(parent.callCount).equals(2) + o(target.callCount).equals(1) + o(sequence).deepEquals(["target", "parent", "parent"]) + }) + o("events do not propagate to child nodes", function() { + var target = o.spy(function(ev){ + o(ev).equals(e) + o(ev.eventPhase).equals(2) + o(ev.target).equals($document.body) + o(ev.currentTarget).equals($document.body) + }) + var child = o.spy(function(){ + }) + + $document.body.addEventListener("click", target) + div.addEventListener("click", child) + $document.body.dispatchEvent(e) + + o(target.callCount).equals(1) + o(child.callCount).equals(0) + }) + o("e.stopPropagation 1/6", function () { + var capParent = o.spy(function(e){e.stopPropagation()}) + var capTarget = o.spy() + var bubTarget = o.spy() + var legacyTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(0) + o(bubTarget.callCount).equals(0) + o(legacyTarget.callCount).equals(0) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + o("e.stopPropagation 2/6", function () { + var capParent = o.spy() + var capTarget = o.spy(function(e){e.stopPropagation()}) + var bubTarget = o.spy() + var legacyTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + + o("e.stopPropagation 3/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var bubTarget = o.spy(function(e){e.stopPropagation()}) + var legacyTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + o("e.stopPropagation 4/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var bubTarget = o.spy() + var legacyTarget = o.spy(function(e){e.stopPropagation()}) + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + o("e.stopPropagation 5/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var bubTarget = o.spy() + var legacyTarget = o.spy() + var bubParent = o.spy(function(e){e.stopPropagation()}) + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(1) + o(legacyParent.callCount).equals(1) + }) + o("e.stopPropagation 6/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var legacyTarget = o.spy() + var bubTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy(function(e){e.stopPropagation()}) + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(1) + o(legacyParent.callCount).equals(1) + }) + o("e.stopImmediatePropagation 1/6", function () { + var capParent = o.spy(function(e){e.stopImmediatePropagation()}) + var capTarget = o.spy() + var bubTarget = o.spy() + var legacyTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(0) + o(bubTarget.callCount).equals(0) + o(legacyTarget.callCount).equals(0) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + o("e.stopImmediatePropagation 2/6", function () { + var capParent = o.spy() + var capTarget = o.spy(function(e){e.stopImmediatePropagation()}) + var bubTarget = o.spy() + var legacyTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(0) + o(legacyTarget.callCount).equals(0) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + + o("e.stopImmediatePropagation 3/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var bubTarget = o.spy(function(e){e.stopImmediatePropagation()}) + var legacyTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(0) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + o("e.stopImmediatePropagation 4/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var bubTarget = o.spy() + var legacyTarget = o.spy(function(e){e.stopImmediatePropagation()}) + var bubParent = o.spy() + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(0) + o(legacyParent.callCount).equals(0) + }) + o("e.stopImmediatePropagation 5/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var bubTarget = o.spy() + var legacyTarget = o.spy() + var bubParent = o.spy(function(e){e.stopImmediatePropagation()}) + var legacyParent = o.spy() + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(1) + o(legacyParent.callCount).equals(0) + }) + o("e.stopImmediatePropagation 6/6", function () { + var capParent = o.spy() + var capTarget = o.spy() + var legacyTarget = o.spy() + var bubTarget = o.spy() + var bubParent = o.spy() + var legacyParent = o.spy(function(e){e.stopImmediatePropagation()}) + + $document.body.addEventListener("click", capParent, true) + $document.body.addEventListener("click", bubParent, false) + $document.body.onclick = legacyParent + + div.addEventListener("click", capTarget, true) + div.addEventListener("click", bubTarget, false) + div.onclick = legacyTarget + + div.dispatchEvent(e) + + o(capParent.callCount).equals(1) + o(capTarget.callCount).equals(1) + o(bubTarget.callCount).equals(1) + o(legacyTarget.callCount).equals(1) + o(bubParent.callCount).equals(1) + o(legacyParent.callCount).equals(1) + }) + o("errors thrown in handlers don't interrupt the chain", function(done) { + var errMsg = "The presence of these six errors in the log is expected in non-Node.js environments" + var handler = o.spy(function(){throw errMsg}) + + $document.body.addEventListener("click", handler, true) + $document.body.addEventListener("click", handler, false) + $document.body.onclick = handler + + div.addEventListener("click", handler, true) + div.addEventListener("click", handler, false) + div.onclick = handler + + div.dispatchEvent(e) + + o(handler.callCount).equals(6) + + // Swallow the async errors in NodeJS + if (typeof process !== "undefined" && typeof process.once === "function"){ + process.once("uncaughtException", function(e) { + if (e !== errMsg) throw e + process.once("uncaughtException", function(e) { + if (e !== errMsg) throw e + process.once("uncaughtException", function(e) { + if (e !== errMsg) throw e + process.once("uncaughtException", function(e) { + if (e !== errMsg) throw e + process.once("uncaughtException", function(e) { + if (e !== errMsg) throw e + process.once("uncaughtException", function(e) { + if (e !== errMsg) throw e + done() + }) + }) + }) + }) + }) + }) + } else { + done() + } + }) + }) }) o.spec("attributes", function() { o.spec("a[href]", function() { @@ -628,14 +1257,14 @@ o.spec("domMock", function() { a.setAttribute("href", "") o(a.href).notEquals("") - o(a.attributes["href"].nodeValue).equals("") + o(a.attributes["href"].value).equals("") }) o("is path if property is set", function() { var a = $document.createElement("a") a.href = "" o(a.href).notEquals("") - o(a.attributes["href"].nodeValue).equals("") + o(a.attributes["href"].value).equals("") }) }) o.spec("input[checked]", function() { @@ -656,7 +1285,7 @@ o.spec("domMock", function() { input.setAttribute("checked", "") o(input.checked).equals(true) - o(input.attributes["checked"].nodeValue).equals("") + o(input.attributes["checked"].value).equals("") input.removeAttribute("checked") @@ -690,6 +1319,18 @@ o.spec("domMock", function() { o(input.checked).equals(true) }) + o("doesn't toggle on click when preventDefault() is used", function() { + var input = $document.createElement("input") + input.setAttribute("type", "checkbox") + input.checked = false + input.onclick = function(e) {e.preventDefault()} + + var e = $document.createEvent("MouseEvents") + e.initEvent("click", true, true) + input.dispatchEvent(e) + + o(input.checked).equals(false) + }) }) o.spec("input[value]", function() { o("only exists in input elements", function() { @@ -699,20 +1340,95 @@ o.spec("domMock", function() { o("value" in input).equals(true) o("value" in a).equals(false) }) + o("converts null to ''", function() { + var input = $document.createElement("input") + input.value = "x" + + o(input.value).equals("x") + + input.value = null + + o(input.value).equals("") + }) + o("converts values to strings", function() { + var input = $document.createElement("input") + input.value = 5 + + o(input.value).equals("5") + + input.value = 0 + + o(input.value).equals("0") + + input.value = undefined + + o(input.value).equals("undefined") + }) + if (typeof Symbol === "function") o("throws when set to a symbol", function() { + var threw = false + var input = $document.createElement("input") + try { + input.value = Symbol("") + } catch (e) { + o(e instanceof TypeError).equals(true) + threw = true + } + + o(input.value).equals("") + o(threw).equals(true) + }) + }) + o.spec("input[type]", function(){ + o("only exists in input elements", function() { + var input = $document.createElement("input") + var a = $document.createElement("a") + + o("type" in input).equals(true) + o("type" in a).equals(false) + }) + o("is 'text' by default", function() { + var input = $document.createElement("input") + + o(input.type).equals("text") + }) + "radio|button|checkbox|color|date|datetime|datetime-local|email|file|hidden|month|number|password|range|research|search|submit|tel|text|url|week|image" + .split("|").forEach(function(type) { + o("can be set to " + type, function(){ + var input = $document.createElement("input") + input.type = type + + o(input.getAttribute("type")).equals(type) + o(input.type).equals(type) + }) + o("bad values set the attribute, but the getter corrects to 'text', " + type, function(){ + var input = $document.createElement("input") + input.type = "badbad" + type + + o(input.getAttribute("type")).equals("badbad" + type) + o(input.type).equals("text") + }) + }) }) o.spec("textarea[value]", function() { - o("reads from child if no value", function() { - var input = $document.createElement("textarea") - input.appendChild($document.createTextNode("aaa")) + o("reads from child if no value was ever set", function() { + var textarea = $document.createElement("textarea") + textarea.appendChild($document.createTextNode("aaa")) - o(input.value).equals("aaa") + o(textarea.value).equals("aaa") }) o("ignores child if value set", function() { - var input = $document.createElement("textarea") - input.value = "aaa" - input.setAttribute("value", "bbb") + var textarea = $document.createElement("textarea") + textarea.value = null + textarea.appendChild($document.createTextNode("aaa")) - o(input.value).equals("aaa") + o(textarea.value).equals("") + }) + o("textarea[value] doesn't reflect `attributes.value`", function() { + var textarea = $document.createElement("textarea") + textarea.value = "aaa" + textarea.setAttribute("value", "bbb") + + o(textarea.value).equals("aaa") }) }) o.spec("select[value] and select[selectedIndex]", function() { @@ -773,10 +1489,76 @@ o.spec("domMock", function() { option2.setAttribute("value", "b") select.appendChild(option2) + var option3 = $document.createElement("option") + option3.setAttribute("value", "") + select.appendChild(option3) + + var option4 = $document.createElement("option") + option4.setAttribute("value", "null") + select.appendChild(option4) + select.value = "b" o(select.value).equals("b") o(select.selectedIndex).equals(1) + + select.value = "" + + o(select.value).equals("") + o(select.selectedIndex).equals(2) + + select.value = "null" + + o(select.value).equals("null") + o(select.selectedIndex).equals(3) + + select.value = null + + o(select.value).equals("") + o(select.selectedIndex).equals(-1) + }) + o("setting valid value works with type conversion", function() { + var select = $document.createElement("select") + + var option1 = $document.createElement("option") + option1.setAttribute("value", "0") + select.appendChild(option1) + + var option2 = $document.createElement("option") + option2.setAttribute("value", "undefined") + select.appendChild(option2) + + var option3 = $document.createElement("option") + option3.setAttribute("value", "") + select.appendChild(option3) + + select.value = 0 + + o(select.value).equals("0") + o(select.selectedIndex).equals(0) + + select.value = undefined + + o(select.value).equals("undefined") + o(select.selectedIndex).equals(1) + + if (typeof Symbol === "function") { + var threw = false + try { + select.value = Symbol("x") + } catch (e) { + threw = true + } + o(threw).equals(true) + o(select.value).equals("undefined") + o(select.selectedIndex).equals(1) + } + }) + o("option.value = null is converted to 'null'", function() { + var option = $document.createElement("option") + option.value = null + + o(option.value).equals("null") }) o("setting valid value works with optgroup", function() { var select = $document.createElement("select") @@ -920,55 +1702,55 @@ o.spec("domMock", function() { var canvas = $document.createElement("canvas") canvas.width = 100 - o(canvas.attributes["width"].nodeValue).equals("100") + o(canvas.attributes["width"].value).equals("100") o(canvas.width).equals(100) canvas.height = 100 - o(canvas.attributes["height"].nodeValue).equals("100") + o(canvas.attributes["height"].value).equals("100") o(canvas.height).equals(100) }) o("setting string casts to number", function() { var canvas = $document.createElement("canvas") canvas.width = "100" - o(canvas.attributes["width"].nodeValue).equals("100") + o(canvas.attributes["width"].value).equals("100") o(canvas.width).equals(100) canvas.height = "100" - o(canvas.attributes["height"].nodeValue).equals("100") + o(canvas.attributes["height"].value).equals("100") o(canvas.height).equals(100) }) o("setting float casts to int", function() { var canvas = $document.createElement("canvas") canvas.width = 1.2 - o(canvas.attributes["width"].nodeValue).equals("1") + o(canvas.attributes["width"].value).equals("1") o(canvas.width).equals(1) canvas.height = 1.2 - o(canvas.attributes["height"].nodeValue).equals("1") + o(canvas.attributes["height"].value).equals("1") o(canvas.height).equals(1) }) o("setting percentage fails", function() { var canvas = $document.createElement("canvas") canvas.width = "100%" - o(canvas.attributes["width"].nodeValue).equals("0") + o(canvas.attributes["width"].value).equals("0") o(canvas.width).equals(0) canvas.height = "100%" - o(canvas.attributes["height"].nodeValue).equals("0") + o(canvas.attributes["height"].value).equals("0") o(canvas.height).equals(0) }) o("setting attribute works", function() { var canvas = $document.createElement("canvas") canvas.setAttribute("width", "100%") - o(canvas.attributes["width"].nodeValue).equals("100%") + o(canvas.attributes["width"].value).equals("100%") o(canvas.width).equals(100) canvas.setAttribute("height", "100%") - o(canvas.attributes["height"].nodeValue).equals("100%") + o(canvas.attributes["height"].value).equals("100%") o(canvas.height).equals(100) }) }) @@ -979,7 +1761,7 @@ o.spec("domMock", function() { el.className = "a" o(el.className).equals("a") - o(el.attributes["class"].nodeValue).equals("a") + o(el.attributes["class"].value).equals("a") }) o("setter throws in svg", function(done) { var el = $document.createElementNS("http://www.w3.org/2000/svg", "svg") @@ -991,4 +1773,143 @@ o.spec("domMock", function() { } }) }) + o.spec("spies", function() { + var $window + o.beforeEach(function() { + $window = domMock({spy: o.spy}) + }) + o("basics", function() { + o(typeof $window.__getSpies).equals("function") + o("__getSpies" in domMock()).equals(false) + }) + o("input elements have spies on value and type setters", function() { + var input = $window.document.createElement("input") + + var spies = $window.__getSpies(input) + + o(typeof spies).equals("object") + o(spies).notEquals(null) + o(typeof spies.valueSetter).equals("function") + o(typeof spies.typeSetter).equals("function") + o(spies.valueSetter.callCount).equals(0) + o(spies.typeSetter.callCount).equals(0) + + input.value = "aaa" + input.type = "radio" + + o(spies.valueSetter.callCount).equals(1) + o(spies.valueSetter.this).equals(input) + o(spies.valueSetter.args[0]).equals("aaa") + + o(spies.typeSetter.callCount).equals(1) + o(spies.typeSetter.this).equals(input) + o(spies.typeSetter.args[0]).equals("radio") + }) + o("select elements have spies on value setters", function() { + var select = $window.document.createElement("select") + + var spies = $window.__getSpies(select) + + o(typeof spies).equals("object") + o(spies).notEquals(null) + o(typeof spies.valueSetter).equals("function") + o(spies.valueSetter.callCount).equals(0) + + select.value = "aaa" + + o(spies.valueSetter.callCount).equals(1) + o(spies.valueSetter.this).equals(select) + o(spies.valueSetter.args[0]).equals("aaa") + }) + o("option elements have spies on value setters", function() { + var option = $window.document.createElement("option") + + var spies = $window.__getSpies(option) + + o(typeof spies).equals("object") + o(spies).notEquals(null) + o(typeof spies.valueSetter).equals("function") + o(spies.valueSetter.callCount).equals(0) + + option.value = "aaa" + + o(spies.valueSetter.callCount).equals(1) + o(spies.valueSetter.this).equals(option) + o(spies.valueSetter.args[0]).equals("aaa") + }) + o("textarea elements have spies on value setters", function() { + var textarea = $window.document.createElement("textarea") + + var spies = $window.__getSpies(textarea) + + o(typeof spies).equals("object") + o(spies).notEquals(null) + o(typeof spies.valueSetter).equals("function") + o(spies.valueSetter.callCount).equals(0) + + textarea.value = "aaa" + + o(spies.valueSetter.callCount).equals(1) + o(spies.valueSetter.this).equals(textarea) + o(spies.valueSetter.args[0]).equals("aaa") + }) + }) + o.spec("DOMParser for SVG", function(){ + var $DOMParser + o.beforeEach(function() { + $DOMParser = $window.DOMParser + }) + o("basics", function(){ + o(typeof $DOMParser).equals("function") + + var parser = new $DOMParser() + + o(parser instanceof $DOMParser).equals(true) + o(typeof parser.parseFromString).equals("function") + }) + o("empty document", function() { + var parser = new $DOMParser() + var doc = parser.parseFromString( + "", + "image/svg+xml" + ) + + o(typeof doc.documentElement).notEquals(undefined) + o(doc.documentElement.nodeName).equals("svg") + o(doc.documentElement.namespaceURI).equals("http://www.w3.org/2000/svg") + o(doc.documentElement.childNodes.length).equals(0) + }) + o("text elements", function() { + var parser = new $DOMParser() + var doc = parser.parseFromString( + "" + + "hello" + + " " + + "world" + + "", + "image/svg+xml" + ) + + o(doc.documentElement.nodeName).equals("svg") + o(doc.documentElement.namespaceURI).equals("http://www.w3.org/2000/svg") + + var nodes = doc.documentElement.childNodes + o(nodes.length).equals(3) + o(nodes[0].nodeName).equals("text") + o(nodes[0].namespaceURI).equals("http://www.w3.org/2000/svg") + o(nodes[0].childNodes.length).equals(1) + o(nodes[0].childNodes[0].nodeName).equals("#text") + o(nodes[0].childNodes[0].nodeValue).equals("hello") + o(nodes[1].nodeName).equals("text") + o(nodes[1].namespaceURI).equals("http://www.w3.org/2000/svg") + o(nodes[1].childNodes.length).equals(1) + o(nodes[1].childNodes[0].nodeName).equals("#text") + o(nodes[1].childNodes[0].nodeValue).equals(" ") + o(nodes[2].nodeName).equals("text") + o(nodes[2].namespaceURI).equals("http://www.w3.org/2000/svg") + o(nodes[2].childNodes.length).equals(1) + o(nodes[2].childNodes[0].nodeName).equals("#text") + o(nodes[2].childNodes[0].nodeValue).equals("world") + }) + }) }) diff --git a/test-utils/tests/test-throttleMock.js b/test-utils/tests/test-throttleMock.js new file mode 100644 index 00000000..69920623 --- /dev/null +++ b/test-utils/tests/test-throttleMock.js @@ -0,0 +1,91 @@ +"use strict" + +var o = require("../../ospec/ospec") +var throttleMocker = require("../../test-utils/throttleMock") + +o.spec("throttleMock", function() { + o("works with one callback", function() { + var throttleMock = throttleMocker() + var spy = o.spy() + + o(throttleMock.queueLength()).equals(0) + + var throttled = throttleMock.throttle(spy) + + o(throttleMock.queueLength()).equals(0) + o(spy.callCount).equals(0) + + throttled() + + o(throttleMock.queueLength()).equals(1) + o(spy.callCount).equals(0) + + throttled() + + o(throttleMock.queueLength()).equals(1) + o(spy.callCount).equals(0) + + throttleMock.fire() + + o(throttleMock.queueLength()).equals(0) + o(spy.callCount).equals(1) + + throttleMock.fire() + + o(spy.callCount).equals(1) + }) + o("works with two callbacks", function() { + var throttleMock = throttleMocker() + var spy1 = o.spy() + var spy2 = o.spy() + + o(throttleMock.queueLength()).equals(0) + + var throttled1 = throttleMock.throttle(spy1) + + o(throttleMock.queueLength()).equals(0) + o(spy1.callCount).equals(0) + o(spy2.callCount).equals(0) + + throttled1() + + o(throttleMock.queueLength()).equals(1) + o(spy1.callCount).equals(0) + o(spy2.callCount).equals(0) + + throttled1() + + o(throttleMock.queueLength()).equals(1) + o(spy1.callCount).equals(0) + o(spy2.callCount).equals(0) + + var throttled2 = throttleMock.throttle(spy2) + + o(throttleMock.queueLength()).equals(1) + o(spy1.callCount).equals(0) + o(spy2.callCount).equals(0) + + throttled2() + + o(throttleMock.queueLength()).equals(2) + o(spy1.callCount).equals(0) + o(spy2.callCount).equals(0) + + throttled2() + + o(throttleMock.queueLength()).equals(2) + o(spy1.callCount).equals(0) + o(spy2.callCount).equals(0) + + throttleMock.fire() + + o(throttleMock.queueLength()).equals(0) + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + + throttleMock.fire() + + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + }) +}) diff --git a/test-utils/tests/test-xhrMock.js b/test-utils/tests/test-xhrMock.js index 8bdfcf21..8c64a308 100644 --- a/test-utils/tests/test-xhrMock.js +++ b/test-utils/tests/test-xhrMock.js @@ -72,6 +72,14 @@ o.spec("xhrMock", function() { } xhr.send("a=b") }) + o("Setting a header twice merges the header", function() { + // Source: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader + var xhr = new $window.XMLHttpRequest() + xhr.open("POST", "/test") + xhr.setRequestHeader("Content-Type", "foo") + xhr.setRequestHeader("Content-Type", "bar") + o(xhr.getRequestHeader("Content-Type")).equals("foo, bar") + }) }) o.spec("jsonp", function() { o("works", function(done) { diff --git a/test-utils/throttleMock.js b/test-utils/throttleMock.js new file mode 100644 index 00000000..6cdb5710 --- /dev/null +++ b/test-utils/throttleMock.js @@ -0,0 +1,27 @@ +"use strict" + +module.exports = function() { + var queue = [] + return { + throttle: function(fn) { + var pending = false + return function() { + if (!pending) { + queue.push(function(){ + pending = false + fn() + }) + pending = true + } + } + }, + fire: function() { + var tasks = queue + queue = [] + tasks.forEach(function(fn) {fn()}) + }, + queueLength: function(){ + return queue.length + } + } +} diff --git a/test-utils/xhrMock.js b/test-utils/xhrMock.js index d3bbf5b8..379bce28 100644 --- a/test-utils/xhrMock.js +++ b/test-utils/xhrMock.js @@ -17,7 +17,17 @@ module.exports = function() { var headers = {} var aborted = false this.setRequestHeader = function(header, value) { - headers[header] = value + /* + the behavior of setHeader is not your expected setX API. + If the header is already set, it'll merge with whatever you add + rather than overwrite + Source: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader + */ + if (headers[header]) { + headers[header] += ", " + value; + } else { + headers[header] = value + } } this.getRequestHeader = function(header) { return headers[header] diff --git a/tests/index.html b/tests/index.html index 91db3a04..21f8359b 100644 --- a/tests/index.html +++ b/tests/index.html @@ -9,6 +9,7 @@ + diff --git a/tests/test-api.js b/tests/test-api.js index 3240141c..49938d82 100644 --- a/tests/test-api.js +++ b/tests/test-api.js @@ -163,14 +163,24 @@ o.spec("api", function() { var count = 0 var root = window.document.createElement("div") m.mount(root, createComponent({view: function() {count++}})) + o(count).equals(1) + m.redraw() + o(count).equals(1) setTimeout(function() { - m.redraw() o(count).equals(2) done() }, FRAME_BUDGET) }) + o("sync", function() { + var root = window.document.createElement("div") + var view = o.spy() + m.mount(root, createComponent({view: view})) + o(view.callCount).equals(1) + m.redraw.sync() + o(view.callCount).equals(2) + }) }) }) })