commit 779d64e19a26a4e4944d4f0d7d9e280a27fbc6e5 Author: Uku Taht Date: Mon Sep 2 12:29:19 2019 +0100 Initial commit diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 000000000..8a6391c6a --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,5 @@ +[ + import_deps: [:ecto, :phoenix], + inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], + subdirectories: ["priv/*/migrations"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..d50ff3dd0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +plausible-*.tar + +# If NPM crashes, it generates a log, let's ignore it too. +npm-debug.log + +# The directory NPM downloads your dependencies sources to. +/assets/node_modules/ + +# Since we are building assets from assets/, +# we ignore priv/static. You may want to comment +# this depending on your deployment strategy. +/priv/static/ + +# Files matching config/*.secret.exs pattern contain sensitive +# data and you should not commit them into version control. +# +# Alternatively, you may comment the line below and commit the +# secrets files as long as you replace their contents by environment +# variables. +/config/*.secret.exs diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..965206e5a --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +erlang 21.1 +elixir 1.7.4-otp-21 diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..66c4e414e --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: MIX_ENV=prod mix phx.server diff --git a/README.md b/README.md new file mode 100644 index 000000000..cf039068a --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# Plausible Insights + +Plausible is a simple, lightweight web analytics service that provides the most +important traffic stats without intruding on your visitors' privacy. It collects +unique visitors, referrers, top pages, countries, and device information using a +lightweight script. + +![](https://i.ibb.co/wzWYMYb/screenshot.png) + +[View live demo of our own analytics](https://plausible.io/plausible.io) + +### Why Plausible? + +* **Clutter Free**: Stop digging through complex reports to find what you’re looking for. Plausible presents the most important information to you on a single page. +* **Anonymous**: Measure traffic, not individuals. No personal data or IP addresses are ever stored in our database. Read more about our data policy +* **Lightweight**: Plausible works by loading a script on your website, like Google Analytics. Our script is 14x smaller, making your website quicker to load. + +Interested? [Read more on our website](https://plausible.io) + +### Can Plausible be self-hosted? + +At the moment we don't provide support for easily self-hosting the code. Currently, the purpose of +keeping the code open-source is to be transparent with the community about how we +collect and process data. + +### Technology + +Plausible is a standard Elixir/Phoenix application backed by a PostgreSQL database. On the frontend we use +[TailwindCSS](https://tailwindcss.com/) for styling and some vanilla Javascript for interactive bits. + +### Feedback & Roadmap + +We have a [feedback board](https://feedback.plausible.io/) and a [public roadmap](https://feedback.plausible.io/roadmap). +Please let us know if you have any requests and vote on open issues so we can better prioritize. diff --git a/assets/.babelrc b/assets/.babelrc new file mode 100644 index 000000000..ce33b24d4 --- /dev/null +++ b/assets/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": [ + "@babel/preset-env" + ] +} diff --git a/assets/css/app.css b/assets/css/app.css new file mode 100644 index 000000000..05ce81d73 --- /dev/null +++ b/assets/css/app.css @@ -0,0 +1,201 @@ +@tailwind preflight; +@tailwind components; +@import "modal.css"; +@import "coffee_cup.css"; +@import "tooltip.css"; + +.button { + @apply inline-block bg-indigo text-white text-center font-bold tracking-wide py-3 px-5 rounded no-underline; +} + +.button:hover { + @apply shadow; +} + +.button:focus { + @apply outline-none; +} + +.button-outline { + @apply bg-transparent border border-indigo text-indigo; +} + +.button-sm { + @apply text-sm py-2 px-4; +} + +.button-md { + @apply py-2 px-4; +} + +html { + @apply font-sans text-grey-darkest; +} + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a, button { + @apply no-underline; +} + +blockquote { + @apply my-4 py-2 px-4 border-l-4 border-grey; +} + +@screen xl { + .container { + max-width: 70rem; + } +} + +@tailwind utilities; + +a { + color: inherit; +} + +.light-text { + color: #F0F4F8; +} + +.transition { + transition: all .1s ease-in; +} + +.truncated-label { + @apply no-underline text-grey-darkest; + white-space: nowrap; + width: calc(100% - 40px); + overflow: hidden; + text-overflow: ellipsis; + text-align: left; +} + +.bar { + height: 4px; + width: 100%; + background-color: #f5f6f7; + border-radius: 4px; + margin-top: 8px; + margin-bottom: 24px; +} + +.bar__fill { + height: 100%; + border-radius: 4px; +} + +.pulsating-circle { + position: absolute; + left: 50%; + top: 50%; + transform: translateX(-50%) translateY(-50%); + width: 20px; + height: 20px; +} + +.pulsating-circle:before { + content: ''; + position: relative; + display: block; + width: 300%; + height: 300%; + box-sizing: border-box; + margin-left: -100%; + margin-top: -100%; + border-radius: 45px; + background-color: #01a4e9; + animation: pulse-ring 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite; + @apply bg-green; +} +.pulsating-circle:after { + content: ''; + position: absolute; + left: 0; + top: 0; + display: block; + width: 100%; + height: 100%; + background-color: white; + border-radius: 15px; + animation: pulse-dot 1.25s cubic-bezier(0.455, 0.03, 0.515, 0.955) -.4s infinite; + @apply bg-green; +} + + +@keyframes pulse-ring { + 0% { + transform: scale(.33); + } + 80%, 100% { + opacity: 0; + } +} + +@keyframes pulse-dot { + 0% { + transform: scale(.8); + } + 50% { + transform: scale(1); + } + 100% { + transform: scale(.8); + } +} + +.just-text h1, .just-text h2, .just-text h3 { + margin-top: 1em; + margin-bottom: .5em; +} + +.just-text p { + margin-top: 0; + margin-bottom: 1rem; +} + +.dropdown-content::before { + top: -16px; + right: 64px; + left: auto; +} +.dropdown-content::before { + border: 8px solid transparent; + border-bottom-color: rgba(27,31,35,0.15); +} +.dropdown-content::before, .dropdown-content::after { + position: absolute; + display: inline-block; + content: ""; + } +.dropdown-content::after { + border: 7px solid transparent; + border-bottom-color: #fff; +} +.dropdown-content::after { + top: -14px; + right: 65px; + left: auto; +} + + +.group:hover > .group-hover-fill-red > svg { + fill: #ff6978!important; +} + +.group:hover > .group-hover-fill-red { + color: #ff6978; +} + +.group-hover-fill-red svg { + transition: all .1 ease-in; +} + +.feather { + height: 1em; + width: 1em; + transform: translateY(0.15em); +} diff --git a/assets/css/coffee_cup.css b/assets/css/coffee_cup.css new file mode 100644 index 000000000..a97d62fad --- /dev/null +++ b/assets/css/coffee_cup.css @@ -0,0 +1,22 @@ +.loading { + width: 50px; + height: 50px; +} + +.loading div { + display: inline-block; + width: 50px; + height: 50px; + border: 3px solid #dae1e7; + border-radius: 50%; + border-top-color: #606f7b; + animation: spin 1s ease-in-out infinite; + -webkit-animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { -webkit-transform: rotate(360deg); } +} +@-webkit-keyframes spin { + to { -webkit-transform: rotate(360deg); } +} diff --git a/assets/css/modal.css b/assets/css/modal.css new file mode 100644 index 000000000..c8cdc787b --- /dev/null +++ b/assets/css/modal.css @@ -0,0 +1,68 @@ +.modal { + display: none; +} + +.modal.is-open { + display: block; +} + +.modal[aria-hidden="false"] .modal__overlay { + animation: mmfadeIn .2s cubic-bezier(0.0, 0.0, 0.2, 1); +} + +.modal[aria-hidden="true"] .modal__overlay { + animation: mmfadeOut .2s cubic-bezier(0.0, 0.0, 0.2, 1); +} + +.modal__overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.6); + z-index: 99; + overflow-x: hidden; + overflow-y: auto; +} + +.modal__container { + background-color: #fff; + padding: 1rem 2rem; + max-width: 860px; + border-radius: 4px; + margin: 50px auto; + box-sizing: border-box; + min-height: 509px; +} + +.modal__header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal__close { + background: transparent; + border: 0; + font-size: 20px; + font-weight: bold; +} + +.modal__header .modal__close:before { content: "\2715"; } + +.modal__content { + margin-top: 2rem; + margin-bottom: 2rem; + line-height: 1.5; +} + +@keyframes mmfadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes mmfadeOut { + from { opacity: 1; } + to { opacity: 0; } +} diff --git a/assets/css/tooltip.css b/assets/css/tooltip.css new file mode 100644 index 000000000..d2dd2f474 --- /dev/null +++ b/assets/css/tooltip.css @@ -0,0 +1,43 @@ +[tooltip]{ + position:relative; + display:inline-block; +} + +[tooltip]::before { + transition: .3s; + content: ""; + position: absolute; + top:-6px; + left:50%; + transform: translateX(-50%); + border-width: 4px 6px 0 6px; + border-style: solid; + border-color: rgba(0,0,0,0.8) transparent transparent transparent; + z-index: 99; + opacity:0; +} + +[tooltip]::after { + transition: .3s; + white-space: nowrap; + content: attr(tooltip); + position: absolute; + left:50%; + top:-6px; + transform: translateX(-50%) translateY(-100%); + background: rgba(0,0,0,0.8); + text-align: center; + color: #fff; + font-size: .875rem; + min-width: 80px; + max-width: 320px; + border-radius: 3px; + pointer-events: none; + padding: 4px 8px; + z-index:99; + opacity:0; +} + +[tooltip]:hover::after,[tooltip]:hover::before { + opacity:1 +} diff --git a/assets/js/app.js b/assets/js/app.js new file mode 100644 index 000000000..3a17c5aa0 --- /dev/null +++ b/assets/js/app.js @@ -0,0 +1,34 @@ +import css from "../css/app.css" +import "./polyfills/closest" +import "./stats" +import "./timeframe-selector" +import "phoenix_html" + +const triggers = document.querySelectorAll('[data-dropdown-trigger]') + +for (const trigger of triggers) { + trigger.addEventListener('click', function(e) { + e.stopPropagation() + e.currentTarget.nextElementSibling.classList.remove('hidden') + }) +} + +if (triggers.length > 0) { + document.addEventListener('click', function(e) { + const clickedInDropdown = e.target.closest('[data-dropdown]') + + if (!clickedInDropdown) { + for (const dropdown of document.querySelectorAll('[data-dropdown]')) { + dropdown.classList.add('hidden') + } + } + }) +} + +const flash = document.getElementById('flash') + +if (flash) { + setTimeout(function() { + flash.style.display = 'none' + }, 2500) +} diff --git a/assets/js/p.js b/assets/js/p.js new file mode 100644 index 000000000..83cbcae30 --- /dev/null +++ b/assets/js/p.js @@ -0,0 +1,103 @@ +(function(window, plausibleHost){ + 'use strict'; + + try { + function setCookie(name,value) { + var date = new Date(); + date.setTime(date.getTime() + (3*365*24*60*60*1000)); // 3 YEARS + var expires = "; expires=" + date.toUTCString(); + document.cookie = name + "=" + (value || "") + expires + "; path=/"; + } + + function getCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return null; + } + + function pseudoUUIDv4() { + var d = new Date().getTime(); + if (typeof performance !== 'undefined' && typeof performance.now === 'function'){ + d += performance.now(); //use high-precision timer if available + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); + } + + function ignore(reason) { + console.warn('[Plausible] Ignoring pageview because ' + reason); + } + + function getUrl() { + return window.location.protocol + '//' + window.location.hostname + window.location.pathname + window.location.search; + } + + function page() { + if (/localhost$/.test(window.location.hostname)) return ignore('website is running locally'); + if (window.location.protocol === 'file:') return ignore('website is running locally'); + if (window.document.visibilityState === 'prerender') return ignore('document is prerendering'); + + var existingUid = getCookie('nm_uid'); + + var request = new XMLHttpRequest(); + request.open('POST', plausibleHost + '/api/page', true); + request.setRequestHeader('Content-Type', 'text/plain'); + var uid = existingUid || pseudoUUIDv4() + + request.send(JSON.stringify({ + url: getUrl(), + new_visitor: !existingUid, + uid: uid, + user_agent: window.navigator.userAgent, + referrer: window.document.referrer, + screenWidth: window.innerWidth + })); + + request.onreadystatechange = function() { + if (request.readyState == XMLHttpRequest.DONE) { + if (!existingUid) { + setCookie('nm_uid', uid) + } + } + } + } + + function trackPushState() { + var his = window.history + if (his.pushState) { + var originalFn = his['pushState'] + his.pushState = function() { + originalFn.apply(this, arguments) + page(); + } + } + } + + const functions = { + page: page, + trackPushState: trackPushState + } + + const queue = window.plausible.q || [] + + window.plausible = function() { + var args = [].slice.call(arguments); + var funcName = args.shift(); + functions[funcName].apply(this, args); + }; + + for (var i = 0; i < queue.length; i++) { + window.plausible.apply(this, queue[i]) + } + } catch (e) { + new Image().src = plausibleHost + '/api/error?message=' + encodeURIComponent(e.message); + } +})(window, BASE_URL); diff --git a/assets/js/plausible.js b/assets/js/plausible.js new file mode 100644 index 000000000..94e222bba --- /dev/null +++ b/assets/js/plausible.js @@ -0,0 +1,86 @@ +(function(window, plausibleHost){ + 'use strict'; + + try { + function setCookie(name,value) { + var date = new Date(); + date.setTime(date.getTime() + (3*365*24*60*60*1000)); // 3 YEARS + var expires = "; expires=" + date.toUTCString(); + document.cookie = name + "=" + (value || "") + expires + "; path=/"; + } + + function getCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return null; + } + + function pseudoUUIDv4() { + var d = new Date().getTime(); + if (typeof performance !== 'undefined' && typeof performance.now === 'function'){ + d += performance.now(); //use high-precision timer if available + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); + } + + function ignore(reason) { + console.warn('[Plausible] Ignoring pageview because ' + reason); + } + + function getUrl() { + return window.location.protocol + '//' + window.location.hostname + window.location.pathname + window.location.search; + } + + function page() { + if (/localhost$/.test(window.location.hostname)) return ignore('website is running locally'); + if (window.location.protocol === 'file:') return ignore('website is running locally'); + if (window.document.visibilityState === 'prerender') return ignore('document is prerendering'); + + var existingUid = getCookie('nm_uid'); + + var request = new XMLHttpRequest(); + request.open('POST', plausibleHost + '/api/page', true); + request.setRequestHeader('Content-Type', 'text/plain'); + var uid = existingUid || pseudoUUIDv4() + + request.send(JSON.stringify({ + url: getUrl(), + new_visitor: !existingUid, + uid: uid, + user_agent: window.navigator.userAgent, + referrer: window.document.referrer, + screenWidth: window.innerWidth + })); + + request.onreadystatechange = function() { + if (request.readyState == XMLHttpRequest.DONE) { + if (!existingUid) { + setCookie('nm_uid', uid) + } + } + } + } + + var his = window.history + if (his.pushState) { + var originalFn = his['pushState'] + his.pushState = function() { + originalFn.apply(this, arguments) + page(); + } + } + + page() + } catch (e) { + new Image().src = plausibleHost + '/api/error?message=' + encodeURIComponent(e.message); + } +})(window, BASE_URL); diff --git a/assets/js/polyfills/closest.js b/assets/js/polyfills/closest.js new file mode 100644 index 000000000..0d21966fd --- /dev/null +++ b/assets/js/polyfills/closest.js @@ -0,0 +1,13 @@ +if (window.Element && !Element.prototype.closest) { + Element.prototype.closest = + function(s) { + var matches = (this.document || this.ownerDocument).querySelectorAll(s), + i, + el = this; + do { + i = matches.length; + while (--i >= 0 && matches.item(i) !== el) {}; + } while ((i < 0) && (el = el.parentElement)); + return el; + }; +} diff --git a/assets/js/stats/index.js b/assets/js/stats/index.js new file mode 100644 index 000000000..87a9dbc5a --- /dev/null +++ b/assets/js/stats/index.js @@ -0,0 +1,128 @@ +import Router from './router' +import * as m from './modal' +import {renderMainGraph, renderComparisons} from './main-graph' + +const SPINNER = ` +
+` + +function delayAtLeast(promise, time) { + const delayed = new Promise((resolve) => setTimeout(resolve, time)) + return Promise.all([promise, delayed]).then(function(val) { return val[0] }) +} + +function fetchModal(modal, path) { + const promise = fetch(path) + m.setModalBody(modal, SPINNER) + + delayAtLeast(promise, 200) + .then(function(res) { + return res.text() + }).then(function(res) { + m.setModalBody(modal, res) + router.updateLinkHandlers() + }) +} + +const router = new Router() + +function showModal(domain, endpoint) { + m.showModal({ + onShow: function(modal) { + fetchModal(modal, endpoint + window.location.search) + }, + onClose: function() { + router.navigate('/' + domain + window.location.search) + } + }) +} + +router + .on('/:domain/referrers/:referrer', function(params) { + showModal(params.domain, `/api/${params.domain}/referrers/${params.referrer}`) + }) + .on('/:domain/referrers', function(params) { + showModal(params.domain, `/api/${params.domain}/referrers`) + }) + .on('/:domain/pages', function(params) { + showModal(params.domain, `/api/${params.domain}/pages`) + }) + .on('/:domain/countries', function(params) { + showModal(params.domain, `/api/${params.domain}/countries`) + }) + .on('/:domain/operating-systems', function(params) { + showModal(params.domain, `/api/${params.domain}/operating-systems`) + }) + .on('/:domain/browsers', function(params) { + showModal(params.domain, `/api/${params.domain}/browsers`) + }) + .on('/:domain', function() { + m.ensureModalClosed() + }) + .resolve(); + +const domainEl = document.querySelector('[data-site-domain]') +if (domainEl) { + const domain = domainEl.getAttribute('data-site-domain') + + const promises = [] + + promises.push( + fetch(`/stats/${domain}/main-graph${location.search}`) + .then(res => res.json()) + .then(res => renderMainGraph(res)) + .then(graphData => fetch(`/api/${domain}/compare${location.search || '?'}&pageviews=${graphData.pageviews}&unique_visitors=${graphData.unique_visitors}`)) + .then(res => res.json()) + .then(res => renderComparisons(res)) + ) + + promises.push( + fetch(`/stats/${domain}/referrers${location.search}`) + .then(res => res.text()) + .then((res) => { + document.getElementById('referrer-stats').innerHTML = res + router.updateLinkHandlers() + }) + ) + + promises.push( + fetch(`/stats/${domain}/pages${location.search}`) + .then(res => res.text()) + .then((res) => { + document.getElementById('pages-stats').innerHTML = res + router.updateLinkHandlers() + }) + ) + + promises.push( + fetch(`/stats/${domain}/countries${location.search}`) + .then(res => res.text()) + .then((res) => { + document.getElementById('countries-stats').innerHTML = res + router.updateLinkHandlers() + }) + ) + + Promise.all(promises).then(() => { + fetch(`/stats/${domain}/screen-sizes${location.search}`) + .then(res => res.text()) + .then((res) => { + document.getElementById('screen-sizes-stats').innerHTML = res + router.updateLinkHandlers() + }) + + fetch(`/stats/${domain}/operating-systems${location.search}`) + .then(res => res.text()) + .then((res) => { + document.getElementById('operating-systems-stats').innerHTML = res + router.updateLinkHandlers() + }) + + fetch(`/stats/${domain}/browsers${location.search}`) + .then(res => res.text()) + .then((res) => { + document.getElementById('browsers-stats').innerHTML = res + router.updateLinkHandlers() + }) + }) +} diff --git a/assets/js/stats/main-graph.js b/assets/js/stats/main-graph.js new file mode 100644 index 000000000..83af94bd6 --- /dev/null +++ b/assets/js/stats/main-graph.js @@ -0,0 +1,242 @@ +export function renderMainGraph(graphData) { + const extraClass = graphData.interval === 'hour' ? '' : 'cursor-pointer' + + const TEMPLATE = ` +
+
+
UNIQUE VISITORS
+
+ ${numberFormatter(graphData.unique_visitors)} +
+
+
+
TOTAL PAGEVIEWS
+
+ ${numberFormatter(graphData.pageviews)} +
+
+
+
+ +
+ ` + + const mainGraphDiv = document.getElementById('main-graph') + mainGraphDiv.innerHTML = TEMPLATE + drawGraph(graphData) + return graphData +} + +export function renderComparisons(comparisons) { + const visitorsDiv = document.getElementById('visitors') + const pageviewsDiv = document.getElementById('pageviews') + + if (comparisons.change_pageviews && comparisons.change_visitors) { + const formattedChangeVisitors = numberFormatter(Math.abs(comparisons.change_visitors)) + + if (comparisons.change_visitors >= 0) { + visitorsDiv.innerHTML +=` + ↑ ${formattedChangeVisitors}% + ` + } else if (comparisons.change_visitors < 0) { + visitorsDiv.innerHTML +=` + ↓ ${formattedChangeVisitors}% + ` + } + + const formattedChangePageviews = numberFormatter(Math.abs(comparisons.change_pageviews)) + + if (comparisons.change_pageviews >= 0) { + pageviewsDiv.innerHTML +=` + ↑ ${formattedChangePageviews}% + ` + } else if (comparisons.change_pageviews < 0) { + pageviewsDiv.innerHTML +=` + ↓ ${formattedChangePageviews}% + ` + } + } else { + visitorsDiv.innerHTML +=` + N/A + ` + pageviewsDiv.innerHTML +=` + N/A + ` + } +} + +function dataSets(graphData, ctx) { + var gradient = ctx.createLinearGradient(0, 0, 0, 300); + gradient.addColorStop(0, 'rgba(101,116,205, 0.2)'); + gradient.addColorStop(1, 'rgba(101,116,205, 0)'); + + if (graphData.present_index) { + var dashedPart = graphData.plot.slice(graphData.present_index - 1); + var dashedPlot = (new Array(graphData.plot.length - dashedPart.length)).concat(dashedPart) + for(var i = graphData.present_index; i < graphData.plot.length; i++) { + graphData.plot[i] = undefined + } + + return [{ + label: 'Visitors', + data: graphData.plot, + borderWidth: 3, + borderColor: 'rgba(101,116,205)', + pointBackgroundColor: 'rgba(101,116,205)', + backgroundColor: gradient, + }, + { + label: 'Visitors', + data: dashedPlot, + borderWidth: 3, + borderDash: [5, 10], + borderColor: 'rgba(101,116,205)', + pointBackgroundColor: 'rgba(101,116,205)', + backgroundColor: gradient, + }] + } else { + return [{ + label: 'Visitors', + data: graphData.plot, + borderWidth: 3, + borderColor: 'rgba(101,116,205)', + pointBackgroundColor: 'rgba(101,116,205)', + backgroundColor: gradient, + }] + } +} + +function drawGraph(graphData) { + var ctx = document.getElementById("main-graph-canvas").getContext('2d'); + + new Chart(ctx, { + type: 'line', + data: { + labels: graphData.labels, + datasets: dataSets(graphData, ctx) + }, + options: { + animation: false, + legend: {display: false}, + responsive: true, + elements: {line: {tension: 0.1}, point: {radius: 0}}, + onClick: onClick(graphData), + tooltips: { + mode: 'index', + intersect: false, + xPadding: 10, + yPadding: 10, + titleFontSize: 16, + footerFontSize: 14, + footerFontColor: '#e6e8ff', + backgroundColor: 'rgba(25, 30, 56)', + callbacks: { + title: function(dataPoints) { + var data = dataPoints[0] + if (graphData.interval === 'month') { + return data.yLabel.toLocaleString() + ' visitors in ' + data.xLabel + } else if (graphData.interval === 'date') { + return data.yLabel.toLocaleString() + ' visitors on ' + data.xLabel + } else if (graphData.interval === 'hour') { + return data.yLabel.toLocaleString() + ' visitors at ' + data.xLabel + } + }, + label: function() {}, + afterBody: function(dataPoints) { + if (graphData.interval === 'month') { + return 'Click to view month' + } else if (graphData.interval === 'date') { + return 'Click to view day' + } + } + } + }, + scales: { + yAxes: [{ + ticks: { + callback: numberFormatter, + beginAtZero: true, + autoSkip: true, + maxTicksLimit: 8, + }, + gridLines: { + zeroLineColor: 'transparent', + drawBorder: false, + } + }], + xAxes: [{ + gridLines: { + display: false, + }, + ticks: { + autoSkip: true, + maxTicksLimit: 8, + callback: dateFormatter(graphData), + } + }] + } + } + }); +} + +const THOUSAND = 1000 +const HUNDRED_THOUSAND = 100000 +const MILLION = 1000000 +const HUNDRED_MILLION = 100000000 + +function numberFormatter(num) { + if (num >= THOUSAND && num < MILLION) { + const thousands = num / THOUSAND + if (thousands === Math.floor(thousands) || num >= HUNDRED_THOUSAND) { + return Math.floor(thousands) + 'k' + } else { + return (Math.floor(thousands * 10) / 10) + 'k' + } + } else if (num >= MILLION && num < HUNDRED_MILLION) { + const millions = num / MILLION + if (millions === Math.floor(millions)) { + return Math.floor(millions) + 'm' + } else { + return (Math.floor(millions * 10) / 10) + 'm' + } + } else { + return num + } +} + +const MONTHS = [ + "January", "February", "March", + "April", "May", "June", "July", + "August", "September", "October", + "November", "December" +] + +function dateFormatter(graphData) { + return function(isoDate) { + const date = new Date(isoDate) + + if (graphData.interval === 'month') { + return MONTHS[date.getMonth()]; + } else if (graphData.interval === 'date') { + return date.getDate() + ' ' + MONTHS[date.getMonth()]; + } else if (graphData.interval === 'hour') { + var hours = date.getHours(); + var ampm = hours >= 12 ? 'pm' : 'am'; + hours = hours % 12; + hours = hours ? hours : 12; // the hour '0' should be '12' + return hours + ampm; + } + } +} + +function onClick(graphData) { + return function(e) { + const element = this.getElementsAtEventForMode(e, 'index', {intersect: false})[0] + const date = element._chart.config.data.labels[element._index] + if (graphData.interval === 'month') { + document.location = '?period=month&date=' + date + } else if (graphData.interval === 'date') { + document.location = '?period=day&date=' + date + } + } +} diff --git a/assets/js/stats/modal.js b/assets/js/stats/modal.js new file mode 100644 index 000000000..7dfaf4277 --- /dev/null +++ b/assets/js/stats/modal.js @@ -0,0 +1,47 @@ +const SPINNER = ` +
+` + +const EMPTY_MODAL = ` + +` + +let el, instance; + +export function showModal(callbacks) { + if (!el) { + const modal = document.createElement('div') + modal.innerHTML = EMPTY_MODAL + document.body.append(modal) + el = modal + } + + MicroModal.show('stats-modal', { + disableFocus: true, + disableScroll: true, + onShow: function(modal, e) { + instance = this + callbacks.onShow(modal) + }, + onClose: function(modal) { + setModalBody(modal, '') + callbacks.onClose(modal) + } + }) +} + +export function setModalBody(modal, body) { + modal.children[0].children[0].innerHTML = body +} + +export function ensureModalClosed() { + if (instance) { + const currentOnClose = instance.onClose + instance.onClose = function() {} + MicroModal.close('stats-modal') + } +} diff --git a/assets/js/stats/router.js b/assets/js/stats/router.js new file mode 100644 index 000000000..3b11fcedf --- /dev/null +++ b/assets/js/stats/router.js @@ -0,0 +1,81 @@ +const PARAMETER_REGEXP = /([:*])(\w+)/g; +const REPLACE_VARIABLE_REGEXP = '([^\/]+)'; + +export default class Router { + constructor() { + this.routes = [] + this.updateLinkHandlers() + window.addEventListener('popstate', this.resolve.bind(this)); + } + + on(path, handler) { + const {regex, paramNames} = this._generateRouteRegex(path) + this.routes.push({regex, paramNames, handler}) + return this + } + + resolve() { + const match = this._matchRoute() + if (match) { + match.route.handler(match.params) + } + return this + } + + navigate(to) { + history.pushState({}, '', to) + this.resolve() + } + + updateLinkHandlers() { + const self = this + const links = document.querySelectorAll('[data-pushstate]') + + for (const link of links) { + if (!link.hasListenerAttached) { + link.addEventListener('click', function (e) { + if (e.ctrlKey || e.metaKey) { + return false; + } + + e.preventDefault(); + const location = link.getAttribute('href');; + self.navigate(location.replace(/\/+$/, '').replace(/^\/+/, '/')); + }) + + link.hasListenerAttached = true; + } + } + } + + _matchRoute() { + const currentPath = window.location.pathname + + for (const route of this.routes) { + const match = currentPath.replace(/^\/+/, '/').match(route.regex) + + if (match) { + const params = this._extractParams(match, route.paramNames) + return {route, params} + } + } + } + + _generateRouteRegex(path) { + const paramNames = [] + const regex = path.replace(PARAMETER_REGEXP, function (full, dots, name) { + paramNames.push(name); + return REPLACE_VARIABLE_REGEXP; + }) + return {regex: new RegExp(regex), paramNames} + } + + _extractParams(match, paramNames) { + return match.slice(1, match.length).reduce(function(params, value, index) { + if (params === null) params = {}; + params[paramNames[index]] = decodeURIComponent(value); + return params; + }, null); + } +} + diff --git a/assets/js/timeframe-selector.js b/assets/js/timeframe-selector.js new file mode 100644 index 000000000..b9421ae98 --- /dev/null +++ b/assets/js/timeframe-selector.js @@ -0,0 +1,71 @@ +const selector = document.querySelector('[data-timeframe-select]') + +if (selector) { + selector.addEventListener('change', function(e) { + if (e.target.value === 'Day') { + document.location = '?period=day' + } else if (e.target.value === 'Week') { + document.location = '?period=week' + } else if (e.target.value === 'Month') { + document.location = '?period=month' + } else if (e.target.value === 'Last 3 Months') { + document.location = '?period=3mo' + } else if (e.target.value === 'Last 6 Months') { + document.location = '?period=6mo' + } else if (e.target.value === 'Custom') { + const parent = e.target.closest('div') + parent.classList.add('hidden') + parent.nextElementSibling.classList.remove('hidden') + } + }) +} + +function getQueryVariable(variable) { + var query = window.location.search.substring(1); + var vars = query.split('&'); + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split('='); + if (decodeURIComponent(pair[0]) == variable) { + return decodeURIComponent(pair[1]); + } + } +} + +function defaultDate() { + const from = getQueryVariable("from") + const to = getQueryVariable("to") + return [Date.parse(from), Date.parse(to)] +} + +function dateToISOString(date) { + const year = date.getFullYear(); + let month = date.getMonth()+1; + let dt = date.getDate(); + + if (dt < 10) { + dt = '0' + dt; + } + if (month < 10) { + month = '0' + month; + } + + return year + '-' + month + '-'+ dt; +} + +const dateRangeTrigger = document.querySelector('#custom-daterange-trigger') + +if (dateRangeTrigger) { + const picker = flatpickr('#custom-daterange-trigger', { + mode: "range", + dateFormat: "M j", + maxDate: 'today', + onChange: function(selectedDates, dateStr) { + if (selectedDates.length === 2) { + dateRangeTrigger.innerHTML = dateStr + const from = dateToISOString(selectedDates[0]) + const to = dateToISOString(selectedDates[1]) + document.location = '?period=custom&from=' + from + '&to=' + to + } + }, + }) +} diff --git a/assets/package-lock.json b/assets/package-lock.json new file mode 100644 index 000000000..02318cbec --- /dev/null +++ b/assets/package-lock.json @@ -0,0 +1,9583 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/core": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.5.5.tgz", + "integrity": "sha512-i4qoSr2KTtce0DmkuuQBV4AuQgGPUcPXMr9L5MyYAtk06z068lQ10a4O009fe5OB/DfNV+h+qqT7ddNV8UnRjg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.5.5", + "@babel/helpers": "^7.5.5", + "@babel/parser": "^7.5.5", + "@babel/template": "^7.4.4", + "@babel/traverse": "^7.5.5", + "@babel/types": "^7.5.5", + "convert-source-map": "^1.1.0", + "debug": "^4.1.0", + "json5": "^2.1.0", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.5.tgz", + "integrity": "sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==", + "dev": true, + "requires": { + "@babel/types": "^7.5.5", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", + "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", + "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-call-delegate": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz", + "integrity": "sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.4.4", + "@babel/traverse": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-define-map": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.5.5.tgz", + "integrity": "sha512-fTfxx7i0B5NJqvUOBBGREnrqbTxRh7zinBANpZXAVDlsZxYdclDp467G1sQ8VZYMnAURY3RpBUAgOYT9GfzHBg==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/types": "^7.5.5", + "lodash": "^4.17.13" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + } + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", + "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz", + "integrity": "sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz", + "integrity": "sha512-5qZ3D1uMclSNqYcXqiHoA0meVdv+xUEex9em2fqMnrk/scphGlGgg66zjMrPJESPwrFJ6sbfFQYUSa0Mz7FabA==", + "dev": true, + "requires": { + "@babel/types": "^7.5.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", + "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-module-transforms": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz", + "integrity": "sha512-jBeCvETKuJqeiaCdyaheF40aXnnU1+wkSiUs/IQg3tB85up1LyL8x77ClY8qJpuRJUcXQo+ZtdNESmZl4j56Pw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-simple-access": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/template": "^7.4.4", + "@babel/types": "^7.5.5", + "lodash": "^4.17.13" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + } + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", + "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", + "dev": true + }, + "@babel/helper-regex": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.5.5.tgz", + "integrity": "sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw==", + "dev": true, + "requires": { + "lodash": "^4.17.13" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + } + } + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", + "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-wrap-function": "^7.1.0", + "@babel/template": "^7.1.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-replace-supers": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz", + "integrity": "sha512-XvRFWrNnlsow2u7jXDuH4jDDctkxbS7gXssrP4q2nUD606ukXHRvydj346wmNg+zAgpFx4MWf4+usfC93bElJg==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.5.5", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/traverse": "^7.5.5", + "@babel/types": "^7.5.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", + "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", + "dev": true, + "requires": { + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", + "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-wrap-function": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz", + "integrity": "sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/template": "^7.1.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.2.0" + } + }, + "@babel/helpers": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.5.5.tgz", + "integrity": "sha512-nRq2BUhxZFnfEn/ciJuhklHvFOqjJUD5wpx+1bxUF2axL9C+v4DE/dmp5sT2dKnpOs4orZWzpAZqlCy8QqE/7g==", + "dev": true, + "requires": { + "@babel/template": "^7.4.4", + "@babel/traverse": "^7.5.5", + "@babel/types": "^7.5.5" + } + }, + "@babel/highlight": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", + "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", + "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==", + "dev": true + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz", + "integrity": "sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.1.0", + "@babel/plugin-syntax-async-generators": "^7.2.0" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz", + "integrity": "sha512-x/iMjggsKTFHYC6g11PL7Qy58IK8H5zqfm9e6hu4z1iH2IRyAp9u9dL80zA6R76yFovETFLKz2VJIC2iIPBuFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz", + "integrity": "sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-json-strings": "^7.2.0" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.5.tgz", + "integrity": "sha512-F2DxJJSQ7f64FyTVl5cw/9MWn6naXGdk3Q3UhDbFEEHv+EilCPoeRD3Zh/Utx1CJz4uyKlQ4uH+bJPbEhMV7Zw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz", + "integrity": "sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz", + "integrity": "sha512-j1NwnOqMG9mFUOH58JTFsA/+ZYzQLUZ/drqWUqxCYLGeu2JFZL8YrNC9hBxKmWtAuOCHPcRpgv7fhap09Fb4kA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.5.4" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz", + "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz", + "integrity": "sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz", + "integrity": "sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", + "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz", + "integrity": "sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", + "integrity": "sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz", + "integrity": "sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.1.0" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz", + "integrity": "sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.5.5.tgz", + "integrity": "sha512-82A3CLRRdYubkG85lKwhZB0WZoHxLGsJdux/cOVaJCJpvYFl1LVzAIFyRsa7CvXqW8rBM4Zf3Bfn8PHt5DP0Sg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + } + } + }, + "@babel/plugin-transform-classes": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.5.5.tgz", + "integrity": "sha512-U2htCNK/6e9K7jGyJ++1p5XRU+LJjrwtoiVn9SzRlDT2KubcZ11OOwy3s24TjHxPgxNwonCYP7U2K51uVYCMDg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-define-map": "^7.5.5", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.5.5", + "@babel/helper-split-export-declaration": "^7.4.4", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz", + "integrity": "sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz", + "integrity": "sha512-YbYgbd3TryYYLGyC7ZR+Tq8H/+bCmwoaxHfJHupom5ECstzbRLTch6gOQbhEY9Z4hiCNHEURgq06ykFv9JZ/QQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz", + "integrity": "sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.5.4" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz", + "integrity": "sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz", + "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz", + "integrity": "sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz", + "integrity": "sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz", + "integrity": "sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz", + "integrity": "sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz", + "integrity": "sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz", + "integrity": "sha512-xmHq0B+ytyrWJvQTc5OWAC4ii6Dhr0s22STOoydokG51JjWhyYo5mRPXoi+ZmtHQhZZwuXNN+GG5jy5UZZJxIQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.4.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-simple-access": "^7.1.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz", + "integrity": "sha512-Q2m56tyoQWmuNGxEtUyeEkm6qJYFqs4c+XyXH5RAuYxObRNz9Zgj/1g2GMnjYp2EUyEy7YTrxliGCXzecl/vJg==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.4.4", + "@babel/helper-plugin-utils": "^7.0.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz", + "integrity": "sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.5.tgz", + "integrity": "sha512-z7+2IsWafTBbjNsOxU/Iv5CvTJlr5w4+HGu1HovKYTtgJ362f7kBcQglkfmlspKKZ3bgrbSGvLfNx++ZJgCWsg==", + "dev": true, + "requires": { + "regexp-tree": "^0.1.6" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz", + "integrity": "sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.5.5.tgz", + "integrity": "sha512-un1zJQAhSosGFBduPgN/YFNvWVpRuHKU7IHBglLoLZsGmruJPOo6pbInneflUdmq7YvSVqhpPs5zdBvLnteltQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.5.5" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz", + "integrity": "sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw==", + "dev": true, + "requires": { + "@babel/helper-call-delegate": "^7.4.4", + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz", + "integrity": "sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz", + "integrity": "sha512-gBKRh5qAaCWntnd09S8QC7r3auLCqq5DI6O0DlfoyDjslSBVqBibrMdsqO+Uhmx3+BlOmE/Kw1HFxmGbv0N9dA==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.0" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz", + "integrity": "sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", + "integrity": "sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz", + "integrity": "sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz", + "integrity": "sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.0.0" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz", + "integrity": "sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz", + "integrity": "sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz", + "integrity": "sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.5.4" + } + }, + "@babel/preset-env": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.5.5.tgz", + "integrity": "sha512-GMZQka/+INwsMz1A5UEql8tG015h5j/qjptpKY2gJ7giy8ohzU710YciJB5rcKsWGWHiW3RUnHib0E5/m3Tp3A==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-async-generator-functions": "^7.2.0", + "@babel/plugin-proposal-dynamic-import": "^7.5.0", + "@babel/plugin-proposal-json-strings": "^7.2.0", + "@babel/plugin-proposal-object-rest-spread": "^7.5.5", + "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-syntax-async-generators": "^7.2.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-syntax-json-strings": "^7.2.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", + "@babel/plugin-transform-arrow-functions": "^7.2.0", + "@babel/plugin-transform-async-to-generator": "^7.5.0", + "@babel/plugin-transform-block-scoped-functions": "^7.2.0", + "@babel/plugin-transform-block-scoping": "^7.5.5", + "@babel/plugin-transform-classes": "^7.5.5", + "@babel/plugin-transform-computed-properties": "^7.2.0", + "@babel/plugin-transform-destructuring": "^7.5.0", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/plugin-transform-duplicate-keys": "^7.5.0", + "@babel/plugin-transform-exponentiation-operator": "^7.2.0", + "@babel/plugin-transform-for-of": "^7.4.4", + "@babel/plugin-transform-function-name": "^7.4.4", + "@babel/plugin-transform-literals": "^7.2.0", + "@babel/plugin-transform-member-expression-literals": "^7.2.0", + "@babel/plugin-transform-modules-amd": "^7.5.0", + "@babel/plugin-transform-modules-commonjs": "^7.5.0", + "@babel/plugin-transform-modules-systemjs": "^7.5.0", + "@babel/plugin-transform-modules-umd": "^7.2.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.4.5", + "@babel/plugin-transform-new-target": "^7.4.4", + "@babel/plugin-transform-object-super": "^7.5.5", + "@babel/plugin-transform-parameters": "^7.4.4", + "@babel/plugin-transform-property-literals": "^7.2.0", + "@babel/plugin-transform-regenerator": "^7.4.5", + "@babel/plugin-transform-reserved-words": "^7.2.0", + "@babel/plugin-transform-shorthand-properties": "^7.2.0", + "@babel/plugin-transform-spread": "^7.2.0", + "@babel/plugin-transform-sticky-regex": "^7.2.0", + "@babel/plugin-transform-template-literals": "^7.4.4", + "@babel/plugin-transform-typeof-symbol": "^7.2.0", + "@babel/plugin-transform-unicode-regex": "^7.4.4", + "@babel/types": "^7.5.5", + "browserslist": "^4.6.0", + "core-js-compat": "^3.1.1", + "invariant": "^2.2.2", + "js-levenshtein": "^1.1.3", + "semver": "^5.5.0" + } + }, + "@babel/template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", + "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/traverse": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.5.tgz", + "integrity": "sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.5.5", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/parser": "^7.5.5", + "@babel/types": "^7.5.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz", + "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + } + } + }, + "@types/q": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", + "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", + "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", + "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", + "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", + "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", + "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", + "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", + "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "mamacro": "^0.0.3" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", + "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", + "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", + "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", + "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", + "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", + "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/helper-wasm-section": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-opt": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", + "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", + "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", + "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", + "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/floating-point-hex-parser": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-code-frame": "1.8.5", + "@webassemblyjs/helper-fsm": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", + "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "acorn": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", + "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", + "dev": true + }, + "ajv": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", + "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.0.tgz", + "integrity": "sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk=", + "dev": true + }, + "ajv-keywords": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", + "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "dev": true + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "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": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "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.1" + } + } + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "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 + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "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.1" + } + }, + "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.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "autoprefixer": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.6.1.tgz", + "integrity": "sha512-aVo5WxR3VyvyJxcJC3h4FKfwCQvQWb1tSI5VHNibddCVWrcD1NvlxEweg3TSgiPztMnWfjpy2FURKA2kvDE+Tw==", + "requires": { + "browserslist": "^4.6.3", + "caniuse-lite": "^1.0.30000980", + "chalk": "^2.4.2", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^7.0.17", + "postcss-value-parser": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-value-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", + "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "babel-extract-comments": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-extract-comments/-/babel-extract-comments-1.0.0.tgz", + "integrity": "sha512-qWWzi4TlddohA91bFwgt6zO/J0X+io7Qp184Fw0m2JYRSTZnJbFR8+07KmzudHCZgOiKRCrjhylwv9Xd8gfhVQ==", + "dev": true, + "requires": { + "babylon": "^6.18.0" + }, + "dependencies": { + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + } + } + }, + "babel-loader": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz", + "integrity": "sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==", + "dev": true, + "requires": { + "find-cache-dir": "^2.0.0", + "loader-utils": "^1.0.2", + "mkdirp": "^0.5.1", + "pify": "^4.0.1" + }, + "dependencies": { + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", + "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "http://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", + "dev": true + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", + "dev": true, + "requires": { + "babel-plugin-syntax-object-rest-spread": "^6.8.0", + "babel-runtime": "^6.26.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "bluebird": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", + "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.6.tgz", + "integrity": "sha512-D2Nk3W9JL9Fp/gIcWei8LrERCS+eXu9AM5cfXA8WEZ84lFks+ARnZ0q/R69m2SV3Wjma83QDDPxsNKXUwdIsyA==", + "requires": { + "caniuse-lite": "^1.0.30000984", + "electron-to-chromium": "^1.3.191", + "node-releases": "^1.1.25" + }, + "dependencies": { + "electron-to-chromium": { + "version": "1.3.234", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.234.tgz", + "integrity": "sha512-SVXY503NJLFAqBx8VdJaO47G+qUQggHgRjZnyjH9/SZ1w0CJpeBrEssNPk71TeKU8OGHdYjjNNHeJ6v+TJoCBg==" + } + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "cacache": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.3.tgz", + "integrity": "sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30000989", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz", + "integrity": "sha512-vrMcvSuMz16YY6GSVZ0dWDTJP8jqk3iFQ/Aq5iqblPwxSVVZI+zxDyTX0VPqtQsDnfdrBDcsmhgTEOh5R8Lbpw==" + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chokidar": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", + "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "chownr": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", + "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dev": true, + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz", + "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", + "dev": true, + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "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=" + }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "dev": true + }, + "comment-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/comment-regex/-/comment-regex-1.0.1.tgz", + "integrity": "sha512-IWlN//Yfby92tOIje7J18HkNmWRR7JESA/BK8W7wqY/akITpU5B0JQWnbTjCfdChSrDNb0DrdA9jfAxiiBXyiQ==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "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.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "copy-webpack-plugin": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.0.4.tgz", + "integrity": "sha512-YBuYGpSzoCHSSDGyHy6VJ7SHojKp6WHT4D7ItcQFNAYx2hrwkMe56e97xfVR0/ovDuMTrMffXUiltvQljtAGeg==", + "dev": true, + "requires": { + "cacache": "^11.3.3", + "find-cache-dir": "^2.1.0", + "glob-parent": "^3.1.0", + "globby": "^7.1.1", + "is-glob": "^4.0.1", + "loader-utils": "^1.2.3", + "minimatch": "^3.0.4", + "normalize-path": "^3.0.0", + "p-limit": "^2.2.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^1.7.0", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + } + } + }, + "core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==", + "dev": true + }, + "core-js-compat": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.2.1.tgz", + "integrity": "sha512-MwPZle5CF9dEaMYdDeWm73ao/IflDH+FjeJCWEADcEgFSE9TLimFKwJsfmkwzI8eC0Aj0mgvMDjeQjrElkz4/A==", + "dev": true, + "requires": { + "browserslist": "^4.6.6", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "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": "4.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz", + "integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==", + "dev": true, + "requires": { + "is-directory": "^0.3.1", + "js-yaml": "^3.9.0", + "parse-json": "^4.0.0", + "require-from-string": "^2.0.1" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "http://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true + }, + "css-declaration-sorter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", + "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", + "dev": true, + "requires": { + "postcss": "^7.0.1", + "timsort": "^0.3.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "css-loader": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.2.0.tgz", + "integrity": "sha512-QTF3Ud5H7DaZotgdcJjGMvyDj5F3Pn1j/sC6VBEOVp94cbwqyIBdcs/quzj4MC1BKQSrTpQznegH/5giYbhnCQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.17", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.1.0", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.0.0", + "schema-utils": "^2.0.0" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "postcss-value-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", + "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", + "dev": true + }, + "schema-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz", + "integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "css-select": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz", + "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^2.1.2", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", + "dev": true + }, + "css-tree": { + "version": "1.0.0-alpha.33", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.33.tgz", + "integrity": "sha512-SPt57bh5nQnpsTBsx/IXbO14sRc9xXu5MtMAVuo0BaQQmyf0NupNPPSoMaqiAF5tDFafYsTkfeH4Q/HCKXkg4w==", + "dev": true, + "requires": { + "mdn-data": "2.0.4", + "source-map": "^0.5.3" + } + }, + "css-unit-converter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.1.tgz", + "integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=", + "dev": true + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true + }, + "css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "cssnano": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", + "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "cssnano-preset-default": "^4.0.7", + "is-resolvable": "^1.0.0", + "postcss": "^7.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cssnano-preset-default": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz", + "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", + "dev": true, + "requires": { + "css-declaration-sorter": "^4.0.1", + "cssnano-util-raw-cache": "^4.0.1", + "postcss": "^7.0.0", + "postcss-calc": "^7.0.1", + "postcss-colormin": "^4.0.3", + "postcss-convert-values": "^4.0.1", + "postcss-discard-comments": "^4.0.2", + "postcss-discard-duplicates": "^4.0.2", + "postcss-discard-empty": "^4.0.1", + "postcss-discard-overridden": "^4.0.1", + "postcss-merge-longhand": "^4.0.11", + "postcss-merge-rules": "^4.0.3", + "postcss-minify-font-values": "^4.0.2", + "postcss-minify-gradients": "^4.0.2", + "postcss-minify-params": "^4.0.2", + "postcss-minify-selectors": "^4.0.2", + "postcss-normalize-charset": "^4.0.1", + "postcss-normalize-display-values": "^4.0.2", + "postcss-normalize-positions": "^4.0.2", + "postcss-normalize-repeat-style": "^4.0.2", + "postcss-normalize-string": "^4.0.2", + "postcss-normalize-timing-functions": "^4.0.2", + "postcss-normalize-unicode": "^4.0.1", + "postcss-normalize-url": "^4.0.1", + "postcss-normalize-whitespace": "^4.0.2", + "postcss-ordered-values": "^4.1.2", + "postcss-reduce-initial": "^4.0.3", + "postcss-reduce-transforms": "^4.0.2", + "postcss-svgo": "^4.0.2", + "postcss-unique-selectors": "^4.0.1" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cssnano-util-get-arguments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", + "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", + "dev": true + }, + "cssnano-util-get-match": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", + "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", + "dev": true + }, + "cssnano-util-raw-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", + "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cssnano-util-same-parent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", + "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", + "dev": true + }, + "csso": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz", + "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==", + "dev": true, + "requires": { + "css-tree": "1.0.0-alpha.29" + }, + "dependencies": { + "css-tree": { + "version": "1.0.0-alpha.29", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz", + "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==", + "dev": true, + "requires": { + "mdn-data": "~1.1.0", + "source-map": "^0.5.3" + } + }, + "mdn-data": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz", + "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==", + "dev": true + } + } + }, + "cyclist": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", + "dev": true + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "dir-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "dev": true, + "requires": { + "path-type": "^3.0.0" + } + }, + "dom-serializer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz", + "integrity": "sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", + "dev": true + } + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "elliptic": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.0.tgz", + "integrity": "sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", + "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.4.0", + "tapable": "^1.0.0" + } + }, + "entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", + "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==", + "dev": true + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "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=" + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "figgy-pudding": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "dev": true + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gather-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gather-stream/-/gather-stream-1.0.0.tgz", + "integrity": "sha1-szmUr0V6gRVwDUEPMXczy+egkEs=", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "requires": { + "global-prefix": "^3.0.0" + }, + "dependencies": { + "global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "requires": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + } + } + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", + "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "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.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "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" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", + "dev": true + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "dev": true + }, + "html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", + "dev": true + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", + "dev": true, + "requires": { + "import-from": "^2.1.0" + } + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.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" + } + } + } + }, + "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-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dev": true, + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.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" + } + } + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "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": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "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 + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "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.0.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" + } + } + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "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-svg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", + "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", + "dev": true, + "requires": { + "html-comment-regex": "^1.1.0" + } + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "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": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "js-base64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.9.tgz", + "integrity": "sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ==", + "dev": true + }, + "js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json5": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", + "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "last-call-webpack-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz", + "integrity": "sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==", + "dev": true, + "requires": { + "lodash": "^4.17.5", + "webpack-sources": "^1.1.0" + } + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0" + }, + "dependencies": { + "json5": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + } + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", + "dev": true + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", + "dev": true + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mini-css-extract-plugin": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz", + "integrity": "sha512-MNpRGbNA52q6U92i0qbVpQNsgk7LExy41MdAlG84FeytfDOtRIf/mCHdEgG8rpTKOaNKiqUnZdlptF469hxqOw==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "normalize-url": "1.9.1", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "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.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-emoji": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", + "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", + "dev": true, + "requires": { + "lodash.toarray": "^4.4.0" + } + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "node-releases": { + "version": "1.1.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.27.tgz", + "integrity": "sha512-9iXUqHKSGo6ph/tdXVbHFbhRVQln4ZDTIBJCzsa90HimnBYc5jw8RWYt4wBYFHehGyC3koIz5O4mb2fHrbPOuA==", + "requires": { + "semver": "^5.3.0" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "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.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=" + }, + "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-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "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" + } + } + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "object.values": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", + "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optimize-css-assets-webpack-plugin": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.3.tgz", + "integrity": "sha512-q9fbvCRS6EYtUKKSwI87qm2IxlyJK5b4dygW1rKUBT6mMDhdG5e5bZT63v6tnJR9F9FB/H5a0HTmtw+laUBxKA==", + "dev": true, + "requires": { + "cssnano": "^4.1.10", + "last-call-webpack-plugin": "^3.0.0" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "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-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pako": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", + "dev": true + }, + "parallel-transform": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "dev": true, + "requires": { + "cyclist": "~0.2.2", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "parse-asn1": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", + "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "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": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "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.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "perfectionist": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/perfectionist/-/perfectionist-2.4.0.tgz", + "integrity": "sha1-wUetNxThJkZ/F2QSnuct+GHUfqA=", + "dev": true, + "requires": { + "comment-regex": "^1.0.0", + "defined": "^1.0.0", + "minimist": "^1.2.0", + "postcss": "^5.0.8", + "postcss-scss": "^0.3.0", + "postcss-value-parser": "^3.3.0", + "read-file-stdin": "^0.2.0", + "string.prototype.repeat": "^0.2.0", + "vendors": "^1.0.0", + "write-file-stdout": "0.0.2" + } + }, + "phoenix": { + "version": "file:../deps/phoenix" + }, + "phoenix_html": { + "version": "file:../deps/phoenix_html" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "http://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.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "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 + } + } + }, + "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 + }, + "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" + } + } + } + }, + "postcss-calc": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.1.tgz", + "integrity": "sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ==", + "dev": true, + "requires": { + "css-unit-converter": "^1.1.1", + "postcss": "^7.0.5", + "postcss-selector-parser": "^5.0.0-rc.4", + "postcss-value-parser": "^3.3.1" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-colormin": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", + "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "color": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-convert-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", + "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-discard-comments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", + "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-discard-duplicates": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", + "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-discard-empty": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", + "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-discard-overridden": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", + "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-functions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz", + "integrity": "sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=", + "dev": true, + "requires": { + "glob": "^7.1.2", + "object-assign": "^4.1.1", + "postcss": "^6.0.9", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-2.0.2.tgz", + "integrity": "sha512-HxXLw1lrczsbVXxyC+t/VIfje9ZeZhkkXE8KpFa3MEKfp2FyHDv29JShYY9eLhYrhLyWWHNIuwkktTfLXu2otw==", + "dev": true, + "requires": { + "camelcase-css": "^2.0.1", + "postcss": "^7.0.17" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-load-config": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.0.0.tgz", + "integrity": "sha512-V5JBLzw406BB8UIfsAWSK2KSwIJ5yoEIVFb4gVkXci0QdKgA24jLmHZ/ghe/GgX0lJ0/D1uUK1ejhzEY94MChQ==", + "dev": true, + "requires": { + "cosmiconfig": "^4.0.0", + "import-cwd": "^2.0.0" + } + }, + "postcss-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", + "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "postcss": "^7.0.0", + "postcss-load-config": "^2.0.0", + "schema-utils": "^1.0.0" + }, + "dependencies": { + "postcss": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.6.tgz", + "integrity": "sha512-Nq/rNjnHFcKgCDDZYO0lNsl6YWe6U7tTy+ESN+PnLxebL8uBtYX59HZqvrj7YLK5UCyll2hqDsJOo3ndzEW8Ug==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.5.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-merge-longhand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", + "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", + "dev": true, + "requires": { + "css-color-names": "0.0.4", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "stylehacks": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-merge-rules": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", + "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "cssnano-util-same-parent": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0", + "vendors": "^1.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-minify-font-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", + "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-minify-gradients": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", + "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "is-color-stop": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-minify-params": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", + "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "browserslist": "^4.0.0", + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "uniqs": "^2.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-minify-selectors": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", + "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-modules-local-by-default": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", + "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", + "dev": true, + "requires": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.16", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "postcss-value-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", + "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-modules-scope": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz", + "integrity": "sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "requires": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-nested": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-4.1.2.tgz", + "integrity": "sha512-9bQFr2TezohU3KRSu9f6sfecXmf/x6RXDedl8CHF6fyuyVW7UqgNMRdWMHZQWuFY6Xqs2NYk+Fj4Z4vSOf7PQg==", + "dev": true, + "requires": { + "postcss": "^7.0.14", + "postcss-selector-parser": "^5.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-normalize-charset": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", + "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-normalize-display-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", + "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-normalize-positions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", + "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-normalize-repeat-style": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", + "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-normalize-string": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", + "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", + "dev": true, + "requires": { + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-normalize-timing-functions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", + "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-normalize-unicode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", + "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-normalize-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", + "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", + "dev": true, + "requires": { + "is-absolute-url": "^2.0.0", + "normalize-url": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", + "dev": true + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-normalize-whitespace": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", + "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-ordered-values": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", + "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-reduce-initial": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", + "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-reduce-transforms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", + "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-scss": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-0.3.1.tgz", + "integrity": "sha1-ZcYQ2OKn7g5isYNbcbiHBzSBbks=", + "dev": true, + "requires": { + "postcss": "^5.2.4" + } + }, + "postcss-selector-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", + "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-svgo": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz", + "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", + "dev": true, + "requires": { + "is-svg": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "svgo": "^1.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-unique-selectors": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", + "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "postcss": "^7.0.0", + "uniqs": "^2.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "dev": true + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dev": true, + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "read-file-stdin": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/read-file-stdin/-/read-file-stdin-0.2.1.tgz", + "integrity": "sha1-JezP86FTtoCa+ssj7hU4fbng7mE=", + "dev": true, + "requires": { + "gather-stream": "^1.0.0" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz", + "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==", + "dev": true, + "requires": { + "regenerate": "^1.4.0" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "regenerator-transform": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.1.tgz", + "integrity": "sha512-flVuee02C3FKRISbxhXl9mGzdbWUVHubl1SMaknjxkFB1/iqpJhArQUvRxOOPEc/9tAiX0BaQ28FJH10E4isSQ==", + "dev": true, + "requires": { + "private": "^0.1.6" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexp-tree": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.11.tgz", + "integrity": "sha512-7/l/DgapVVDzZobwMCCgMlqiqyLFJ0cduo/j+3BcDJIB+yJdsYCfKuI3l/04NV+H/rfNRdPIDbXNZHM9XvQatg==", + "dev": true + }, + "regexpu-core": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.5.5.tgz", + "integrity": "sha512-FpI67+ky9J+cDizQUJlIlNZFKual/lUkFr1AG6zOCpwZ9cLrg8UUVakyUQJD7fCDIe9Z2nwTQJNPyonatNmDFQ==", + "dev": true, + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.1.0", + "regjsgen": "^0.5.0", + "regjsparser": "^0.6.0", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.1.0" + } + }, + "regjsgen": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", + "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==", + "dev": true + }, + "regjsparser": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", + "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "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 + }, + "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": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "dev": true, + "requires": { + "path-parse": "^1.0.5" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "dependencies": { + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + } + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", + "dev": true + }, + "rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + }, + "serialize-javascript": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.7.0.tgz", + "integrity": "sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA==", + "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 + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "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 + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + } + } + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "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" + } + } + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "string.prototype.repeat": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz", + "integrity": "sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8=", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-comments": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-1.0.2.tgz", + "integrity": "sha512-kL97alc47hoyIQSV165tTt9rG5dn4w1dNnBhOQ3bOU1Nc1hel09jnXANaHJ7vzHLd4Ju8kseDGzlev96pghLFw==", + "dev": true, + "requires": { + "babel-extract-comments": "^1.0.0", + "babel-plugin-transform-object-rest-spread": "^6.26.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "stylehacks": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", + "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "dev": true, + "requires": { + "dot-prop": "^4.1.1", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "svgo": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.0.tgz", + "integrity": "sha512-MLfUA6O+qauLDbym+mMZgtXCGRfIxyQoeH6IKVcFslyODEe/ElJNwr0FohQ3xG4C6HK6bk3KYPPXwHVJk3V5NQ==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.33", + "csso": "^3.5.1", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + } + }, + "tailwindcss": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-0.7.4.tgz", + "integrity": "sha512-+GeQjHRJ2VmeLkrNwMCbPDfm2cc5P8eoc7n+DtZfI8oQdlo5eSHqsIlPEuZOtoqQlIALsd2jAggWrUUBFGP2ow==", + "dev": true, + "requires": { + "autoprefixer": "^9.4.5", + "bytes": "^3.0.0", + "chalk": "^2.4.1", + "css.escape": "^1.5.1", + "fs-extra": "^4.0.2", + "lodash": "^4.17.5", + "node-emoji": "^1.8.1", + "perfectionist": "^2.4.0", + "postcss": "^7.0.11", + "postcss-functions": "^3.0.0", + "postcss-js": "^2.0.0", + "postcss-nested": "^4.1.1", + "postcss-selector-parser": "^5.0.0", + "pretty-hrtime": "^1.0.3", + "strip-comments": "^1.0.2" + }, + "dependencies": { + "autoprefixer": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.6.1.tgz", + "integrity": "sha512-aVo5WxR3VyvyJxcJC3h4FKfwCQvQWb1tSI5VHNibddCVWrcD1NvlxEweg3TSgiPztMnWfjpy2FURKA2kvDE+Tw==", + "dev": true, + "requires": { + "browserslist": "^4.6.3", + "caniuse-lite": "^1.0.30000980", + "chalk": "^2.4.2", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^7.0.17", + "postcss-value-parser": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true + }, + "postcss": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "requires": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-value-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", + "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "terser": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.1.4.tgz", + "integrity": "sha512-+ZwXJvdSwbd60jG0Illav0F06GDJF0R4ydZ21Q3wGAFKoBGyJGo34F63vzJHgvYxc1ukOtIjvwEvl9MkjzM6Pg==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz", + "integrity": "sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==", + "dev": true, + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^1.7.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "dependencies": { + "cacache": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.2.tgz", + "integrity": "sha512-ifKgxH2CKhJEg6tNdAwziu6Q33EvuG26tYcda6PT3WKisZcYDXsnEdnRv67Po3yCzFfaSoMjGZzJyD2c3DT1dg==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "timers-browserify": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", + "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.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" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "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 + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "dev": true + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uglify-js": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", + "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "dev": true, + "requires": { + "commander": "~2.20.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "uglifyjs-webpack-plugin": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-2.2.0.tgz", + "integrity": "sha512-mHSkufBmBuJ+KHQhv5H0MXijtsoA1lynJt1lXOaotja8/I0pR4L9oGaPIZw+bQBOFittXZg9OC1sXSGO9D9ZYg==", + "dev": true, + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^1.7.0", + "source-map": "^0.6.1", + "uglify-js": "^3.6.0", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "dependencies": { + "cacache": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.2.tgz", + "integrity": "sha512-ifKgxH2CKhJEg6tNdAwziu6Q33EvuG26tYcda6PT3WKisZcYDXsnEdnRv67Po3yCzFfaSoMjGZzJyD2c3DT1dg==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz", + "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", + "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "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" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "upath": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", + "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + }, + "v8-compile-cache": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", + "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", + "dev": true + }, + "vendors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.2.tgz", + "integrity": "sha512-w/hry/368nO21AN9QljsaIhb9ZiZtZARoVH5f3CsFbawdLdayCgKRPup7CggujvySMxx0I91NOyxdVENohprLQ==", + "dev": true + }, + "vm-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "dev": true + }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "dev": true, + "requires": { + "chokidar": "^2.0.2", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + } + }, + "webpack": { + "version": "4.39.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.39.2.tgz", + "integrity": "sha512-AKgTfz3xPSsEibH00JfZ9sHXGUwIQ6eZ9tLN8+VLzachk1Cw2LVmy+4R7ZiwTa9cZZ15tzySjeMui/UnSCAZhA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/wasm-edit": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "acorn": "^6.2.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.1", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.1", + "watchpack": "^1.6.0", + "webpack-sources": "^1.4.1" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + } + } + }, + "webpack-cli": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.7.tgz", + "integrity": "sha512-OhTUCttAsr+IZSMVwGROGRHvT+QAs8H6/mHIl4SvhAwYywjiylYjpwybGx7WQ9Hkb45FhjtsymkwiRRbGJ1SZQ==", + "dev": true, + "requires": { + "chalk": "2.4.2", + "cross-spawn": "6.0.5", + "enhanced-resolve": "4.1.0", + "findup-sync": "3.0.0", + "global-modules": "2.0.0", + "import-local": "2.0.0", + "interpret": "1.2.0", + "loader-utils": "1.2.3", + "supports-color": "6.1.0", + "v8-compile-cache": "2.0.3", + "yargs": "13.2.4" + }, + "dependencies": { + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-stdout": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/write-file-stdout/-/write-file-stdout-0.0.2.tgz", + "integrity": "sha1-wlLXx8WxtAKJdjDjRTx7/mkNnKE=", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "dev": true + }, + "yargs": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", + "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.0" + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } +} diff --git a/assets/package.json b/assets/package.json new file mode 100644 index 000000000..790f10533 --- /dev/null +++ b/assets/package.json @@ -0,0 +1,27 @@ +{ + "repository": {}, + "license": "MIT", + "scripts": { + "deploy": "webpack --mode production", + "watch": "webpack --mode development --watch" + }, + "dependencies": { + "autoprefixer": "^9.6.1", + "phoenix": "file:../deps/phoenix", + "phoenix_html": "file:../deps/phoenix_html" + }, + "devDependencies": { + "@babel/core": "^7.5.5", + "@babel/preset-env": "^7.5.5", + "babel-loader": "^8.0.6", + "copy-webpack-plugin": "^5.0.4", + "css-loader": "^3.2.0", + "mini-css-extract-plugin": "^0.8.0", + "optimize-css-assets-webpack-plugin": "^5.0.3", + "postcss-loader": "^3.0.0", + "tailwindcss": "^0.7.4", + "uglifyjs-webpack-plugin": "^2.2.0", + "webpack": "4.39.2", + "webpack-cli": "^3.3.7" + } +} diff --git a/assets/postcss.config.js b/assets/postcss.config.js new file mode 100644 index 000000000..cebc0b9a6 --- /dev/null +++ b/assets/postcss.config.js @@ -0,0 +1,8 @@ +var tailwindcss = require('tailwindcss'); + +module.exports = { + plugins: [ + tailwindcss('./tailwind.js'), + require('autoprefixer') + ] +} diff --git a/assets/static/favicon.ico b/assets/static/favicon.ico new file mode 100644 index 000000000..73de524aa Binary files /dev/null and b/assets/static/favicon.ico differ diff --git a/assets/static/images/icon/plausible_favicon.png b/assets/static/images/icon/plausible_favicon.png new file mode 100644 index 000000000..6e9ba0861 Binary files /dev/null and b/assets/static/images/icon/plausible_favicon.png differ diff --git a/assets/static/images/icon/plausible_logo.png b/assets/static/images/icon/plausible_logo.png new file mode 100644 index 000000000..e7d76ad88 Binary files /dev/null and b/assets/static/images/icon/plausible_logo.png differ diff --git a/assets/static/images/icon/plausible_logo_inverted.png b/assets/static/images/icon/plausible_logo_inverted.png new file mode 100644 index 000000000..d356441cc Binary files /dev/null and b/assets/static/images/icon/plausible_logo_inverted.png differ diff --git a/assets/static/images/testimonials/felipe.jpg b/assets/static/images/testimonials/felipe.jpg new file mode 100644 index 000000000..0ad4701c8 Binary files /dev/null and b/assets/static/images/testimonials/felipe.jpg differ diff --git a/assets/static/images/testimonials/makis.jpg b/assets/static/images/testimonials/makis.jpg new file mode 100644 index 000000000..7dd10af9b Binary files /dev/null and b/assets/static/images/testimonials/makis.jpg differ diff --git a/assets/static/images/testimonials/markus.jpg b/assets/static/images/testimonials/markus.jpg new file mode 100644 index 000000000..3afff7213 Binary files /dev/null and b/assets/static/images/testimonials/markus.jpg differ diff --git a/assets/static/robots.txt b/assets/static/robots.txt new file mode 100644 index 000000000..3c9c7c01f --- /dev/null +++ b/assets/static/robots.txt @@ -0,0 +1,5 @@ +# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / diff --git a/assets/tailwind.js b/assets/tailwind.js new file mode 100644 index 000000000..8ba7b1783 --- /dev/null +++ b/assets/tailwind.js @@ -0,0 +1,959 @@ +/* + +Tailwind - The Utility-First CSS Framework + +A project by Adam Wathan (@adamwathan), Jonathan Reinink (@reinink), +David Hemphill (@davidhemphill) and Steve Schoger (@steveschoger). + +Welcome to the Tailwind config file. This is where you can customize +Tailwind specifically for your project. Don't be intimidated by the +length of this file. It's really just a big JavaScript object and +we've done our very best to explain each section. + +View the full documentation at https://tailwindcss.com. + + +|------------------------------------------------------------------------------- +| The default config +|------------------------------------------------------------------------------- +| +| This variable contains the default Tailwind config. You don't have +| to use it, but it can sometimes be helpful to have available. For +| example, you may choose to merge your custom configuration +| values with some of the Tailwind defaults. +| +*/ + +let defaultConfig = require('tailwindcss/defaultConfig')() + + +/* +|------------------------------------------------------------------------------- +| Colors https://tailwindcss.com/docs/colors +|------------------------------------------------------------------------------- +| +| Here you can specify the colors used in your project. To get you started, +| we've provided a generous palette of great looking colors that are perfect +| for prototyping, but don't hesitate to change them for your project. You +| own these colors, nothing will break if you change everything about them. +| +| We've used literal color names ("red", "blue", etc.) for the default +| palette, but if you'd rather use functional names like "primary" and +| "secondary", or even a numeric scale like "100" and "200", go for it. +| +*/ + +let colors = { + 'transparent': 'transparent', + + 'black': '#22292f', + 'grey-darkest': '#3d4852', + 'grey-darker': '#606f7b', + 'grey-dark': '#8795a1', + 'grey': '#b8c2cc', + 'grey-light': '#dae1e7', + 'grey-lighter': '#f1f5f8', + 'grey-lightest': '#f8fafc', + 'white': '#ffffff', + + 'red-darkest': '#3b0d0c', + 'red-darker': '#621b18', + 'red-dark': '#ff4457', // USE + 'red': '#ff6978', // USE + 'red-light': '#ef5753', + 'red-lighter': '#f9acaa', + 'red-lightest': '#fcebea', + + 'orange-darkest': '#462a16', + 'orange-darker': '#613b1f', + 'orange-dark': '#de751f', + 'orange': '#f6993f', + 'orange-light': '#faad63', + 'orange-lighter': '#fcd9b6', + 'orange-lightest': '#fff5eb', + + 'yellow-darkest': '#453411', + 'yellow-darker': '#684f1d', + 'yellow-dark': '#f2d024', + 'yellow': '#ffed4a', + 'yellow-light': '#fff382', + 'yellow-lighter': '#fff9c2', + 'yellow-lightest': '#fcfbeb', + + 'green-darkest': '#0f2f21', + 'green-darker': '#1a4731', + 'green-dark': '#1f9d55', + 'green': '#38C172', // USE + 'green-light': '#51d88a', + 'green-lighter': '#a2f5bf', + 'green-lightest': '#e3fcec', + + 'teal-darkest': '#0d3331', + 'teal-darker': '#20504f', + 'teal-dark': '#38a89d', + 'teal': '#4dc0b5', + 'teal-light': '#64d5ca', + 'teal-lighter': '#a0f0ed', + 'teal-lightest': '#e8fffe', + + 'blue-darkest': '#12283a', + 'blue-darker': '#1c3d5a', + 'blue-dark': '#2779bd', + 'blue': '#3490dc', + 'blue-light': '#6cb2eb', + 'blue-lighter': '#bcdefa', + 'blue-lightest': '#eff8ff', + + 'indigo-darkest': '#191e38', + 'indigo-darker': '#2f365f', + 'indigo-dark': '#5661b3', + 'indigo': '#6574cd', + 'indigo-light': '#7886d7', + 'indigo-lighter': '#b2b7ff', + 'indigo-lightest': '#e6e8ff', + + 'purple-darkest': '#21183c', + 'purple-darker': '#382b5f', + 'purple-dark': '#794acf', + 'purple': '#9561e2', + 'purple-light': '#a779e9', + 'purple-lighter': '#d6bbfc', + 'purple-lightest': '#f3ebff', + + 'pink-darkest': '#451225', + 'pink-darker': '#6f213f', + 'pink-dark': '#eb5286', + 'pink': '#f66d9b', + 'pink-light': '#fa7ea8', + 'pink-lighter': '#ffbbca', + 'pink-lightest': '#ffebef', +} + +module.exports = { + + /* + |----------------------------------------------------------------------------- + | Colors https://tailwindcss.com/docs/colors + |----------------------------------------------------------------------------- + | + | The color palette defined above is also assigned to the "colors" key of + | your Tailwind config. This makes it easy to access them in your CSS + | using Tailwind's config helper. For example: + | + | .error { color: config('colors.red') } + | + */ + + colors: colors, + + + /* + |----------------------------------------------------------------------------- + | Screens https://tailwindcss.com/docs/responsive-design + |----------------------------------------------------------------------------- + | + | Screens in Tailwind are translated to CSS media queries. They define the + | responsive breakpoints for your project. By default Tailwind takes a + | "mobile first" approach, where each screen size represents a minimum + | viewport width. Feel free to have as few or as many screens as you + | want, naming them in whatever way you'd prefer for your project. + | + | Tailwind also allows for more complex screen definitions, which can be + | useful in certain situations. Be sure to see the full responsive + | documentation for a complete list of options. + | + | Class name: .{screen}:{utility} + | + */ + + screens: { + 'sm': '576px', + 'md': '768px', + 'lg': '992px', + 'xl': '1200px', + }, + + + /* + |----------------------------------------------------------------------------- + | Fonts https://tailwindcss.com/docs/fonts + |----------------------------------------------------------------------------- + | + | Here is where you define your project's font stack, or font families. + | Keep in mind that Tailwind doesn't actually load any fonts for you. + | If you're using custom fonts you'll need to import them prior to + | defining them here. + | + | By default we provide a native font stack that works remarkably well on + | any device or OS you're using, since it just uses the default fonts + | provided by the platform. + | + | Class name: .font-{name} + | + */ + + fonts: { + 'sans': [ + 'system-ui', + 'BlinkMacSystemFont', + '-apple-system', + 'Segoe UI', + 'Roboto', + 'Oxygen', + 'Ubuntu', + 'Cantarell', + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + 'sans-serif', + ], + 'serif': [ + 'Constantia', + 'Lucida Bright', + 'Lucidabright', + 'Lucida Serif', + 'Lucida', + 'DejaVu Serif', + 'Bitstream Vera Serif', + 'Liberation Serif', + 'Georgia', + 'serif', + ], + 'mono': [ + 'Menlo', + 'Monaco', + 'Consolas', + 'Liberation Mono', + 'Courier New', + 'monospace', + ], + }, + + + /* + |----------------------------------------------------------------------------- + | Text sizes https://tailwindcss.com/docs/text-sizing + |----------------------------------------------------------------------------- + | + | Here is where you define your text sizes. Name these in whatever way + | makes the most sense to you. We use size names by default, but + | you're welcome to use a numeric scale or even something else + | entirely. + | + | By default Tailwind uses the "rem" unit type for most measurements. + | This allows you to set a root font size which all other sizes are + | then based on. That said, you are free to use whatever units you + | prefer, be it rems, ems, pixels or other. + | + | Class name: .text-{size} + | + */ + + textSizes: { + 'xs': '.75rem', // 12px + 'sm': '.875rem', // 14px + 'base': '1rem', // 16px + 'lg': '1.125rem', // 18px + 'xl': '1.25rem', // 20px + '2xl': '1.5rem', // 24px + '3xl': '1.875rem', // 30px + '4xl': '2.25rem', // 36px + '5xl': '3rem', // 48px + }, + + + /* + |----------------------------------------------------------------------------- + | Font weights https://tailwindcss.com/docs/font-weight + |----------------------------------------------------------------------------- + | + | Here is where you define your font weights. We've provided a list of + | common font weight names with their respective numeric scale values + | to get you started. It's unlikely that your project will require + | all of these, so we recommend removing those you don't need. + | + | Class name: .font-{weight} + | + */ + + fontWeights: { + 'hairline': 100, + 'thin': 200, + 'light': 300, + 'normal': 400, + 'medium': 500, + 'semibold': 600, + 'bold': 700, + 'extrabold': 800, + 'black': 900, + }, + + + /* + |----------------------------------------------------------------------------- + | Leading (line height) https://tailwindcss.com/docs/line-height + |----------------------------------------------------------------------------- + | + | Here is where you define your line height values, or as we call + | them in Tailwind, leadings. + | + | Class name: .leading-{size} + | + */ + + leading: { + 'none': 1, + 'tight': 1.25, + 'normal': 1.5, + 'loose': 2, + }, + + + /* + |----------------------------------------------------------------------------- + | Tracking (letter spacing) https://tailwindcss.com/docs/letter-spacing + |----------------------------------------------------------------------------- + | + | Here is where you define your letter spacing values, or as we call + | them in Tailwind, tracking. + | + | Class name: .tracking-{size} + | + */ + + tracking: { + 'tight': '-0.05em', + 'normal': '0', + 'wide': '0.05em', + }, + + + /* + |----------------------------------------------------------------------------- + | Text colors https://tailwindcss.com/docs/text-color + |----------------------------------------------------------------------------- + | + | Here is where you define your text colors. By default these use the + | color palette we defined above, however you're welcome to set these + | independently if that makes sense for your project. + | + | Class name: .text-{color} + | + */ + + textColors: colors, + + + /* + |----------------------------------------------------------------------------- + | Background colors https://tailwindcss.com/docs/background-color + |----------------------------------------------------------------------------- + | + | Here is where you define your background colors. By default these use + | the color palette we defined above, however you're welcome to set + | these independently if that makes sense for your project. + | + | Class name: .bg-{color} + | + */ + + backgroundColors: colors, + + + /* + |----------------------------------------------------------------------------- + | Background sizes https://tailwindcss.com/docs/background-size + |----------------------------------------------------------------------------- + | + | Here is where you define your background sizes. We provide some common + | values that are useful in most projects, but feel free to add other sizes + | that are specific to your project here as well. + | + | Class name: .bg-{size} + | + */ + + backgroundSize: { + 'auto': 'auto', + 'cover': 'cover', + 'contain': 'contain', + }, + + + /* + |----------------------------------------------------------------------------- + | Border widths https://tailwindcss.com/docs/border-width + |----------------------------------------------------------------------------- + | + | Here is where you define your border widths. Take note that border + | widths require a special "default" value set as well. This is the + | width that will be used when you do not specify a border width. + | + | Class name: .border{-side?}{-width?} + | + */ + + borderWidths: { + default: '1px', + '0': '0', + '2': '2px', + '4': '4px', + '8': '8px', + }, + + + /* + |----------------------------------------------------------------------------- + | Border colors https://tailwindcss.com/docs/border-color + |----------------------------------------------------------------------------- + | + | Here is where you define your border colors. By default these use the + | color palette we defined above, however you're welcome to set these + | independently if that makes sense for your project. + | + | Take note that border colors require a special "default" value set + | as well. This is the color that will be used when you do not + | specify a border color. + | + | Class name: .border-{color} + | + */ + + borderColors: global.Object.assign({ default: colors['grey-light'] }, colors), + + + /* + |----------------------------------------------------------------------------- + | Border radius https://tailwindcss.com/docs/border-radius + |----------------------------------------------------------------------------- + | + | Here is where you define your border radius values. If a `default` radius + | is provided, it will be made available as the non-suffixed `.rounded` + | utility. + | + | If your scale includes a `0` value to reset already rounded corners, it's + | a good idea to put it first so other values are able to override it. + | + | Class name: .rounded{-side?}{-size?} + | + */ + + borderRadius: { + 'none': '0', + 'sm': '.125rem', + default: '.25rem', + 'lg': '.5rem', + 'full': '9999px', + }, + + + /* + |----------------------------------------------------------------------------- + | Width https://tailwindcss.com/docs/width + |----------------------------------------------------------------------------- + | + | Here is where you define your width utility sizes. These can be + | percentage based, pixels, rems, or any other units. By default + | we provide a sensible rem based numeric scale, a percentage + | based fraction scale, plus some other common use-cases. You + | can, of course, modify these values as needed. + | + | + | It's also worth mentioning that Tailwind automatically escapes + | invalid CSS class name characters, which allows you to have + | awesome classes like .w-2/3. + | + | Class name: .w-{size} + | + */ + + width: { + 'auto': 'auto', + 'px': '1px', + '1': '0.25rem', + '2': '0.5rem', + '3': '0.75rem', + '4': '1rem', + '5': '1.25rem', + '6': '1.5rem', + '8': '2rem', + '10': '2.5rem', + '12': '3rem', + '16': '4rem', + '24': '6rem', + '32': '8rem', + '48': '12rem', + '64': '16rem', + '1/2': '50%', + '31percent': '31%', + '1/3': '33.33333%', + '2/3': '66.66667%', + '1/4': '25%', + '3/4': '75%', + '1/5': '20%', + '2/5': '40%', + '3/5': '60%', + '4/5': '80%', + '1/6': '16.66667%', + '5/6': '83.33333%', + 'full': '100%', + 'screen': '100vw', + }, + + + /* + |----------------------------------------------------------------------------- + | Height https://tailwindcss.com/docs/height + |----------------------------------------------------------------------------- + | + | Here is where you define your height utility sizes. These can be + | percentage based, pixels, rems, or any other units. By default + | we provide a sensible rem based numeric scale plus some other + | common use-cases. You can, of course, modify these values as + | needed. + | + | Class name: .h-{size} + | + */ + + height: { + 'auto': 'auto', + 'px': '1px', + '1': '0.25rem', + '2': '0.5rem', + '3': '0.75rem', + '4': '1rem', + '5': '1.25rem', + '6': '1.5rem', + '8': '2rem', + '10': '2.5rem', + '12': '3rem', + '16': '4rem', + '24': '6rem', + '32': '8rem', + '48': '12rem', + '64': '16rem', + '72': '18rem', + 'full': '100%', + 'screen': '100vh', + }, + + + /* + |----------------------------------------------------------------------------- + | Minimum width https://tailwindcss.com/docs/min-width + |----------------------------------------------------------------------------- + | + | Here is where you define your minimum width utility sizes. These can + | be percentage based, pixels, rems, or any other units. We provide a + | couple common use-cases by default. You can, of course, modify + | these values as needed. + | + | Class name: .min-w-{size} + | + */ + + minWidth: { + '0': '0', + 'full': '100%', + }, + + + /* + |----------------------------------------------------------------------------- + | Minimum height https://tailwindcss.com/docs/min-height + |----------------------------------------------------------------------------- + | + | Here is where you define your minimum height utility sizes. These can + | be percentage based, pixels, rems, or any other units. We provide a + | few common use-cases by default. You can, of course, modify these + | values as needed. + | + | Class name: .min-h-{size} + | + */ + + minHeight: { + '0': '0', + 'full': '100%', + 'screen': '100vh', + }, + + + /* + |----------------------------------------------------------------------------- + | Maximum width https://tailwindcss.com/docs/max-width + |----------------------------------------------------------------------------- + | + | Here is where you define your maximum width utility sizes. These can + | be percentage based, pixels, rems, or any other units. By default + | we provide a sensible rem based scale and a "full width" size, + | which is basically a reset utility. You can, of course, + | modify these values as needed. + | + | Class name: .max-w-{size} + | + */ + + maxWidth: { + 'xs': '20rem', + 'sm': '30rem', + 'md': '40rem', + 'lg': '50rem', + 'xl': '60rem', + '2xl': '70rem', + '3xl': '80rem', + '4xl': '90rem', + '5xl': '100rem', + 'full': '100%', + }, + + + /* + |----------------------------------------------------------------------------- + | Maximum height https://tailwindcss.com/docs/max-height + |----------------------------------------------------------------------------- + | + | Here is where you define your maximum height utility sizes. These can + | be percentage based, pixels, rems, or any other units. We provide a + | couple common use-cases by default. You can, of course, modify + | these values as needed. + | + | Class name: .max-h-{size} + | + */ + + maxHeight: { + 'full': '100%', + 'screen': '100vh', + }, + + + /* + |----------------------------------------------------------------------------- + | Padding https://tailwindcss.com/docs/padding + |----------------------------------------------------------------------------- + | + | Here is where you define your padding utility sizes. These can be + | percentage based, pixels, rems, or any other units. By default we + | provide a sensible rem based numeric scale plus a couple other + | common use-cases like "1px". You can, of course, modify these + | values as needed. + | + | Class name: .p{side?}-{size} + | + */ + + padding: { + 'px': '1px', + '0': '0', + '1': '0.25rem', + '2': '0.5rem', + '3': '0.75rem', + '4': '1rem', + '5': '1.25rem', + '6': '1.5rem', + '8': '2rem', + '10': '2.5rem', + '12': '3rem', + '16': '4rem', + '20': '5rem', + '24': '6rem', + '32': '8rem', + '48': '12rem', + }, + + + /* + |----------------------------------------------------------------------------- + | Margin https://tailwindcss.com/docs/margin + |----------------------------------------------------------------------------- + | + | Here is where you define your margin utility sizes. These can be + | percentage based, pixels, rems, or any other units. By default we + | provide a sensible rem based numeric scale plus a couple other + | common use-cases like "1px". You can, of course, modify these + | values as needed. + | + | Class name: .m{side?}-{size} + | + */ + + margin: { + 'auto': 'auto', + 'px': '1px', + '0': '0', + '1': '0.25rem', + '2': '0.5rem', + '3': '0.75rem', + '4': '1rem', + '5': '1.25rem', + '6': '1.5rem', + '8': '2rem', + '10': '2.5rem', + '12': '3rem', + '16': '4rem', + '20': '5rem', + '24': '6rem', + '32': '8rem', + '48': '12rem', + }, + + + /* + |----------------------------------------------------------------------------- + | Negative margin https://tailwindcss.com/docs/negative-margin + |----------------------------------------------------------------------------- + | + | Here is where you define your negative margin utility sizes. These can + | be percentage based, pixels, rems, or any other units. By default we + | provide matching values to the padding scale since these utilities + | generally get used together. You can, of course, modify these + | values as needed. + | + | Class name: .-m{side?}-{size} + | + */ + + negativeMargin: { + 'px': '1px', + '0': '0', + '1': '0.25rem', + '2': '0.5rem', + '3': '0.75rem', + '4': '1rem', + '5': '1.25rem', + '6': '1.5rem', + '8': '2rem', + '10': '2.5rem', + '12': '3rem', + '16': '4rem', + '20': '5rem', + '24': '6rem', + '32': '8rem', + }, + + + /* + |----------------------------------------------------------------------------- + | Shadows https://tailwindcss.com/docs/shadows + |----------------------------------------------------------------------------- + | + | Here is where you define your shadow utilities. As you can see from + | the defaults we provide, it's possible to apply multiple shadows + | per utility using comma separation. + | + | If a `default` shadow is provided, it will be made available as the non- + | suffixed `.shadow` utility. + | + | Class name: .shadow-{size?} + | + */ + + shadows: { + default: '0 2px 4px 0 rgba(0,0,0,0.10)', + 'md': '0 4px 8px 0 rgba(0,0,0,0.12), 0 2px 4px 0 rgba(0,0,0,0.08)', + 'lg': '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)', + 'inner': 'inset 0 2px 4px 0 rgba(0,0,0,0.06)', + 'outline': '0 0 0 3px rgba(52,144,220,0.5)', + 'none': 'none', + }, + + + /* + |----------------------------------------------------------------------------- + | Z-index https://tailwindcss.com/docs/z-index + |----------------------------------------------------------------------------- + | + | Here is where you define your z-index utility values. By default we + | provide a sensible numeric scale. You can, of course, modify these + | values as needed. + | + | Class name: .z-{index} + | + */ + + zIndex: { + 'auto': 'auto', + '0': 0, + '10': 10, + '20': 20, + '30': 30, + '40': 40, + '50': 50, + }, + + + /* + |----------------------------------------------------------------------------- + | Opacity https://tailwindcss.com/docs/opacity + |----------------------------------------------------------------------------- + | + | Here is where you define your opacity utility values. By default we + | provide a sensible numeric scale. You can, of course, modify these + | values as needed. + | + | Class name: .opacity-{name} + | + */ + + opacity: { + '0': '0', + '25': '.25', + '50': '.5', + '75': '.75', + '100': '1', + }, + + + /* + |----------------------------------------------------------------------------- + | SVG fill https://tailwindcss.com/docs/svg + |----------------------------------------------------------------------------- + | + | Here is where you define your SVG fill colors. By default we just provide + | `fill-current` which sets the fill to the current text color. This lets you + | specify a fill color using existing text color utilities and helps keep the + | generated CSS file size down. + | + | Class name: .fill-{name} + | + */ + + svgFill: { + 'current': 'currentColor', + }, + + + /* + |----------------------------------------------------------------------------- + | SVG stroke https://tailwindcss.com/docs/svg + |----------------------------------------------------------------------------- + | + | Here is where you define your SVG stroke colors. By default we just provide + | `stroke-current` which sets the stroke to the current text color. This lets + | you specify a stroke color using existing text color utilities and helps + | keep the generated CSS file size down. + | + | Class name: .stroke-{name} + | + */ + + svgStroke: { + 'current': 'currentColor', + }, + + + /* + |----------------------------------------------------------------------------- + | Modules https://tailwindcss.com/docs/configuration#modules + |----------------------------------------------------------------------------- + | + | Here is where you control which modules are generated and what variants are + | generated for each of those modules. + | + | Currently supported variants: + | - responsive + | - hover + | - focus + | - focus-within + | - active + | - group-hover + | + | To disable a module completely, use `false` instead of an array. + | + */ + + modules: { + appearance: ['responsive'], + backgroundAttachment: ['responsive'], + backgroundColors: ['responsive', 'hover', 'focus'], + backgroundPosition: ['responsive'], + backgroundRepeat: ['responsive'], + backgroundSize: ['responsive'], + borderCollapse: [], + borderColors: ['responsive', 'hover', 'focus'], + borderRadius: ['responsive'], + borderStyle: ['responsive'], + borderWidths: ['responsive'], + cursor: ['responsive'], + display: ['responsive'], + flexbox: ['responsive'], + float: ['responsive'], + fonts: ['responsive'], + fontWeights: ['responsive', 'hover', 'focus'], + height: ['responsive'], + leading: ['responsive'], + lists: ['responsive'], + margin: ['responsive'], + maxHeight: ['responsive'], + maxWidth: ['responsive'], + minHeight: ['responsive'], + minWidth: ['responsive'], + negativeMargin: ['responsive'], + objectFit: false, + objectPosition: false, + opacity: ['responsive', 'group-hover'], + outline: ['focus'], + overflow: ['responsive'], + padding: ['responsive'], + pointerEvents: ['responsive'], + position: ['responsive'], + resize: ['responsive'], + shadows: ['responsive', 'hover', 'focus'], + svgFill: [], + svgStroke: [], + tableLayout: ['responsive'], + textAlign: ['responsive'], + textColors: ['responsive', 'hover', 'focus', 'group-hover'], + textSizes: ['responsive'], + textStyle: ['responsive', 'hover', 'focus'], + tracking: ['responsive'], + userSelect: ['responsive'], + verticalAlign: ['responsive'], + visibility: ['responsive'], + whitespace: ['responsive'], + width: ['responsive'], + zIndex: ['responsive'], + }, + + + /* + |----------------------------------------------------------------------------- + | Plugins https://tailwindcss.com/docs/plugins + |----------------------------------------------------------------------------- + | + | Here is where you can register any plugins you'd like to use in your + | project. Tailwind's built-in `container` plugin is enabled by default to + | give you a Bootstrap-style responsive container component out of the box. + | + | Be sure to view the complete plugin documentation to learn more about how + | the plugin system works. + | + */ + + plugins: [ + require('tailwindcss/plugins/container')({ + center: true, + padding: '1rem', + }), + ], + + + /* + |----------------------------------------------------------------------------- + | Advanced Options https://tailwindcss.com/docs/configuration#options + |----------------------------------------------------------------------------- + | + | Here is where you can tweak advanced configuration options. We recommend + | leaving these options alone unless you absolutely need to change them. + | + */ + + options: { + prefix: '', + important: false, + separator: ':', + }, + +} diff --git a/assets/webpack.config.js b/assets/webpack.config.js new file mode 100644 index 000000000..bce37233a --- /dev/null +++ b/assets/webpack.config.js @@ -0,0 +1,48 @@ +const path = require('path'); +const glob = require('glob'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); +const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const DefinePlugin = require('webpack').DefinePlugin; + +module.exports = (env, options) => ({ + optimization: { + minimizer: [ + new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: false }), + new OptimizeCSSAssetsPlugin({}) + ] + }, + entry: { + 'app': ['./js/app.js'], + 'p': ['./js/p.js'], + 'analytics': ['./js/plausible.js'], + 'plausible': ['./js/plausible.js'] + }, + output: { + filename: '[name].js', + path: path.resolve(__dirname, '../priv/static/js') + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader' + } + }, + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] + } + ] + }, + plugins: [ + new MiniCssExtractPlugin({ filename: '../css/app.css' }), + new CopyWebpackPlugin([{ from: 'static/', to: '../' }]), + new DefinePlugin({ + "BASE_URL": JSON.stringify(process.env.BASE_URL || "http://localtest.me:8000") + }), + ] +}); diff --git a/compile b/compile new file mode 100644 index 000000000..e34bb58f9 --- /dev/null +++ b/compile @@ -0,0 +1,4 @@ +cd $phoenix_dir +npm --prefix ./assets run deploy +mix "${phoenix_ex}.digest" +mix "${phoenix_ex}.digest.clean" diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 000000000..f58873c9d --- /dev/null +++ b/config/config.exs @@ -0,0 +1,53 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +# +# This configuration file is loaded before any dependency and +# is restricted to this project. + +# General application configuration +use Mix.Config + +config :plausible, + ecto_repos: [Plausible.Repo] + +# Configures the endpoint +config :plausible, PlausibleWeb.Endpoint, + url: [host: "localhost"], + secret_key_base: "/NJrhNtbyCVAsTyvtk1ZYCwfm981Vpo/0XrVwjJvemDaKC/vsvBRevLwsc6u8RCg", + render_errors: [view: PlausibleWeb.ErrorView, accepts: ~w(html json)], + pubsub: [name: Plausible.PubSub, adapter: Phoenix.PubSub.PG2] + +config :sentry, + dsn: "https://0350a42aa6234a2eaf1230866788598e@sentry.io/1382353", + included_environments: [:prod, :staging], + environment_name: String.to_atom(Map.get(System.get_env(), "APP_ENV", "dev")), + enable_source_code_context: true, + root_source_code_path: File.cwd! + +# Configures Elixir's Logger +config :logger, :console, + format: "$time $metadata[$level] $message\n", + metadata: [:request_id] + +# Use Jason for JSON parsing in Phoenix +config :phoenix, :json_library, Jason + +config :ua_inspector, + database_path: "priv/ua_inspector" + +config :ref_inspector, + database_path: "priv/ref_inspector" + +config :plausible, PlausibleWeb.Endpoint, + instrumenters: [Appsignal.Phoenix.Instrumenter] + +config :phoenix, :template_engines, + eex: Appsignal.Phoenix.Template.EExEngine, + exs: Appsignal.Phoenix.Template.ExsEngine + +config :plausible, + paddle_api: Plausible.Billing.PaddleApi + +# Import environment specific config. This must remain at the bottom +# of this file so it overrides the configuration defined above. +import_config "#{Mix.env()}.exs" diff --git a/config/dev.exs b/config/dev.exs new file mode 100644 index 000000000..e918e1bde --- /dev/null +++ b/config/dev.exs @@ -0,0 +1,48 @@ +use Mix.Config + +# For development, we disable any cache and enable +# debugging and code reloading. +# +# The watchers configuration can be used to run external +# watchers to your application. For example, we use it +# with webpack to recompile .js and .css sources. +config :plausible, PlausibleWeb.Endpoint, + http: [port: 8000], + debug_errors: true, + code_reloader: true, + check_origin: false, + watchers: [ + node: [ + "node_modules/webpack/bin/webpack.js", + "--mode", + "development", + "--watch-stdin", + cd: Path.expand("../assets", __DIR__) + ] + ] + +config :plausible, PlausibleWeb.Endpoint, + live_reload: [ + patterns: [ + ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, + ~r{priv/gettext/.*(po)$}, + ~r{lib/plausible_web/views/.*(ex)$}, + ~r{lib/plausible_web/templates/.*(eex)$} + ] + ] + +config :logger, :console, format: "[$level] $message\n" +config :phoenix, :stacktrace_depth, 20 +config :phoenix, :plug_init_mode, :runtime + +config :plausible, Plausible.Repo, + username: "postgres", + password: "postgres", + database: "plausible_dev", + hostname: "localhost", + pool_size: 10 + +config :plausible, Plausible.Mailer, + adapter: Bamboo.LocalAdapter + +import_config "dev.secret.exs" diff --git a/config/prod.exs b/config/prod.exs new file mode 100644 index 000000000..58ec155ba --- /dev/null +++ b/config/prod.exs @@ -0,0 +1,91 @@ +use Mix.Config + +# For production, don't forget to configure the url host +# to something meaningful, Phoenix uses this information +# when generating URLs. +# +# Note we also include the path to a cache manifest +# containing the digested version of static files. This +# manifest is generated by the `mix phx.digest` task, +# which you should run after static files are built and +# before starting your production server. +config :plausible, PlausibleWeb.Endpoint, + http: [:inet6, port: System.get_env("PORT") || 4000], + url: [host: System.get_env("HOST"), scheme: "https"], + cache_static_manifest: "priv/static/cache_manifest.json" + +# Do not print debug messages in production +config :logger, level: :info + +# ## SSL Support +# +# To get SSL working, you will need to add the `https` key +# to the previous section and set your `:url` port to 443: +# +# config :plausible, PlausibleWeb.Endpoint, +# ... +# url: [host: "example.com", port: 443], +# https: [ +# :inet6, +# port: 443, +# cipher_suite: :strong, +# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), +# certfile: System.get_env("SOME_APP_SSL_CERT_PATH") +# ] +# +# The `cipher_suite` is set to `:strong` to support only the +# latest and more secure SSL ciphers. This means old browsers +# and clients may not be supported. You can set it to +# `:compatible` for wider support. +# +# `:keyfile` and `:certfile` expect an absolute path to the key +# and cert in disk or a relative path inside priv, for example +# "priv/ssl/server.key". For all supported SSL configuration +# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 +# +# We also recommend setting `force_ssl` in your endpoint, ensuring +# no data is ever sent via http, always redirecting to https: +# +# config :plausible, PlausibleWeb.Endpoint, +# force_ssl: [hsts: true] +# +# Check `Plug.SSL` for all available options in `force_ssl`. + +# ## Using releases (distillery) +# +# If you are doing OTP releases, you need to instruct Phoenix +# to start the server for all endpoints: +# +# config :phoenix, :serve_endpoints, true +# +# Alternatively, you can configure exactly which server to +# start per endpoint: +# +# config :plausible, PlausibleWeb.Endpoint, server: true +# +# Note you can't rely on `System.get_env/1` when using releases. +# See the releases documentation accordingly. + +# Finally import the config/prod.secret.exs which should be versioned +# separately. +config :plausible, PlausibleWeb.Endpoint, + secret_key_base: System.get_env("SECRET_KEY_BASE") + +# Configure your database +config :plausible, Plausible.Repo, + adapter: Ecto.Adapters.Postgres, + url: System.get_env("AVIEN_DATABASE_URL"), + pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), + timeout: 10_000, + ssl: true + +config :plausible, :amplitude, + api_key: System.get_env("AMPLITUDE_API_KEY") + +config :plausible, :google, + client_id: System.get_env("GOOGLE_CLIENT_ID"), + client_secret: System.get_env("GOOGLE_CLIENT_SECRET") + +config :plausible, Plausible.Mailer, + adapter: Bamboo.PostmarkAdapter, + api_key: System.get_env("POSTMARK_API_KEY") diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 000000000..979fbe82e --- /dev/null +++ b/config/test.exs @@ -0,0 +1,27 @@ +use Mix.Config + +# We don't run a server during test. If one is required, +# you can enable the server option below. +config :plausible, PlausibleWeb.Endpoint, + http: [port: 4002], + server: false + +# Print only warnings and errors during test +config :logger, level: :warn + +# Reduce bcrypt rounds to speed up test suite +config :bcrypt_elixir, :log_rounds, 4 + +# Configure your database +config :plausible, Plausible.Repo, + username: "postgres", + password: "postgres", + database: "plausible_test", + hostname: "localhost", + pool: Ecto.Adapters.SQL.Sandbox + +config :plausible, Plausible.Mailer, + adapter: Bamboo.TestAdapter + +config :plausible, + paddle_api: Plausible.PaddleApi.Mock diff --git a/elixir_buildpack.config b/elixir_buildpack.config new file mode 100644 index 000000000..b187da455 --- /dev/null +++ b/elixir_buildpack.config @@ -0,0 +1,3 @@ +erlang_version=21.1.1 +elixir_version=1.7.4 +always_rebuild=false diff --git a/lib/mix/tasks/check_overuse.ex b/lib/mix/tasks/check_overuse.ex new file mode 100644 index 000000000..fee235df0 --- /dev/null +++ b/lib/mix/tasks/check_overuse.ex @@ -0,0 +1,35 @@ +defmodule Mix.Tasks.CheckOveruse do + use Mix.Task + use Plausible.Repo + require Logger + + @doc """ + This is scheduled to run every 6 hours. + """ + + def run(args) do + Application.ensure_all_started(:plausible) + Logger.configure(level: :error) + execute(args) + end + + def execute(_args \\ []) do + active_users = Repo.all( + from u in Plausible.Auth.User, + join: s in Plausible.Billing.Subscription, on: s.user_id == u.id, + where: s.status == "active", + select: {u, s} + ) + + for {user, subscription} <- active_users do + IO.puts("Checking #{user.email}...") + usage = Plausible.Billing.usage(user) + allowance = Plausible.Billing.Plans.allowance(subscription) + if usage > allowance do + IO.puts("Overuse: #{user.email}") + IO.puts("Usage: #{usage}") + IO.puts("Allowance: #{allowance}") + end + end + end +end diff --git a/lib/mix/tasks/send_feedback_emails.ex b/lib/mix/tasks/send_feedback_emails.ex new file mode 100644 index 000000000..b68e1c085 --- /dev/null +++ b/lib/mix/tasks/send_feedback_emails.ex @@ -0,0 +1,48 @@ +defmodule Mix.Tasks.SendFeedbackEmails do + use Mix.Task + use Plausible.Repo + require Logger + + @doc """ + This is scheduled to run every 6 hours. + """ + + def run(args) do + Application.ensure_all_started(:plausible) + execute(args) + end + + def execute(args \\ []) do + q = + from(u in Plausible.Auth.User, + left_join: fe in "feedback_emails", on: fe.user_id == u.id, + where: is_nil(fe.id), + where: u.inserted_at < fragment("(now() at time zone 'utc') - '30 days'::interval"), + where: u.last_seen > fragment("now() at time zone 'utc' - '7 days'::interval") + ) + + for user <- Repo.all(q) do + if Plausible.Auth.user_completed_setup?(user) do + send_feedback_email(args, user) + end + end + end + + defp send_feedback_email(["--dry-run"], user) do + Logger.info("DRY RUN: feedback survey email to #{user.name}") + end + + defp send_feedback_email(_, user) do + PlausibleWeb.Email.feedback_survey_email(user) + |> Plausible.Mailer.deliver_now() + + feedback_email_sent(user) + end + + defp feedback_email_sent(user) do + Repo.insert_all("feedback_emails", [%{ + user_id: user.id, + timestamp: NaiveDateTime.utc_now() + }]) + end +end diff --git a/lib/mix/tasks/send_intro_emails.ex b/lib/mix/tasks/send_intro_emails.ex new file mode 100644 index 000000000..ad9901265 --- /dev/null +++ b/lib/mix/tasks/send_intro_emails.ex @@ -0,0 +1,64 @@ +defmodule Mix.Tasks.SendIntroEmails do + use Mix.Task + use Plausible.Repo + require Logger + + @doc """ + This is scheduled to run every 6 hours. + """ + + def run(args) do + Application.ensure_all_started(:plausible) + execute(args) + end + + def execute(args \\ []) do + q = + from(u in Plausible.Auth.User, + left_join: ie in "intro_emails", on: ie.user_id == u.id, + where: is_nil(ie.id), + where: + u.inserted_at > fragment("(now() at time zone 'utc') - '24 hours'::interval") and + u.inserted_at < fragment("(now() at time zone 'utc') - '6 hours'::interval") + ) + + for user <- Repo.all(q) do + if Plausible.Auth.user_completed_setup?(user) do + Logger.info("#{user.name} has completed the setup. Sending welcome email.") + send_welcome_email(args, user) + else + Logger.info("#{user.name} has not completed the setup. Sending help email.") + send_help_email(args, user) + end + end + end + + defp send_welcome_email(["--dry-run"], user) do + Logger.info("DRY RUN: welcome email to #{user.name}") + end + + defp send_welcome_email(_, user) do + PlausibleWeb.Email.welcome_email(user) + |> Plausible.Mailer.deliver_now() + + intro_email_sent(user) + end + + defp send_help_email(["--dry-run"], user) do + Logger.info("DRY RUN: help email to #{user.name}") + end + + defp send_help_email(_, user) do + PlausibleWeb.Email.help_email(user) + |> Plausible.Mailer.deliver_now() + + intro_email_sent(user) + end + + defp intro_email_sent(user) do + Repo.insert_all("intro_emails", [%{ + user_id: user.id, + timestamp: NaiveDateTime.utc_now() + }]) + end +end diff --git a/lib/mix/tasks/send_trial_notifications.ex b/lib/mix/tasks/send_trial_notifications.ex new file mode 100644 index 000000000..67cbf0e22 --- /dev/null +++ b/lib/mix/tasks/send_trial_notifications.ex @@ -0,0 +1,124 @@ +defmodule Mix.Tasks.SendTrialNotifications do + use Mix.Task + use Plausible.Repo + require Logger + + @doc """ + This is scheduled to run every day. + """ + + def run(args) do + Application.ensure_all_started(:plausible) + execute(args) + end + + def execute(args \\ []) do + base_query = + from(u in Plausible.Auth.User, + left_join: s in Plausible.Billing.Subscription, on: s.user_id == u.id, + where: is_nil(s.id), + order_by: u.inserted_at + ) + + users = Repo.all(base_query) + + for user <- users do + case Timex.diff(Plausible.Billing.trial_end_date(user), Timex.today(), :days) do + 14 -> + if Plausible.Auth.user_completed_setup?(user) do + send_two_week_reminder(args, user) + end + 1 -> + if Plausible.Auth.user_completed_setup?(user) do + send_tomorrow_reminder(args, user) + end + 0 -> + if Plausible.Auth.user_completed_setup?(user) do + send_today_reminder(args, user) + end + -1 -> + if Plausible.Auth.user_completed_setup?(user) do + send_over_reminder(args, user) + end + _ -> + nil + end + end + + #two_weeks_left = from( + # u in base_query, + # where: type(u.inserted_at, :date) == fragment("now()::date - '14 days'::interval") + #) + + #tomorrow = from( + # u in base_query, + # where: type(u.inserted_at, :date) == fragment("now()::date - '29 days'::interval") + #) + + #today = from( + # u in base_query, + # where: type(u.inserted_at, :date) == fragment("now()::date - '30 days'::interval") + #) + + #yesterday = from( + # u in base_query, + # where: type(u.inserted_at, :date) == fragment("now()::date - '31 days'::interval") + #) + + #for user <- Repo.all(two_weeks_left) do + # if Plausible.Auth.user_completed_setup?(user), do: send_two_week_reminder(args, user) + #end + + #for user <- Repo.all(tomorrow) do + # if Plausible.Auth.user_completed_setup?(user), do: send_tomorrow_reminder(args, user) + #end + + #for user <- Repo.all(today) do + # if Plausible.Auth.user_completed_setup?(user), do: send_today_reminder(args, user) + #end + + #for user <- Repo.all(yesterday) do + # if Plausible.Auth.user_completed_setup?(user), do: send_over_reminder(args, user) + #end + end + + defp send_two_week_reminder(["--dry-run"], user) do + Logger.info("DRY RUN: 2-week trial notification email to #{user.name} [inserted=#{user.inserted_at}]") + end + + defp send_two_week_reminder(_, user) do + PlausibleWeb.Email.trial_two_week_reminder(user) + |> Plausible.Mailer.deliver_now() + end + + defp send_tomorrow_reminder(["--dry-run"], user) do + Logger.info("DRY RUN: tomorrow trial upgrade email to #{user.name} [inserted=#{user.inserted_at}]") + end + + defp send_tomorrow_reminder(_, user) do + usage = Plausible.Billing.usage(user) + + PlausibleWeb.Email.trial_upgrade_email(user, "tomorrow", usage) + |> Plausible.Mailer.deliver_now() + end + + defp send_today_reminder(["--dry-run"], user) do + Logger.info("DRY RUN: today trial upgrade email to #{user.name} [inserted=#{user.inserted_at}]") + end + + defp send_today_reminder(_, user) do + usage = Plausible.Billing.usage(user) + + PlausibleWeb.Email.trial_upgrade_email(user, "today", usage) + |> Plausible.Mailer.deliver_now() + end + + defp send_over_reminder(["--dry-run"], user) do + Logger.info("DRY RUN: over trial notification email to #{user.name} [inserted=#{user.inserted_at}]") + end + + defp send_over_reminder(_, user) do + PlausibleWeb.Email.trial_over_email(user) + |> Plausible.Mailer.deliver_now() + end +end diff --git a/lib/plausible.ex b/lib/plausible.ex new file mode 100644 index 000000000..ebdaaaaed --- /dev/null +++ b/lib/plausible.ex @@ -0,0 +1,9 @@ +defmodule Plausible do + @moduledoc """ + Plausible keeps the contexts that define your domain + and business logic. + + Contexts are also responsible for managing your data, regardless + if it comes from the database, an external API or others. + """ +end diff --git a/lib/plausible/application.ex b/lib/plausible/application.ex new file mode 100644 index 000000000..f92bb6040 --- /dev/null +++ b/lib/plausible/application.ex @@ -0,0 +1,27 @@ +defmodule Plausible.Application do + @moduledoc false + + use Application + + def start(_type, _args) do + children = [ + Plausible.Repo, + PlausibleWeb.Endpoint + ] + + opts = [strategy: :one_for_one, name: Plausible.Supervisor] + {:ok, _} = Logger.add_backend(Sentry.LoggerBackend) + :telemetry.attach( + "appsignal-ecto", + [:plausible, :repo, :query], + &Appsignal.Ecto.handle_event/4, + nil + ) + Supervisor.start_link(children, opts) + end + + def config_change(changed, _new, removed) do + PlausibleWeb.Endpoint.config_change(changed, removed) + :ok + end +end diff --git a/lib/plausible/auth/auth.ex b/lib/plausible/auth/auth.ex new file mode 100644 index 000000000..dbbc563d6 --- /dev/null +++ b/lib/plausible/auth/auth.ex @@ -0,0 +1,30 @@ +defmodule Plausible.Auth do + use Plausible.Repo + alias Plausible.Auth + + def create_user(name, email) do + %Auth.User{} + |> Auth.User.changeset(%{name: name, email: email}) + |> Repo.insert + end + + def find_user_by(opts) do + Repo.get_by(Auth.User, opts) + end + + def user_completed_setup?(user) do + query = + from( + p in Plausible.Pageview, + join: s in Plausible.Site, + on: s.domain == p.hostname, + join: sm in Plausible.Site.Membership, + on: sm.site_id == s.id, + join: u in Plausible.Auth.User, + on: sm.user_id == u.id, + where: u.id == ^user.id + ) + + Repo.exists?(query) + end +end diff --git a/lib/plausible/auth/password.ex b/lib/plausible/auth/password.ex new file mode 100644 index 000000000..193d6e97a --- /dev/null +++ b/lib/plausible/auth/password.ex @@ -0,0 +1,13 @@ +defmodule Plausible.Auth.Password do + def hash(password) do + Bcrypt.hash_pwd_salt(password) + end + + def match?(password, hash) do + Bcrypt.verify_pass(password, hash) + end + + def dummy_calculation do + Bcrypt.no_user_verify() + end +end diff --git a/lib/plausible/auth/token.ex b/lib/plausible/auth/token.ex new file mode 100644 index 000000000..f3215ec6c --- /dev/null +++ b/lib/plausible/auth/token.ex @@ -0,0 +1,23 @@ +defmodule Plausible.Auth.Token do + @one_day_in_seconds 30 * 60 * 24 + @one_hour_in_seconds 30 * 60 + + def sign_password_reset(email) do + Phoenix.Token.sign(PlausibleWeb.Endpoint, "password-reset", %{email: email}) + end + + def verify_password_reset(token) do + Phoenix.Token.verify(PlausibleWeb.Endpoint, "password-reset", token, max_age: @one_hour_in_seconds) + end + + def sign_activation(name, email) do + Phoenix.Token.sign(PlausibleWeb.Endpoint, "activation", %{ + name: name, + email: email + }) + end + + def verify_activation(token) do + Phoenix.Token.verify(PlausibleWeb.Endpoint, "activation", token, max_age: @one_day_in_seconds) + end +end diff --git a/lib/plausible/auth/user.ex b/lib/plausible/auth/user.ex new file mode 100644 index 000000000..b75887a05 --- /dev/null +++ b/lib/plausible/auth/user.ex @@ -0,0 +1,41 @@ +defimpl Bamboo.Formatter, for: Plausible.Auth.User do + def format_email_address(user, _opts) do + {user.name, user.email} + end +end + +defmodule Plausible.Auth.User do + use Ecto.Schema + import Ecto.Changeset + + schema "users" do + field :email, :string + field :password_hash + field :password, :string, virtual: true + field :name, :string + field :last_seen, :naive_datetime + + has_many :site_memberships, Plausible.Site.Membership + has_many :sites, through: [:site_memberships, :site] + has_one :google_auth, Plausible.Site.GoogleAuth + + timestamps() + end + + def changeset(user, attrs \\ %{}) do + user + |> cast(attrs, [:email, :name]) + |> validate_required([:email, :name]) + |> unique_constraint(:email) + end + + def set_password(user, password) do + hash = Plausible.Auth.Password.hash(password) + + user + |> cast(%{password: password}, [:password]) + |> validate_required(:password) + |> validate_length(:password, min: 6) + |> cast(%{password_hash: hash}, [:password_hash]) + end +end diff --git a/lib/plausible/billing/billing.ex b/lib/plausible/billing/billing.ex new file mode 100644 index 000000000..548e19023 --- /dev/null +++ b/lib/plausible/billing/billing.ex @@ -0,0 +1,133 @@ +defmodule Plausible.Billing do + use Plausible.Repo + alias Plausible.Billing.{Subscription, Plans, PaddleApi} + @paddle_api Application.fetch_env!(:plausible, :paddle_api) + + def active_subscription_for(user_id) do + Repo.get_by(Subscription, user_id: user_id, status: "active") + end + + def subscription_created(params) do + changeset = Subscription.changeset(%Subscription{}, format_subscription(params)) + + Repo.insert(changeset) + end + + def subscription_updated(params) do + subscription = Repo.get_by!(Subscription, paddle_subscription_id: params["subscription_id"]) + changeset = Subscription.changeset(subscription, format_subscription(params)) + + Repo.update(changeset) + end + + def subscription_cancelled(params) do + subscription = Repo.get_by(Subscription, paddle_subscription_id: params["subscription_id"]) + + if subscription do + changeset = Subscription.changeset(subscription, %{ + status: params["status"] + }) + + Repo.update(changeset) + else + {:ok, nil} + end + end + + def subscription_payment_succeeded(params) do + subscription = Repo.get_by(Subscription, paddle_subscription_id: params["subscription_id"]) + + if subscription do + {:ok, api_subscription} = @paddle_api.get_subscription(subscription.paddle_subscription_id) + amount = :erlang.float_to_binary(api_subscription["next_payment"]["amount"] / 1, decimals: 2) + + changeset = Subscription.changeset(subscription, %{ + next_bill_amount: amount, + next_bill_date: api_subscription["next_payment"]["date"] + }) + + Repo.update(changeset) + else + {:ok, nil} + end + end + + def change_plan(user, new_plan) do + subscription = active_subscription_for(user.id) + + res = PaddleApi.update_subscription(subscription.paddle_subscription_id, %{ + plan_id: Plans.paddle_id_for_plan(new_plan) + }) + + case res do + {:ok, response} -> + Subscription.changeset(subscription, %{ + paddle_plan_id: Integer.to_string(response["plan_id"]) + }) |> Repo.update + e -> e + end + end + + def needs_to_upgrade?(user) do + if Timex.before?(trial_end_date(user), Timex.today()) do + !active_subscription_for(user.id) + else + false + end + end + + def coupon_for(user) do + if was_beta_user(user) do + "8FE5AF26" + end + end + + def was_beta_user(user) do + Timex.before?(user.inserted_at, ~D[2019-04-25]) + end + + def trial_days_left(user) do + if Timex.before?(user.inserted_at, ~D[2019-04-24]) do + Timex.diff(~D[2019-05-25], Timex.today, :days) + 1 + else + 30 - Timex.diff(Timex.today, user.inserted_at, :days) + end + end + + def trial_end_date(user) do + if Timex.before?(user.inserted_at, ~D[2019-04-25]) do + ~D[2019-05-25] + else + Timex.shift(user.inserted_at, days: 30) |> NaiveDateTime.to_date + end + end + + def usage(user) do + user = Repo.preload(user, :sites) + Enum.reduce(user.sites, 0, fn site, total -> + total + site_usage(site) + end) + end + + defp site_usage(site) do + Repo.aggregate(from( + p in Plausible.Pageview, + where: p.hostname == ^site.domain, + where: p.inserted_at >= fragment("now() - '30 days'::interval") + ), :count, :id + ) + end + + defp format_subscription(params) do + %{ + paddle_subscription_id: params["subscription_id"], + paddle_plan_id: params["subscription_plan_id"], + cancel_url: params["cancel_url"], + update_url: params["update_url"], + user_id: params["passthrough"], + status: params["status"], + next_bill_date: params["next_bill_date"], + next_bill_amount: params["unit_price"] || params["new_unit_price"] + } + end +end diff --git a/lib/plausible/billing/paddle_api.ex b/lib/plausible/billing/paddle_api.ex new file mode 100644 index 000000000..aaaf965d2 --- /dev/null +++ b/lib/plausible/billing/paddle_api.ex @@ -0,0 +1,46 @@ +defmodule Plausible.Billing.PaddleApi do + @update_endpoint "https://vendors.paddle.com/api/2.0/subscription/users/update" + @get_endpoint "https://vendors.paddle.com/api/2.0/subscription/users" + @vendor_id "49430" + @vendor_auth_code "00e75d18fde1457171b73723ecf54c4026f9b0047e70b6dbff" + @headers [ + {"Content-type", "application/json"}, + {"Accept", "application/json"} + ] + + def update_subscription(paddle_subscription_id, params) do + params = Map.merge(params, %{ + vendor_id: @vendor_id, + vendor_auth_code: @vendor_auth_code, + subscription_id: paddle_subscription_id, + quantity: 1 + }) + + {:ok, response} = HTTPoison.post(@update_endpoint, Poison.encode!(params), @headers) + body = Poison.decode!(response.body) + + if body["success"] do + {:ok, body["response"]} + else + {:error, body["error"]} + end + end + + def get_subscription(paddle_subscription_id) do + params = %{ + vendor_id: @vendor_id, + vendor_auth_code: @vendor_auth_code, + subscription_id: paddle_subscription_id + } + + {:ok, response} = HTTPoison.post(@get_endpoint, Poison.encode!(params), @headers) + body = Poison.decode!(response.body) + + if body["success"] do + [subscription] = body["response"] + {:ok, subscription} + else + {:error, body["error"]} + end + end +end diff --git a/lib/plausible/billing/plans.ex b/lib/plausible/billing/plans.ex new file mode 100644 index 000000000..1dc6d83df --- /dev/null +++ b/lib/plausible/billing/plans.ex @@ -0,0 +1,40 @@ +defmodule Plausible.Billing.Plans do + @app_env System.get_env("APP_ENV") || "dev" + + @real_plans %{ + personal: "558018", + startup: "558745", + business: "558746" + } + + @test_plans %{ + personal: "558156", + startup: "558199", + business: "558200" + } + + def paddle_id_for_plan(plan) do + if @app_env == "prod" do + @real_plans[plan] + else + @test_plans[plan] + end + end + + def is?(subscription, plan) do + paddle_id_for_plan(plan) == subscription.paddle_plan_id + end + + def allowance(subscription) do + cond do + is?(subscription, :personal) -> + 10_000 + is?(subscription, :startup) -> + 100_000 + is?(subscription, :business) -> + 1_000_000 + true -> + raise "Subscription not found for #{subscription.paddle_plan_id}" + end + end +end diff --git a/lib/plausible/billing/subscription.ex b/lib/plausible/billing/subscription.ex new file mode 100644 index 000000000..abc5544c4 --- /dev/null +++ b/lib/plausible/billing/subscription.ex @@ -0,0 +1,29 @@ +defmodule Plausible.Billing.Subscription do + use Ecto.Schema + import Ecto.Changeset + + @required_fields [:paddle_subscription_id, :paddle_plan_id, :update_url, :cancel_url, :status, :next_bill_amount, :next_bill_date, :user_id] + @valid_statuses ["active", "past_due", "deleted"] + + schema "subscriptions" do + field :paddle_subscription_id, :string + field :paddle_plan_id, :string + field :update_url, :string + field :cancel_url, :string + field :status, :string + field :next_bill_amount, :string + field :next_bill_date, :date + + belongs_to :user, Plausible.Auth.User + + timestamps() + end + + def changeset(model, attrs \\ %{}) do + model + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> validate_inclusion(:status, @valid_statuses) + |> unique_constraint(:paddle_subscription_id) + end +end diff --git a/lib/plausible/google/api.ex b/lib/plausible/google/api.ex new file mode 100644 index 000000000..90fd3e900 --- /dev/null +++ b/lib/plausible/google/api.ex @@ -0,0 +1,92 @@ +defmodule Plausible.Google.Api do + @scope URI.encode_www_form("https://www.googleapis.com/auth/webmasters.readonly email") + + def authorize_url(site_id) do + if Mix.env() == :test do + "" + else + "https://accounts.google.com/o/oauth2/v2/auth?client_id=#{client_id()}&redirect_uri=#{redirect_uri()}&prompt=consent&response_type=code&access_type=offline&scope=#{@scope}&state=#{site_id}" + end + end + + def fetch_access_token(code) do + res = HTTPoison.post!("https://www.googleapis.com/oauth2/v4/token", "client_id=#{client_id()}&client_secret=#{client_secret()}&code=#{code}&grant_type=authorization_code&redirect_uri=#{redirect_uri()}", ["Content-Type": "application/x-www-form-urlencoded"]) + Jason.decode!(res.body) + end + + def fetch_site(domain, auth) do + auth = if Timex.before?(auth.expires, Timex.now() |> Timex.shift(seconds: 5)) do + refresh_token(auth) + else + auth + end + with_https = URI.encode_www_form("https://#{domain}") + + res = HTTPoison.get!("https://www.googleapis.com/webmasters/v3/sites/#{with_https}",["Content-Type": "application/json", "Authorization": "Bearer #{auth.access_token}"]) + + Jason.decode!(res.body) + end + + def fetch_stats(site, auth, query) do + if Timex.before?(auth.expires, Timex.now() |> Timex.shift(seconds: 5)) do + auth = refresh_token(auth) + fetch_queries(site.domain, auth, query) + else + fetch_queries(site.domain, auth, query) + end + end + + defp fetch_queries(domain, auth, query) do + with_https = URI.encode_www_form("https://#{domain}") + + res = HTTPoison.post!("https://www.googleapis.com/webmasters/v3/sites/#{with_https}/searchAnalytics/query", Jason.encode!(%{ + startDate: Date.to_iso8601(query.date_range.first), + endDate: Date.to_iso8601(query.date_range.last), + dimensions: ["query"], + rowLimit: 20 + }), ["Content-Type": "application/json", "Authorization": "Bearer #{auth.access_token}"]) + + case res.status_code do + 200 -> + terms = Jason.decode!(res.body)["rows"] + |> Enum.filter(fn row -> row["clicks"] > 0 end) + |> Enum.map(fn row -> {row["keys"], round(row["clicks"])} end) + + {:ok, terms} + 401 -> + Sentry.capture_message("Error fetching Google queries", extra: Jason.decode!(res.body)) + {:error, :invalid_credentials} + 403 -> + Sentry.capture_message("Error fetching Google queries", extra: Jason.decode!(res.body)) + msg = Jason.decode!(res.body)["error"]["message"] + {:error, msg} + _ -> + Sentry.capture_message("Error fetching Google queries", extra: Jason.decode!(res.body)) + {:error, :unknown} + end + end + + defp refresh_token(auth) do + res = HTTPoison.post!("https://www.googleapis.com/oauth2/v4/token", "client_id=#{client_id()}&client_secret=#{client_secret()}&refresh_token=#{auth.refresh_token}&grant_type=refresh_token&redirect_uri=#{redirect_uri()}", ["Content-Type": "application/x-www-form-urlencoded"]) + body = Jason.decode!(res.body) + + + Plausible.Site.GoogleAuth.changeset(auth, %{ + access_token: body["access_token"], + expires: NaiveDateTime.utc_now() |> NaiveDateTime.add(body["expires_in"]), + }) |> Plausible.Repo.update! + end + + defp client_id() do + Keyword.fetch!(Application.get_env(:plausible, :google), :client_id) + end + + defp client_secret() do + Keyword.fetch!(Application.get_env(:plausible, :google), :client_secret) + end + + defp redirect_uri() do + PlausibleWeb.Endpoint.clean_url() <> "/auth/google/callback" + end + +end diff --git a/lib/plausible/mailer.ex b/lib/plausible/mailer.ex new file mode 100644 index 000000000..60b06a7b2 --- /dev/null +++ b/lib/plausible/mailer.ex @@ -0,0 +1,3 @@ +defmodule Plausible.Mailer do + use Bamboo.Mailer, otp_app: :plausible +end diff --git a/lib/plausible/pageview/schema.ex b/lib/plausible/pageview/schema.ex new file mode 100644 index 000000000..775982ee2 --- /dev/null +++ b/lib/plausible/pageview/schema.ex @@ -0,0 +1,29 @@ +defmodule Plausible.Pageview do + use Ecto.Schema + import Ecto.Changeset + + schema "pageviews" do + field :hostname, :string + field :pathname, :string + field :referrer, :string + field :raw_referrer, :string + field :user_agent, :string + field :screen_width, :integer + field :screen_size, :string + field :new_visitor, :boolean + field :user_id, :binary_id + field :country_code, :string + + field :operating_system, :string + field :browser, :string + field :referrer_source, :string + + timestamps() + end + + def changeset(pageview, attrs) do + pageview + |> cast(attrs, [:hostname, :pathname, :referrer, :raw_referrer, :user_agent, :new_visitor, :screen_width, :user_id, :operating_system, :browser, :referrer_source, :country_code, :screen_size]) + |> validate_required([:hostname, :pathname, :new_visitor, :user_id]) + end +end diff --git a/lib/plausible/repo.ex b/lib/plausible/repo.ex new file mode 100644 index 000000000..2f9147e66 --- /dev/null +++ b/lib/plausible/repo.ex @@ -0,0 +1,13 @@ +defmodule Plausible.Repo do + use Ecto.Repo, + otp_app: :plausible, + adapter: Ecto.Adapters.Postgres + + defmacro __using__(_) do + quote do + alias Plausible.Repo + import Ecto + import Ecto.Query, only: [from: 1, from: 2] + end + end +end diff --git a/lib/plausible/site/google_auth.ex b/lib/plausible/site/google_auth.ex new file mode 100644 index 000000000..c391c7173 --- /dev/null +++ b/lib/plausible/site/google_auth.ex @@ -0,0 +1,23 @@ +defmodule Plausible.Site.GoogleAuth do + use Ecto.Schema + import Ecto.Changeset + + schema "google_auth" do + field :email, :string + field :refresh_token, :string + field :access_token, :string + field :expires, :naive_datetime + + belongs_to :user, Plausible.Auth.User + belongs_to :site, Plausible.Site + + timestamps() + end + + def changeset(auth, attrs \\ %{}) do + auth + |> cast(attrs, [:refresh_token, :access_token, :expires, :email, :user_id, :site_id]) + |> validate_required([:refresh_token, :access_token, :expires, :email, :user_id, :site_id]) + |> unique_constraint(:site) + end +end diff --git a/lib/plausible/site/membership.ex b/lib/plausible/site/membership.ex new file mode 100644 index 000000000..2732dd8cd --- /dev/null +++ b/lib/plausible/site/membership.ex @@ -0,0 +1,17 @@ +defmodule Plausible.Site.Membership do + use Ecto.Schema + import Ecto.Changeset + + schema "site_memberships" do + belongs_to :site, Plausible.Site + belongs_to :user, Plausible.Auth.User + + timestamps() + end + + def changeset(user, attrs) do + user + |> cast(attrs, [:user_id, :site_id]) + |> validate_required([:user_id, :site_id]) + end +end diff --git a/lib/plausible/site/schema.ex b/lib/plausible/site/schema.ex new file mode 100644 index 000000000..3bd90618f --- /dev/null +++ b/lib/plausible/site/schema.ex @@ -0,0 +1,47 @@ +defmodule Plausible.Site do + use Ecto.Schema + import Ecto.Changeset + alias Plausible.Auth.User + alias Plausible.Site.GoogleAuth + + schema "sites" do + field :domain, :string + field :timezone, :string + field :public, :boolean + + many_to_many :members, User, join_through: Plausible.Site.Membership + has_one :google_auth, GoogleAuth + + timestamps() + end + + def changeset(site, attrs \\ %{}) do + site + |> cast(attrs, [:domain, :timezone]) + |> validate_required([:domain, :timezone]) + |> unique_constraint(:domain) + |> clean_domain + end + + def make_public(site) do + change(site, public: true) + end + + def make_private(site) do + change(site, public: false) + end + + defp clean_domain(changeset) do + clean_domain = (get_field(changeset, :domain) || "") + |> String.trim + |> String.replace_leading("http://", "") + |> String.replace_leading("https://", "") + |> String.replace_leading("www.", "") + |> String.replace_trailing("/", "") + |> String.downcase() + + change(changeset, %{ + domain: clean_domain + }) + end +end diff --git a/lib/plausible/sites.ex b/lib/plausible/sites.ex new file mode 100644 index 000000000..95884c882 --- /dev/null +++ b/lib/plausible/sites.ex @@ -0,0 +1,27 @@ +defmodule Plausible.Sites do + use Plausible.Repo + + def get_for_user!(user_id, domain) do + Repo.one!( + from s in Plausible.Site, + join: sm in Plausible.Site.Membership, on: sm.site_id == s.id, + where: sm.user_id == ^user_id, + where: s.domain == ^domain, + select: s + ) + end + + def has_pageviews?(site) do + Repo.exists?( + from p in Plausible.Pageview, + where: p.hostname == ^site.domain + ) + end + + def is_owner?(user_id, site) do + Repo.exists?( + from sm in Plausible.Site.Membership, + where: sm.user_id == ^user_id and sm.site_id == ^site.id + ) + end +end diff --git a/lib/plausible/slack.ex b/lib/plausible/slack.ex new file mode 100644 index 000000000..cd5f37d5a --- /dev/null +++ b/lib/plausible/slack.ex @@ -0,0 +1,15 @@ +defmodule Plausible.Slack do + @app_env System.get_env("APP_ENV") || "dev" + @feed_channel_url "https://hooks.slack.com/services/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" + + def notify(text) do + Task.start(fn -> + case @app_env do + "prod" -> + HTTPoison.post!(@feed_channel_url, Poison.encode!(%{text: text})) + _ -> + nil + end + end) + end +end diff --git a/lib/plausible/stats/countries.ex b/lib/plausible/stats/countries.ex new file mode 100644 index 000000000..5d58044a2 --- /dev/null +++ b/lib/plausible/stats/countries.ex @@ -0,0 +1,253 @@ +defmodule Plausible.Stats.CountryName do + @country_codes_to_names %{ + "AF" => "Afghanistan", + "AX" => "Aland Islands", + "AL" => "Albania", + "DZ" => "Algeria", + "AS" => "American Samoa", + "AD" => "Andorra", + "AO" => "Angola", + "AI" => "Anguilla", + "AQ" => "Antarctica", + "AG" => "Antigua And Barbuda", + "AR" => "Argentina", + "AM" => "Armenia", + "AW" => "Aruba", + "AU" => "Australia", + "AT" => "Austria", + "AZ" => "Azerbaijan", + "BS" => "Bahamas", + "BH" => "Bahrain", + "BD" => "Bangladesh", + "BB" => "Barbados", + "BY" => "Belarus", + "BE" => "Belgium", + "BZ" => "Belize", + "BJ" => "Benin", + "BM" => "Bermuda", + "BT" => "Bhutan", + "BO" => "Bolivia", + "BA" => "Bosnia And Herzegovina", + "BW" => "Botswana", + "BV" => "Bouvet Island", + "BR" => "Brazil", + "IO" => "British Indian Ocean Territory", + "BN" => "Brunei Darussalam", + "BG" => "Bulgaria", + "BF" => "Burkina Faso", + "BI" => "Burundi", + "KH" => "Cambodia", + "CM" => "Cameroon", + "CA" => "Canada", + "CV" => "Cape Verde", + "KY" => "Cayman Islands", + "CF" => "Central African Republic", + "TD" => "Chad", + "CL" => "Chile", + "CN" => "China", + "CX" => "Christmas Island", + "CC" => "Cocos (Keeling) Islands", + "CO" => "Colombia", + "KM" => "Comoros", + "CG" => "Congo", + "CD" => "Congo, Democratic Republic", + "CK" => "Cook Islands", + "CR" => "Costa Rica", + "CI" => "Cote D\"Ivoire", + "HR" => "Croatia", + "CU" => "Cuba", + "CY" => "Cyprus", + "CZ" => "Czech Republic", + "DK" => "Denmark", + "DJ" => "Djibouti", + "DM" => "Dominica", + "DO" => "Dominican Republic", + "EC" => "Ecuador", + "EG" => "Egypt", + "SV" => "El Salvador", + "GQ" => "Equatorial Guinea", + "ER" => "Eritrea", + "EE" => "Estonia", + "ET" => "Ethiopia", + "FK" => "Falkland Islands (Malvinas)", + "FO" => "Faroe Islands", + "FJ" => "Fiji", + "FI" => "Finland", + "FR" => "France", + "GF" => "French Guiana", + "PF" => "French Polynesia", + "TF" => "French Southern Territories", + "GA" => "Gabon", + "GM" => "Gambia", + "GE" => "Georgia", + "DE" => "Germany", + "GH" => "Ghana", + "GI" => "Gibraltar", + "GR" => "Greece", + "GL" => "Greenland", + "GD" => "Grenada", + "GP" => "Guadeloupe", + "GU" => "Guam", + "GT" => "Guatemala", + "GG" => "Guernsey", + "GN" => "Guinea", + "GW" => "Guinea-Bissau", + "GY" => "Guyana", + "HT" => "Haiti", + "HM" => "Heard Island & Mcdonald Islands", + "VA" => "Holy See (Vatican City State)", + "HN" => "Honduras", + "HK" => "Hong Kong", + "HU" => "Hungary", + "IS" => "Iceland", + "IN" => "India", + "ID" => "Indonesia", + "IR" => "Iran, Islamic Republic Of", + "IQ" => "Iraq", + "IE" => "Ireland", + "IM" => "Isle Of Man", + "IL" => "Israel", + "IT" => "Italy", + "JM" => "Jamaica", + "JP" => "Japan", + "JE" => "Jersey", + "JO" => "Jordan", + "KZ" => "Kazakhstan", + "KE" => "Kenya", + "KI" => "Kiribati", + "KR" => "Korea", + "KW" => "Kuwait", + "KG" => "Kyrgyzstan", + "LA" => "Lao People\"s Democratic Republic", + "LV" => "Latvia", + "LB" => "Lebanon", + "LS" => "Lesotho", + "LR" => "Liberia", + "LY" => "Libyan Arab Jamahiriya", + "LI" => "Liechtenstein", + "LT" => "Lithuania", + "LU" => "Luxembourg", + "MO" => "Macao", + "MK" => "Macedonia", + "MG" => "Madagascar", + "MW" => "Malawi", + "MY" => "Malaysia", + "MV" => "Maldives", + "ML" => "Mali", + "MT" => "Malta", + "MH" => "Marshall Islands", + "MQ" => "Martinique", + "MR" => "Mauritania", + "MU" => "Mauritius", + "YT" => "Mayotte", + "MX" => "Mexico", + "FM" => "Micronesia, Federated States Of", + "MD" => "Moldova", + "MC" => "Monaco", + "MN" => "Mongolia", + "ME" => "Montenegro", + "MS" => "Montserrat", + "MA" => "Morocco", + "MZ" => "Mozambique", + "MM" => "Myanmar", + "NA" => "Namibia", + "NR" => "Nauru", + "NP" => "Nepal", + "NL" => "Netherlands", + "AN" => "Netherlands Antilles", + "NC" => "New Caledonia", + "NZ" => "New Zealand", + "NI" => "Nicaragua", + "NE" => "Niger", + "NG" => "Nigeria", + "NU" => "Niue", + "NF" => "Norfolk Island", + "MP" => "Northern Mariana Islands", + "NO" => "Norway", + "OM" => "Oman", + "PK" => "Pakistan", + "PW" => "Palau", + "PS" => "Palestinian Territory, Occupied", + "PA" => "Panama", + "PG" => "Papua New Guinea", + "PY" => "Paraguay", + "PE" => "Peru", + "PH" => "Philippines", + "PN" => "Pitcairn", + "PL" => "Poland", + "PT" => "Portugal", + "PR" => "Puerto Rico", + "QA" => "Qatar", + "RE" => "Reunion", + "RO" => "Romania", + "RU" => "Russian Federation", + "RW" => "Rwanda", + "BL" => "Saint Barthelemy", + "SH" => "Saint Helena", + "KN" => "Saint Kitts And Nevis", + "LC" => "Saint Lucia", + "MF" => "Saint Martin", + "PM" => "Saint Pierre And Miquelon", + "VC" => "Saint Vincent And Grenadines", + "WS" => "Samoa", + "SM" => "San Marino", + "ST" => "Sao Tome And Principe", + "SA" => "Saudi Arabia", + "SN" => "Senegal", + "RS" => "Serbia", + "SC" => "Seychelles", + "SL" => "Sierra Leone", + "SG" => "Singapore", + "SK" => "Slovakia", + "SI" => "Slovenia", + "SB" => "Solomon Islands", + "SO" => "Somalia", + "ZA" => "South Africa", + "GS" => "South Georgia And Sandwich Isl.", + "ES" => "Spain", + "LK" => "Sri Lanka", + "SD" => "Sudan", + "SR" => "Suriname", + "SJ" => "Svalbard And Jan Mayen", + "SZ" => "Swaziland", + "SE" => "Sweden", + "CH" => "Switzerland", + "SY" => "Syrian Arab Republic", + "TW" => "Taiwan", + "TJ" => "Tajikistan", + "TZ" => "Tanzania", + "TH" => "Thailand", + "TL" => "Timor-Leste", + "TG" => "Togo", + "TK" => "Tokelau", + "TO" => "Tonga", + "TT" => "Trinidad And Tobago", + "TN" => "Tunisia", + "TR" => "Turkey", + "TM" => "Turkmenistan", + "TC" => "Turks And Caicos Islands", + "TV" => "Tuvalu", + "UG" => "Uganda", + "UA" => "Ukraine", + "AE" => "United Arab Emirates", + "GB" => "United Kingdom", + "US" => "United States", + "UM" => "United States Outlying Islands", + "UY" => "Uruguay", + "UZ" => "Uzbekistan", + "VU" => "Vanuatu", + "VE" => "Venezuela", + "VN" => "Viet Nam", + "VG" => "Virgin Islands, British", + "VI" => "Virgin Islands, U.S.", + "WF" => "Wallis And Futuna", + "EH" => "Western Sahara", + "YE" => "Yemen", + "ZM" => "Zambia", + "ZW" => "Zimbabwe" + } + + def from_iso3166(code) do + Map.get(@country_codes_to_names, code, code) + end +end diff --git a/lib/plausible/stats/query.ex b/lib/plausible/stats/query.ex new file mode 100644 index 000000000..27be4f640 --- /dev/null +++ b/lib/plausible/stats/query.ex @@ -0,0 +1,112 @@ +defmodule Plausible.Stats.Query do + defstruct [date_range: nil, step_type: nil, period: nil, steps: nil] + + def new(attrs) do + attrs + |> Enum.into(%{}) + |> Map.put(:__struct__, __MODULE__) + end + + def month(date) do + %__MODULE__{ + date_range: Date.range(Timex.beginning_of_month(date), Timex.end_of_month(date)) + } + end + + def day(date) do + %__MODULE__{ + date_range: Date.range(date, date) + } + end + + def shift_back(%__MODULE__{period: "day"} = query) do + new_date = query.date_range.first |> Timex.shift(days: -1) + Map.put(query, :date_range, Date.range(new_date, new_date)) + end + + def shift_back(query) do + diff = Timex.diff(query.date_range.first, query.date_range.last, :days) + new_first = query.date_range.first |> Timex.shift(days: diff) + new_last = query.date_range.last |> Timex.shift(days: diff) + Map.put(query, :date_range, Date.range(new_first, new_last)) + end + + def from(_tz, %{"period" => "day", "date" => date}) do + date = Date.from_iso8601!(date) + + %__MODULE__{ + period: "day", + date_range: Date.range(date, date), + step_type: "hour" + } + end + + def from(_tz, %{"period" => "month", "date" => month_start}) do + start_date = Date.from_iso8601!(month_start) |> Timex.beginning_of_month + end_date = Timex.end_of_month(start_date) + + %__MODULE__{ + period: "month", + date_range: Date.range(start_date, end_date), + step_type: "date", + steps: Timex.diff(start_date, end_date, :days) + } + end + + def from(tz, %{"period" => "month"}) do + start_date = today(tz) |> Timex.beginning_of_month + end_date = Timex.end_of_month(start_date) + + %__MODULE__{ + period: "month", + date_range: Date.range(start_date, end_date), + step_type: "date", + steps: Timex.diff(start_date, end_date, :days) + } + end + + def from(tz, %{"period" => "3mo"}) do + start_date = Timex.shift(today(tz), months: -2) + |> Timex.beginning_of_month() + + %__MODULE__{ + period: "3mo", + date_range: Date.range(start_date, today(tz)), + step_type: "month", + steps: 3 + } + end + + def from(tz, %{"period" => "6mo"}) do + start_date = Timex.shift(today(tz), months: -5) + |> Timex.beginning_of_month() + + %__MODULE__{ + period: "6mo", + date_range: Date.range(start_date, today(tz)), + step_type: "month", + steps: 6 + } + end + + def from(_tz, %{"period" => "custom", "from" => from, "to" => to}) do + start_date = Date.from_iso8601!(from) + end_date = Date.from_iso8601!(to) + date_range = Date.range(start_date, end_date) + + %__MODULE__{ + period: "custom", + date_range: date_range, + step_type: "date" + } + end + + def from(tz, _) do + __MODULE__.from(tz, %{"period" => "6mo"}) + end + + defp today(tz) do + Timex.now(tz) |> Timex.to_date + end +end + diff --git a/lib/plausible/stats/stats.ex b/lib/plausible/stats/stats.ex new file mode 100644 index 000000000..775398e17 --- /dev/null +++ b/lib/plausible/stats/stats.ex @@ -0,0 +1,207 @@ +defmodule Plausible.Stats do + use Plausible.Repo + alias Plausible.Stats.Query + + def compare_pageviews_and_visitors(site, query, {pageviews, visitors}) do + query = Query.shift_back(query) + {old_pageviews, old_visitors} = pageviews_and_visitors(site, query) + if old_visitors > 0 do + { + round((pageviews - old_pageviews) / old_pageviews * 100), + round((visitors - old_visitors) / old_visitors * 100), + } + else + {nil, nil} + end + end + + def calculate_plot(site, %Query{step_type: "month"} = query) do + steps = Enum.map((query.steps - 1)..0, fn shift -> + Timex.now(site.timezone) + |> Timex.beginning_of_month + |> Timex.shift(months: -shift) + |> DateTime.to_date + end) + + groups = Repo.all( + from p in base_query(site, query), + group_by: 1, + order_by: 1, + select: {fragment("date_trunc('month', ? at time zone 'utc' at time zone ?)", p.inserted_at, ^site.timezone), count(p.user_id, :distinct)} + ) |> Enum.into(%{}) + |> transform_keys(fn dt -> NaiveDateTime.to_date(dt) end) + + plot = Enum.map(steps, fn step -> groups[step] || 0 end) + labels = Enum.map(steps, fn step -> Timex.format!(step, "{ISOdate}") end) + present_index = Enum.find_index(steps, fn step -> step == Timex.now(site.timezone) |> Timex.to_date |> Timex.beginning_of_month end) + + {plot, labels, present_index} + end + + def calculate_plot(site, %Query{step_type: "date"} = query) do + steps = Enum.into(query.date_range, []) + + groups = Repo.all( + from p in base_query(site, query), + group_by: 1, + order_by: 1, + select: {fragment("date_trunc('day', ? at time zone 'utc' at time zone ?)", p.inserted_at, ^site.timezone), count(p.user_id, :distinct)} + ) |> Enum.into(%{}) + |> transform_keys(fn dt -> NaiveDateTime.to_date(dt) end) + + plot = Enum.map(steps, fn step -> groups[step] || 0 end) + labels = Enum.map(steps, fn step -> Timex.format!(step, "{ISOdate}") end) + present_index = Enum.find_index(steps, fn step -> step == Timex.now(site.timezone) |> Timex.to_date end) + + {plot, labels, present_index} + end + + def calculate_plot(site, %Query{step_type: "hour"} = query) do + {:ok, beginning_of_day} = NaiveDateTime.new(query.date_range.first, ~T[00:00:00]) + + steps = Enum.map(0..23, fn shift -> + beginning_of_day + |> Timex.shift(hours: shift) + |> truncate_to_hour + |> NaiveDateTime.truncate(:second) + end) + + groups = Repo.all( + from p in base_query(site, query), + group_by: 1, + order_by: 1, + select: {fragment("date_trunc('hour', ? at time zone 'utc' at time zone ?)", p.inserted_at, ^site.timezone), count(p.user_id, :distinct)} + ) + |> Enum.into(%{}) + |> transform_keys(fn dt -> NaiveDateTime.truncate(dt, :second) end) + + plot = Enum.map(steps, fn step -> groups[step] || 0 end) + labels = Enum.map(steps, fn step -> NaiveDateTime.to_iso8601(step) end) + present_index = Enum.find_index(steps, fn step -> step == Timex.now(site.timezone) |> truncate_to_hour |> NaiveDateTime.truncate(:second) end) + {plot, labels, present_index} + end + + def pageviews_and_visitors(site, query) do + Repo.one(from( + p in base_query(site, query), + select: {count(p.id), count(p.user_id, :distinct)} + )) + end + + def total_pageviews(site, query) do + Repo.aggregate(base_query(site, query), :count, :id) + end + + def unique_visitors(site, query) do + Repo.one(from( + p in base_query(site, query), + select: count(p.user_id, :distinct) + )) + end + + def top_referrers(site, query, limit \\ 5) do + Repo.all(from p in base_query(site, query), + select: {p.referrer_source, count(p.referrer_source)}, + group_by: p.referrer_source, + where: p.new_visitor == true and not is_nil(p.referrer_source), + order_by: [desc: 2], + limit: ^limit + ) + end + + def visitors_from_referrer(site, query, referrer) do + Repo.one(from p in base_query(site, query), + select: count(p), + where: p.new_visitor == true and p.referrer_source == ^referrer + ) + end + + def referrer_drilldown(site, query, referrer) do + Repo.all(from p in base_query(site, query), + select: {p.referrer, count(p)}, + group_by: p.referrer, + where: p.new_visitor == true and p.referrer_source == ^referrer, + order_by: [desc: 2], + limit: 100 + ) + end + + def top_pages(site, query, limit \\ 5) do + Repo.all(from p in base_query(site, query), + select: {p.pathname, count(p.pathname)}, + group_by: p.pathname, + order_by: [desc: count(p.pathname)], + limit: ^limit + ) + end + + @available_screen_sizes ["Desktop", "Laptop", "Tablet", "Mobile"] + + def top_screen_sizes(site, query) do + Repo.all(from p in base_query(site, query), + select: {p.screen_size, count(p.screen_size)}, + group_by: p.screen_size, + where: p.new_visitor == true and not is_nil(p.screen_size) + ) |> Enum.sort(fn {screen_size1, _}, {screen_size2, _} -> + index1 = Enum.find_index(@available_screen_sizes, fn s -> s == screen_size1 end) + index2 = Enum.find_index(@available_screen_sizes, fn s -> s == screen_size2 end) + index2 > index1 + end) + end + + def countries(site, query, limit \\ 5) do + Repo.all(from p in base_query(site, query), + select: {p.country_code, count(p.country_code)}, + group_by: p.country_code, + where: p.new_visitor == true and not is_nil(p.country_code), + order_by: [desc: count(p.country_code)], + limit: ^limit + ) |> Enum.map(fn {country_code, count} -> + {Plausible.Stats.CountryName.from_iso3166(country_code), count} + end) + end + + def browsers(site, query, limit \\ 5) do + Repo.all(from p in base_query(site, query), + select: {p.browser, count(p.browser)}, + group_by: p.browser, + where: p.new_visitor == true and not is_nil(p.browser), + order_by: [desc: count(p.browser)], + limit: ^limit + ) + end + + def operating_systems(site, query, limit \\ 5) do + Repo.all(from p in base_query(site, query), + select: {p.operating_system, count(p.operating_system)}, + group_by: p.operating_system, + where: p.new_visitor == true and not is_nil(p.operating_system), + order_by: [desc: count(p.operating_system)], + limit: ^limit + ) + end + + defp base_query(site, query) do + {:ok, first} = NaiveDateTime.new(query.date_range.first, ~T[00:00:00]) + first_datetime = Timex.to_datetime(first, site.timezone) + + {:ok, last} = NaiveDateTime.new(query.date_range.last |> Timex.shift(days: 1), ~T[00:00:00]) + last_datetime = Timex.to_datetime(last, site.timezone) + + from(p in Plausible.Pageview, + where: p.hostname == ^site.domain, + where: p.inserted_at >= ^first_datetime and p.inserted_at < ^last_datetime + ) + end + + defp transform_keys(map, fun) do + for {key, val} <- map, into: %{} do + {fun.(key), val} + end + end + + defp truncate_to_hour(datetime) do + {:ok, datetime} = NaiveDateTime.new(datetime.year, datetime.month, datetime.day, datetime.hour, 0, 0, 0) + datetime + end +end diff --git a/lib/plausible/timezones.ex b/lib/plausible/timezones.ex new file mode 100644 index 000000000..e49cba3ae --- /dev/null +++ b/lib/plausible/timezones.ex @@ -0,0 +1,92 @@ +defmodule Plausible.Timezones do + @moduledoc "https://stackoverflow.com/a/52265733" + + @options [ + [key: "(GMT-12:00) International Date Line West", value: "Etc/GMT+12", offset: "720"], + [key: "(GMT-11:00) Midway Island, Samoa",value: "Pacific/Midway", offset: "660"], + [key: "(GMT-10:00) Hawaii",value: "Pacific/Honolulu", offset: "600"], + [key: "(GMT-09:00) Alaska",value: "US/Alaska", offset: "540"], + [key: "(GMT-08:00) Pacific Time (US & Canada)",value: "America/Los_Angeles", offset: "480"], + [key: "(GMT-08:00) Tijuana, Baja California",value: "America/Tijuana"], + [key: "(GMT-07:00) Arizona",value: "US/Arizona"], + [key: "(GMT-07:00) Chihuahua, La Paz, Mazatlan",value: "America/Chihuahua"], + [key: "(GMT-07:00) Mountain Time (US & Canada)",value: "US/Mountain", offset: "420"], + [key: "(GMT-06:00) Central America",value: "America/Managua"], + [key: "(GMT-06:00) Central Time (US & Canada)",value: "US/Central", offset: "360"], + [key: "(GMT-06:00) Guadalajara, Mexico City, Monterrey",value: "America/Mexico_City"], + [key: "(GMT-06:00) Saskatchewan",value: "Canada/Saskatchewan"], + [key: "(GMT-05:00) Bogota, Lima, Quito, Rio Branco",value: "America/Bogota"], + [key: "(GMT-05:00) Eastern Time (US & Canada)",value: "US/Eastern", offset: "300"], + [key: "(GMT-05:00) Indiana (East)",value: "US/East-Indiana"], + [key: "(GMT-04:00) Atlantic Time (Canada)",value: "Canada/Atlantic", offset: "240"], + [key: "(GMT-04:00) Caracas, La Paz",value: "America/Caracas"], + [key: "(GMT-04:00) Manaus",value: "America/Manaus"], + [key: "(GMT-04:00) Santiago",value: "America/Santiago"], + [key: "(GMT-03:30) Newfoundland",value: "Canada/Newfoundland"], + [key: "(GMT-03:00) Brasilia",value: "America/Sao_Paulo", offset: "180"], + [key: "(GMT-03:00) Buenos Aires, Georgetown",value: "America/Argentina/Buenos_Aires"], + [key: "(GMT-03:00) Greenland",value: "America/Godthab"], + [key: "(GMT-03:00) Montevideo",value: "America/Montevideo"], + [key: "(GMT-02:00) Mid-Atlantic",value: "America/Noronha", offset: "120"], + [key: "(GMT-01:00) Cape Verde Is.",value: "Atlantic/Cape_Verde", offset: "60"], + [key: "(GMT-01:00) Azores",value: "Atlantic/Azores"], + [key: "(GMT+00:00) Casablanca, Monrovia, Reykjavik",value: "Africa/Casablanca"], + [key: "(GMT+00:00) Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London",value: "Etc/Greenwich", offset: "0"], + [key: "(GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",value: "Europe/Amsterdam", offset: "-60"], + [key: "(GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague",value: "Europe/Belgrade"], + [key: "(GMT+01:00) Brussels, Copenhagen, Madrid, Paris",value: "Europe/Brussels"], + [key: "(GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb",value: "Europe/Sarajevo"], + [key: "(GMT+01:00) West Central Africa",value: "Africa/Lagos"], + [key: "(GMT+02:00) Amman",value: "Asia/Amman"], + [key: "(GMT+02:00) Athens, Bucharest, Istanbul",value: "Europe/Athens"], + [key: "(GMT+02:00) Beirut",value: "Asia/Beirut"], + [key: "(GMT+02:00) Cairo",value: "Africa/Cairo"], + [key: "(GMT+02:00) Harare, Pretoria",value: "Africa/Harare"], + [key: "(GMT+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius",value: "Europe/Helsinki", offset: "-120"], + [key: "(GMT+02:00) Jerusalem",value: "Asia/Jerusalem"], + [key: "(GMT+02:00) Minsk",value: "Europe/Minsk"], + [key: "(GMT+02:00) Windhoek",value: "Africa/Windhoek"], + [key: "(GMT+03:00) Kuwait, Riyadh, Baghdad",value: "Asia/Kuwait"], + [key: "(GMT+03:00) Moscow, St. Petersburg, Volgograd",value: "Europe/Moscow", offset: "-180"], + [key: "(GMT+03:00) Nairobi",value: "Africa/Nairobi"], + [key: "(GMT+03:00) Tbilisi",value: "Asia/Tbilisi"], + [key: "(GMT+03:30) Tehran",value: "Asia/Tehran", offset: "-210"], + [key: "(GMT+04:00) Abu Dhabi, Muscat",value: "Asia/Muscat", offset: "-240"], + [key: "(GMT+04:00) Baku",value: "Asia/Baku"], + [key: "(GMT+04:00) Yerevan",value: "Asia/Yerevan"], + [key: "(GMT+04:30) Kabul",value: "Asia/Kabul", offset: "-270"], + [key: "(GMT+05:00) Yekaterinburg",value: "Asia/Yekaterinburg"], + [key: "(GMT+05:00) Islamabad, Karachi, Tashkent",value: "Asia/Karachi", offset: "-300"], + [key: "(GMT+05:30) Chennai, Kolkata, Mumbai, New Delhi",value: "Asia/Calcutta", offset: "-330"], + [key: "(GMT+05:30) Sri Jayawardenapura",value: "Asia/Calcutta"], + [key: "(GMT+05:45) Kathmandu",value: "Asia/Katmandu", offset: "-345"], + [key: "(GMT+06:00) Almaty, Novosibirsk",value: "Asia/Almaty", offset: "-360"], + [key: "(GMT+06:00) Astana, Dhaka",value: "Asia/Dhaka"], + [key: "(GMT+06:30) Yangon (Rangoon)",value: "Asia/Rangoon", offset: "-390"], + [key: "(GMT+07:00) Bangkok, Hanoi, Jakarta",value: "Asia/Bangkok", offset: "-420"], + [key: "(GMT+07:00) Krasnoyarsk",value: "Asia/Krasnoyarsk"], + [key: "(GMT+08:00) Beijing, Chongqing, Hong Kong, Urumqi",value: "Asia/Hong_Kong", offset: "-480"], + [key: "(GMT+08:00) Kuala Lumpur, Singapore",value: "Asia/Kuala_Lumpur"], + [key: "(GMT+08:00) Irkutsk, Ulaan Bataar",value: "Asia/Irkutsk"], + [key: "(GMT+08:00) Perth",value: "Australia/Perth"], + [key: "(GMT+08:00) Taipei",value: "Asia/Taipei"], + [key: "(GMT+09:00) Osaka, Sapporo, Tokyo",value: "Asia/Tokyo", offset: "-540"], + [key: "(GMT+09:00) Seoul",value: "Asia/Seoul"], + [key: "(GMT+09:00) Yakutsk",value: "Asia/Yakutsk"], + [key: "(GMT+09:30) Adelaide",value: "Australia/Adelaide", offset: "-570"], + [key: "(GMT+09:30) Darwin",value: "Australia/Darwin"], + [key: "(GMT+10:00) Brisbane",value: "Australia/Brisbane", offset: "-600"], + [key: "(GMT+10:00) Canberra, Melbourne, Sydney",value: "Australia/Canberra"], + [key: "(GMT+10:00) Hobart",value: "Australia/Hobart"], + [key: "(GMT+10:00) Guam, Port Moresby",value: "Pacific/Guam"], + [key: "(GMT+10:00) Vladivostok",value: "Asia/Vladivostok"], + [key: "(GMT+11:00) Magadan, Solomon Is., New Caledonia",value: "Asia/Magadan", offset: "-660"], + [key: "(GMT+12:00) Auckland, Wellington",value: "Pacific/Auckland", offset: "-720"], + [key: "(GMT+12:00) Fiji, Kamchatka, Marshall Is.",value: "Pacific/Fiji"], + [key: "(GMT+13:00) Nuku'alofa",value: "Pacific/Tongatapu", offset: "-780"], + ] + + def options() do + @options + end +end diff --git a/lib/plausible/tracking.ex b/lib/plausible/tracking.ex new file mode 100644 index 000000000..1adc99ceb --- /dev/null +++ b/lib/plausible/tracking.ex @@ -0,0 +1,51 @@ +defmodule Plausible.Tracking do + @api_host "https://api.amplitude.com" + + def event(conn, event, properties \\ %{}) do + Task.start(fn -> + track(Mix.env(), %{ + event_type: event, + user_id: extract_user_id(conn), + device_id: extract_device_id(conn), + event_properties: properties, + time: Timex.now() |> DateTime.to_unix + }) + end) + end + + def identify(conn, user_id, props \\ %{}) do + Task.start(fn -> + track(Mix.env(), %{ + event_type: "$identify", + user_id: user_id, + device_id: extract_device_id(conn), + user_properties: props, + time: Timex.now() |> DateTime.to_unix + }) + end) + end + + defp track(:test, _params) do + # /dev/null + end + + defp track(:dev, _params) do + # /dev/null + end + + defp track(_, params) do + HTTPoison.get!(@api_host <> "/httpapi", [], params: [api_key: api_key(), event: Jason.encode!(params)]) + end + + defp extract_user_id(conn) do + if conn.assigns[:current_user] do + conn.assigns[:current_user].id + end + end + + defp extract_device_id(conn) do + Plug.Conn.get_session(conn, :device_id) + end + + defp api_key, do: Keyword.fetch!(Application.get_env(:plausible, :amplitude), :api_key) +end diff --git a/lib/plausible_web.ex b/lib/plausible_web.ex new file mode 100644 index 000000000..776775ae6 --- /dev/null +++ b/lib/plausible_web.ex @@ -0,0 +1,68 @@ +defmodule PlausibleWeb do + @moduledoc """ + The entrypoint for defining your web interface, such + as controllers, views, channels and so on. + + This can be used in your application as: + + use PlausibleWeb, :controller + use PlausibleWeb, :view + + The definitions below will be executed for every view, + controller, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. Instead, define any helper function in modules + and import those modules here. + """ + + def controller do + quote do + use Phoenix.Controller, namespace: PlausibleWeb + + import Plug.Conn + import PlausibleWeb.ControllerHelpers + alias PlausibleWeb.Router.Helpers, as: Routes + end + end + + def view do + quote do + use Phoenix.View, + root: "lib/plausible_web/templates", + namespace: PlausibleWeb + + # Import convenience functions from controllers + import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] + + # Use all HTML functionality (forms, tags, etc) + use Phoenix.HTML + + import PlausibleWeb.ErrorHelpers + import PhoenixActiveLink + alias PlausibleWeb.Router.Helpers, as: Routes + end + end + + def router do + quote do + use Phoenix.Router + import Plug.Conn + import Phoenix.Controller + end + end + + def channel do + quote do + use Phoenix.Channel + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end diff --git a/lib/plausible_web/auth_plug.ex b/lib/plausible_web/auth_plug.ex new file mode 100644 index 000000000..7885b13ea --- /dev/null +++ b/lib/plausible_web/auth_plug.ex @@ -0,0 +1,20 @@ +defmodule PlausibleWeb.AuthPlug do + import Plug.Conn + + def init(options) do + options + end + + def call(conn, _opts) do + case get_session(conn, :current_user_id) do + nil -> conn + id -> + user = Plausible.Auth.find_user_by(id: id) + if user do + assign(conn, :current_user, user) + else + conn + end + end + end +end diff --git a/lib/plausible_web/channels/user_socket.ex b/lib/plausible_web/channels/user_socket.ex new file mode 100644 index 000000000..2ec5fcba1 --- /dev/null +++ b/lib/plausible_web/channels/user_socket.ex @@ -0,0 +1,33 @@ +defmodule PlausibleWeb.UserSocket do + use Phoenix.Socket + + ## Channels + # channel "room:*", PlausibleWeb.RoomChannel + + # Socket params are passed from the client and can + # be used to verify and authenticate a user. After + # verification, you can put default assigns into + # the socket that will be set for all channels, ie + # + # {:ok, assign(socket, :user_id, verified_user_id)} + # + # To deny connection, return `:error`. + # + # See `Phoenix.Token` documentation for examples in + # performing token verification on connect. + def connect(_params, socket, _connect_info) do + {:ok, socket} + end + + # Socket id's are topics that allow you to identify all sockets for a given user: + # + # def id(socket), do: "user_socket:#{socket.assigns.user_id}" + # + # Would allow you to broadcast a "disconnect" event and terminate + # all active sockets and channels for a given user: + # + # PlausibleWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) + # + # Returning `nil` makes this socket anonymous. + def id(_socket), do: nil +end diff --git a/lib/plausible_web/controllers/api/external_controller.ex b/lib/plausible_web/controllers/api/external_controller.ex new file mode 100644 index 000000000..ce9004762 --- /dev/null +++ b/lib/plausible_web/controllers/api/external_controller.ex @@ -0,0 +1,139 @@ +defmodule PlausibleWeb.Api.ExternalController do + use PlausibleWeb, :controller + require Logger + + @blacklist_user_ids [ + "e8150466-7ddb-4771-bcf5-7c58f232e8a6" + ] + + def page(conn, _params) do + params = parse_body(conn) + + case create_pageview(conn, params) do + {:ok, _pageview} -> + conn |> send_resp(202, "") + {:error, changeset} -> + request = Sentry.Plug.build_request_interface_data(conn, []) + Sentry.capture_message("Error processing pageview", extra: %{errors: inspect(changeset.errors), params: params, request: request}) + Logger.error("Error processing pageview: #{inspect(changeset)}") + conn |> send_resp(400, "") + end + end + + def error(conn, _params) do + request = Sentry.Plug.build_request_interface_data(conn, []) + Sentry.capture_message("JS snippet error", request: request) + send_resp(conn, 200, "") + end + + defp create_pageview(conn, params) do + uri = URI.parse(params["url"]) + country_code = Plug.Conn.get_req_header(conn, "cf-ipcountry") |> List.first + user_agent = Plug.Conn.get_req_header(conn, "user-agent") |> List.first + if UAInspector.bot?(user_agent) || params["uid"] in @blacklist_user_ids do + {:ok, nil} + else + ua = if user_agent do + UAInspector.Parser.parse(user_agent) + end + + ref = params["referrer"] + ref = if ref && strip_www(URI.parse(ref).host) !== strip_www(uri.host) && URI.parse(ref).host !== "localhost" do + RefInspector.parse(ref) + end + + pageview_attrs = %{ + hostname: strip_www(uri.host), + pathname: uri.path, + user_agent: user_agent, + new_visitor: params["new_visitor"], + screen_width: params["screen_width"], + country_code: country_code, + user_id: params["uid"], + operating_system: ua && os_name(ua), + browser: ua && browser_name(ua), + raw_referrer: params["referrer"], + referrer_source: ref && referrer_source(uri, ref), + referrer: ref && clean_referrer(params["referrer"]), + screen_size: calculate_screen_size(params["screen_width"]) + } + + Plausible.Pageview.changeset(%Plausible.Pageview{}, pageview_attrs) + |> Plausible.Repo.insert + end + end + + defp calculate_screen_size(width) when width < 576, do: "Mobile" + defp calculate_screen_size(width) when width < 992, do: "Tablet" + defp calculate_screen_size(width) when width < 1440, do: "Laptop" + defp calculate_screen_size(width) when width >= 1440, do: "Desktop" + defp calculate_screen_size(_) , do: nil + + defp clean_referrer(referrer) do + uri = if referrer do + URI.parse(referrer) + end + + if uri && uri.scheme in ["http", "https"] do + host = String.replace_prefix(uri.host, "www.", "") + host <> (uri.path || "") + end + end + + defp parse_body(conn) do + {:ok, body, _conn} = Plug.Conn.read_body(conn) + Jason.decode!(body) + end + + defp strip_www(nil), do: nil + defp strip_www(hostname) do + String.replace_prefix(hostname, "www.", "") + end + + defp browser_name(ua) do + case ua.client do + %UAInspector.Result.Client{name: "Mobile Safari"} -> "Safari" + %UAInspector.Result.Client{name: "Chrome Mobile"} -> "Chrome" + %UAInspector.Result.Client{name: "Chrome Mobile iOS"} -> "Chrome" + %UAInspector.Result.Client{type: "mobile app"} -> "Mobile App" + :unknown -> nil + client -> client.name + end + end + + defp os_name(ua) do + case ua.os do + :unknown -> nil + os -> os.name + end + end + + defp referrer_source(uri, ref) do + case ref.source do + :unknown -> + query_param_source(uri) || clean_uri(ref.referer) + source -> + source + end + end + + defp clean_uri(uri) do + uri = URI.parse(String.trim(uri)) + if uri.scheme in ["http", "https"] do + String.replace_leading(uri.host, "www.", "") + end + end + + @source_query_params ["ref", "utm_source", "source"] + + defp query_param_source(uri) do + if uri && uri.query do + Enum.find_value(URI.query_decoder(uri.query), fn {key, val} -> + if Enum.member?(@source_query_params, key) do + val + end + end) + end + end + +end diff --git a/lib/plausible_web/controllers/api/internal_controller.ex b/lib/plausible_web/controllers/api/internal_controller.ex new file mode 100644 index 000000000..8f082a7ca --- /dev/null +++ b/lib/plausible_web/controllers/api/internal_controller.ex @@ -0,0 +1,17 @@ +defmodule PlausibleWeb.Api.InternalController do + use PlausibleWeb, :controller + use Plausible.Repo + + def domain_status(conn, %{"domain" => domain}) do + has_pageviews = Repo.exists?( + from p in Plausible.Pageview, + where: p.hostname == ^domain + ) + + if has_pageviews do + json(conn, "READY") + else + json(conn, "WAITING") + end + end +end diff --git a/lib/plausible_web/controllers/api/paddle_controller.ex b/lib/plausible_web/controllers/api/paddle_controller.ex new file mode 100644 index 000000000..665ec6893 --- /dev/null +++ b/lib/plausible_web/controllers/api/paddle_controller.ex @@ -0,0 +1,74 @@ +defmodule PlausibleWeb.Api.PaddleController do + use PlausibleWeb, :controller + use Plausible.Repo + require Logger + + plug :verify_signature + + def webhook(conn, %{"alert_name" => "subscription_created"} = params) do + Plausible.Billing.subscription_created(params) + |> webhook_response(conn, params) + end + + def webhook(conn, %{"alert_name" => "subscription_updated"} = params) do + Plausible.Billing.subscription_updated(params) + |> webhook_response(conn, params) + end + + def webhook(conn, %{"alert_name" => "subscription_cancelled"} = params) do + Plausible.Billing.subscription_cancelled(params) + |> webhook_response(conn, params) + end + + def webhook(conn, %{"alert_name" => "subscription_payment_succeeded"} = params) do + Plausible.Billing.subscription_payment_succeeded(params) + |> webhook_response(conn, params) + end + + def webhook(conn, _params) do + send_resp(conn, 404, "") |> halt + end + + @paddle_key File.read!("priv/paddle.pem") + + def verify_signature(conn, _opts) do + signature = Base.decode64!(conn.params["p_signature"]) + msg = Map.delete(conn.params, "p_signature") + |> Enum.map(fn {key, val} -> {key, "#{val}"} end) + |> List.keysort(0) + |> PhpSerializer.serialize() + + [key_entry] = :public_key.pem_decode(@paddle_key) + public_key = :public_key.pem_entry_decode(key_entry) + + if :public_key.verify(msg, :sha, signature, public_key) do + conn + else + send_resp(conn, 400, "") |> halt + end + end + + def verified_signature?(params) do + signature = Base.decode64!(params["p_signature"]) + msg = Map.delete(params, "p_signature") + |> Enum.map(fn {key, val} -> {key, "#{val}"} end) + |> List.keysort(0) + |> PhpSerializer.serialize() + + [key_entry] = :public_key.pem_decode(@paddle_key) + public_key = :public_key.pem_entry_decode(key_entry) + :public_key.verify(msg, :sha, signature, public_key) + end + + defp webhook_response({:ok, _}, conn, _params) do + json(conn, "") + end + + defp webhook_response({:error, changeset}, conn, params) do + request = Sentry.Plug.build_request_interface_data(conn, []) + Sentry.capture_message("Error processing Paddle webhook", extra: %{errors: inspect(changeset.errors), params: params, request: request}) + Logger.error("Error processing Paddle webhook: #{inspect(changeset)}") + + conn |> send_resp(400, "") |> halt + end +end diff --git a/lib/plausible_web/controllers/auth_controller.ex b/lib/plausible_web/controllers/auth_controller.ex new file mode 100644 index 000000000..96989db2c --- /dev/null +++ b/lib/plausible_web/controllers/auth_controller.ex @@ -0,0 +1,214 @@ +defmodule PlausibleWeb.AuthController do + use PlausibleWeb, :controller + use Plausible.Repo + alias Plausible.Auth + require Logger + + plug PlausibleWeb.RequireLoggedOutPlug when action in [:register_form, :register, :login_form, :login] + plug PlausibleWeb.RequireAccountPlug when action in [:user_settings, :save_settings, :delete_me, :password_form, :set_password] + + def register_form(conn, _params) do + changeset = Plausible.Auth.User.changeset(%Plausible.Auth.User{}) + Plausible.Tracking.event(conn, "Register: View Form") + render(conn, "register_form.html", changeset: changeset, layout: {PlausibleWeb.LayoutView, "focus.html"}) + end + + def register(conn, %{"user" => params}) do + user = Plausible.Auth.User.changeset(%Plausible.Auth.User{}, params) + + case Ecto.Changeset.apply_action(user, :insert) do + {:ok, user} -> + token = Auth.Token.sign_activation(user.name, user.email) + url = PlausibleWeb.Endpoint.clean_url() <> "/claim-activation?token=#{token}" + Logger.info(url) + email_template = PlausibleWeb.Email.activation_email(user, url) + Plausible.Mailer.deliver_now(email_template) + Plausible.Tracking.event(conn, "Register: Submit Form") + conn |> render("register_success.html", email: user.email, layout: {PlausibleWeb.LayoutView, "focus.html"}) + {:error, changeset} -> + render(conn, "register_form.html", changeset: changeset, layout: {PlausibleWeb.LayoutView, "focus.html"}) + end + end + + def claim_activation_link(conn, %{"token" => token}) do + case Auth.Token.verify_activation(token) do + {:ok, %{name: name, email: email}} -> + case Auth.create_user(name, email) do + {:ok, user} -> + Plausible.Tracking.event(conn, "Register: Activate Account") + Plausible.Tracking.identify(conn, user.id, %{name: user.name}) + conn + |> put_session(:current_user_id, user.id) + |> redirect(to: "/password") + {:error, changeset} -> + send_resp(conn, 400, inspect(changeset.errors)) + end + {:error, :expired} -> + render_error(conn, 401, "Your token has expired. Please request another activation link.") + {:error, _} -> + render_error(conn, 400, "Your token is invalid. Please request another activation link.") + end + end + + def password_reset_request_form(conn, _) do + render(conn, "password_reset_request_form.html", layout: {PlausibleWeb.LayoutView, "focus.html"}) + end + + def password_reset_request(conn, %{"email" => ""}) do + render(conn, "password_reset_request_form.html", error: "Please enter an email address", layout: {PlausibleWeb.LayoutView, "focus.html"}) + end + + def password_reset_request(conn, %{"email" => email}) do + user = Repo.get_by(Plausible.Auth.User, email: email) + + if user do + token = Auth.Token.sign_password_reset(email) + url = PlausibleWeb.Endpoint.clean_url() <> "/password/reset?token=#{token}" + Logger.debug("PASSWORD RESET LINK: " <> url) + email_template = PlausibleWeb.Email.password_reset_email(email, url) + Plausible.Mailer.deliver_now(email_template) + render(conn, "password_reset_request_success.html", email: email, layout: {PlausibleWeb.LayoutView, "focus.html"}) + else + render(conn, "password_reset_request_success.html", email: email, layout: {PlausibleWeb.LayoutView, "focus.html"}) + end + end + + def password_reset_form(conn, %{"token" => token}) do + case Auth.Token.verify_password_reset(token) do + {:ok, _} -> + render(conn, "password_reset_form.html", token: token, layout: {PlausibleWeb.LayoutView, "focus.html"}) + {:error, :expired} -> + render_error(conn, 401, "Your token has expired. Please request another password reset link.") + {:error, _} -> + render_error(conn, 401, "Your token is invalid. Please request another password reset link.") + end + end + + def password_reset(conn, %{"token" => token, "password" => pw}) do + case Auth.Token.verify_password_reset(token) do + {:ok, %{email: email}} -> + user = Repo.get_by(Auth.User, email: email) + changeset = Auth.User.set_password(user, pw) + case Repo.update(changeset) do + {:ok, _updated} -> + conn + |> put_flash(:login_title, "Password updated successfully") + |> put_flash(:login_instructions, "Please log in with your new credentials") + |> put_session(:current_user_id, nil) + |> redirect(to: "/login") + {:error, changeset} -> + render(conn, "password_reset_form.html", changeset: changeset, token: token, layout: {PlausibleWeb.LayoutView, "focus.html"}) + end + {:error, :expired} -> + render_error(conn, 401, "Your token has expired. Please request another password reset link.") + {:error, _} -> + render_error(conn, 401, "Your token is invalid. Please request another password reset link.") + end + end + + def login(conn, %{"email" => email, "password" => password}) do + alias Plausible.Auth.Password + + user = Repo.one( + from u in Plausible.Auth.User, + where: u.email == ^email + ) + + if user do + if Password.match?(password, user.password_hash || "") do + login_dest = get_session(conn, :login_dest) || "/" + + conn + |> put_session(:current_user_id, user.id) + |> put_session(:login_dest, nil) + |> redirect(to: login_dest) + else + conn |> render("login_form.html", error: "Wrong email or password. Please try again.", layout: {PlausibleWeb.LayoutView, "focus.html"}) + end + else + Password.dummy_calculation() + conn |> render("login_form.html", error: "Wrong email or password. Please try again.", layout: {PlausibleWeb.LayoutView, "focus.html"}) + end + end + + def login_form(conn, _params) do + render(conn, "login_form.html", layout: {PlausibleWeb.LayoutView, "focus.html"}) + end + + def password_form(conn, _params) do + render(conn, "password_form.html", layout: {PlausibleWeb.LayoutView, "focus.html"}, skip_plausible_tracking: true) + end + + def set_password(conn, %{"password" => pw}) do + changeset = Auth.User.set_password(conn.assigns[:current_user], pw) + + case Repo.update(changeset) do + {:ok, _user} -> + redirect(conn, to: "/sites/new") + {:error, changeset} -> + render(conn, "password_form.html", changeset: changeset, layout: {PlausibleWeb.LayoutView, "focus.html"}) + end + end + + def user_settings(conn, _params) do + changeset = Auth.User.changeset(conn.assigns[:current_user]) + subscription = Plausible.Billing.active_subscription_for(conn.assigns[:current_user].id) + render(conn, "user_settings.html", changeset: changeset, subscription: subscription) + end + + def save_settings(conn, %{"user" => user_params}) do + changes = Auth.User.changeset(conn.assigns[:current_user], user_params) + case Repo.update(changes) do + {:ok, _user} -> + conn + |> put_flash(:success, "Account settings saved succesfully") + |> redirect(to: "/settings") + {:error, changeset} -> + render(conn, "user_settings.html", changeset: changeset) + end + end + + def delete_me(conn, _params) do + user = conn.assigns[:current_user] |> Repo.preload(:sites) + + for site_membership <- user.site_memberships do + Repo.delete!(site_membership) + end + + for site <- user.sites do + Repo.delete!(site) + end + + Repo.delete!(user) + + conn + |> configure_session(drop: true) + |> redirect(to: "/") + end + + def logout(conn, _params) do + conn + |> configure_session(drop: true) + |> redirect(to: "/") + end + + def google_auth_callback(conn, %{"code" => code, "state" => site_id}) do + res = Plausible.Google.Api.fetch_access_token(code) + id_token = res["id_token"] + [_, body, _] = String.split(id_token, ".") + id = body |> Base.decode64!(padding: false) |> Jason.decode! + + Plausible.Site.GoogleAuth.changeset(%Plausible.Site.GoogleAuth{}, %{ + email: id["email"], + refresh_token: res["refresh_token"], + access_token: res["access_token"], + expires: NaiveDateTime.utc_now() |> NaiveDateTime.add(res["expires_in"]), + user_id: conn.assigns[:current_user].id, + site_id: site_id + }) |> Repo.insert! + + site = Repo.get(Plausible.Site, site_id) + + redirect(conn, to: "/#{site.domain}/settings#google-auth") + end +end diff --git a/lib/plausible_web/controllers/billing_controller.ex b/lib/plausible_web/controllers/billing_controller.ex new file mode 100644 index 000000000..dc8499194 --- /dev/null +++ b/lib/plausible_web/controllers/billing_controller.ex @@ -0,0 +1,47 @@ +defmodule PlausibleWeb.BillingController do + use PlausibleWeb, :controller + use Plausible.Repo + alias Plausible.Billing + require Logger + + plug PlausibleWeb.RequireAccountPlug + + def change_plan_form(conn, _params) do + subscription = Billing.active_subscription_for(conn.assigns[:current_user].id) + if subscription do + render(conn, "change_plan.html", subscription: subscription, layout: {PlausibleWeb.LayoutView, "focus.html"}) + else + redirect(conn, to: "/billing/upgrade") + end + end + + def change_plan(conn, %{"plan_name" => plan}) when plan in ["personal", "startup", "business"] do + new_plan = String.to_existing_atom(plan) + + case Billing.change_plan(conn.assigns[:current_user], new_plan) do + {:ok, _subscription} -> + conn + |> put_flash(:success, "Plan changed successfully") + |> redirect(to: "/settings") + {:error, e} -> + Sentry.capture_message("Error changing plans", extra: %{errors: inspect(e), new_plan: new_plan, user_id: conn.assigns[:current_user].id}) + conn + |> put_flash(:error, "Something went wrong. Please try again or contact support at uku@plausible.io") + |> redirect(to: "/settings") + end + end + + def upgrade(conn, _params) do + usage = Plausible.Billing.usage(conn.assigns[:current_user]) + trial_end_date = Plausible.Billing.trial_end_date(conn.assigns[:current_user]) + today = Timex.today() + + render(conn, "upgrade.html", usage: usage, trial_end_date: trial_end_date, today: today, layout: {PlausibleWeb.LayoutView, "focus.html"}) + end + + def success(conn, _params) do + conn + |> put_flash(:success, "Subscription created successfully") + |> redirect(to: "/") + end +end diff --git a/lib/plausible_web/controllers/helpers.ex b/lib/plausible_web/controllers/helpers.ex new file mode 100644 index 000000000..d8db7f88e --- /dev/null +++ b/lib/plausible_web/controllers/helpers.ex @@ -0,0 +1,18 @@ +defmodule PlausibleWeb.ControllerHelpers do + import Plug.Conn + import Phoenix.Controller + + def render_error(conn, status, message) do + conn + |> put_status(status) + |> put_view(PlausibleWeb.ErrorView) + |> render("#{status}.html", layout: false, message: message) + end + + def render_error(conn, status) do + conn + |> put_status(status) + |> put_view(PlausibleWeb.ErrorView) + |> render("#{status}.html", layout: false) + end +end diff --git a/lib/plausible_web/controllers/page_controller.ex b/lib/plausible_web/controllers/page_controller.ex new file mode 100644 index 000000000..1468ea429 --- /dev/null +++ b/lib/plausible_web/controllers/page_controller.ex @@ -0,0 +1,86 @@ +defmodule PlausibleWeb.PageController do + use PlausibleWeb, :controller + use Plausible.Repo + + @demo_referrers [ + {"indiehackers.com", 30}, + {"Twitter", 17}, + {"Google", 6}, + {"DuckDuckGo", 4}, + {"Bing", 2}, + ] + + @demo_countries [ + {"United Kingdom", 41}, + {"United States", 38}, + {"France", 13}, + {"India", 7}, + {"Netherlands", 6}, + ] + + def index(conn, _params) do + if conn.assigns[:current_user] do + Plausible.Tracking.event(conn, "Sites: View Page") + user = conn.assigns[:current_user] |> Repo.preload(:sites) + render(conn, "sites.html", sites: user.sites) + else + Plausible.Tracking.event(conn, "Landing: View Page") + render(conn, "index.html", demo_referrers: @demo_referrers, demo_countries: @demo_countries, landing_nav: true) + end + end + + defmodule Token do + use Joken.Config + end + + defp sign_token!(user) do + claims = %{ + id: user.id, + email: user.email, + name: user.name, + } + + signer = Joken.Signer.create("HS256", "4d1d2ae6-4595-4d0b-b98a-8ca5b1f2095a") + {:ok, token, _} = Token.generate_and_sign(claims, signer) + token + end + + def feedback(conn, _params) do + if conn.assigns[:current_user] do + token = sign_token!(conn.assigns[:current_user]) + redirect(conn, external: "https://feedback.plausible.io/sso/#{token}") + else + redirect(conn, external: "https://feedback.plausible.io") + end + end + + def roadmap(conn, _params) do + if conn.assigns[:current_user] do + token = sign_token!(conn.assigns[:current_user]) + redirect(conn, external: "https://feedback.plausible.io/sso/#{token}?returnUrl=https://feedback.plausible.io/roadmap") + else + redirect(conn, external: "https://feedback.plausible.io/roadmap") + end + end + + def contact_form(conn, _params) do + render(conn, "contact_form.html") + end + + def submit_contact_form(conn, %{"text" => text, "email" => email}) do + PlausibleWeb.Email.feedback(email, text) |> Plausible.Mailer.deliver_now + render(conn, "contact_thanks.html") + end + + def privacy(conn, _params) do + render(conn, "privacy.html") + end + + def data_policy(conn, _params) do + render(conn, "data_policy.html") + end + + def terms(conn, _params) do + render(conn, "terms.html") + end +end diff --git a/lib/plausible_web/controllers/site_controller.ex b/lib/plausible_web/controllers/site_controller.ex new file mode 100644 index 000000000..1f4f1d844 --- /dev/null +++ b/lib/plausible_web/controllers/site_controller.ex @@ -0,0 +1,115 @@ +defmodule PlausibleWeb.SiteController do + use PlausibleWeb, :controller + use Plausible.Repo + alias Plausible.Sites + + plug PlausibleWeb.RequireAccountPlug + + def new(conn, _params) do + changeset = Plausible.Site.changeset(%Plausible.Site{}) + + render(conn, "new.html", changeset: changeset, layout: {PlausibleWeb.LayoutView, "focus.html"}) + end + + def create_site(conn, %{"site" => site_params}) do + user = conn.assigns[:current_user] + + case insert_site(user.id, site_params) do + {:ok, %{site: site}} -> + Plausible.Tracking.event(conn, "New Site: Create", %{domain: site.domain}) + Plausible.Slack.notify("#{user.name} created #{site.domain}") + redirect(conn, to: "/#{site.domain}/snippet") + {:error, :site, changeset, _} -> + render(conn, "new.html", changeset: changeset) + end + end + + def add_snippet(conn, %{"website" => website}) do + site = Sites.get_for_user!(conn.assigns[:current_user].id, website) + conn + |> assign(:skip_plausible_tracking, true) + |> render("snippet.html", site: site, layout: {PlausibleWeb.LayoutView, "focus.html"}) + end + + def settings(conn, %{"website" => website}) do + site = Sites.get_for_user!(conn.assigns[:current_user].id, website) + |> Repo.preload(:google_auth) + + google_search_console_verified = if site.google_auth do + google_site = Plausible.Google.Api.fetch_site(site.domain, site.google_auth) + !google_site["error"] + end + + changeset = Plausible.Site.changeset(site, %{}) + conn + |> assign(:skip_plausible_tracking, true) + |> render("settings.html", site: site, google_search_console_verified: google_search_console_verified, changeset: changeset) + end + + def update_settings(conn, %{"website" => website, "site" => site_params}) do + site = Sites.get_for_user!(conn.assigns[:current_user].id, website) + changeset = site |> Plausible.Site.changeset(site_params) + res = changeset |> Repo.update + + case res do + {:ok, site} -> + conn + |> put_flash(:success, "Site settings saved succesfully") + |> redirect(to: "/#{site.domain}/settings") + {:error, changeset} -> + render("settings.html", site: site, changeset: changeset) + end + end + + def delete_site(conn, %{"website" => website}) do + site = + Sites.get_for_user!(conn.assigns[:current_user].id, website) + |> Repo.preload(:google_auth) + + Repo.delete_all(from sm in "site_memberships", where: sm.site_id == ^site.id) + Repo.delete_all(from p in "pageviews", where: p.hostname == ^site.domain) + + Repo.delete!(site.google_auth) + Repo.delete!(site) + + conn + |> put_flash(:success, "Site deleted succesfully along with all pageviews") + |> redirect(to: "/") + end + + def make_public(conn, %{"website" => website}) do + site = Sites.get_for_user!(conn.assigns[:current_user].id, website) + |> Plausible.Site.make_public + |> Repo.update! + + conn + |> put_flash(:success, "Congrats! Stats for #{site.domain} are now public.") + |> redirect(to: "/" <> site.domain <> "/settings") + end + + def make_private(conn, %{"website" => website}) do + site = Sites.get_for_user!(conn.assigns[:current_user].id, website) + |> Plausible.Site.make_private + |> Repo.update! + + conn + |> put_flash(:success, "Stats for #{site.domain} are now private.") + |> redirect(to: "/" <> site.domain <> "/settings") + end + + defp insert_site(user_id, params) do + site_changeset = Plausible.Site.changeset(%Plausible.Site{}, params) + + Ecto.Multi.new() + |> Ecto.Multi.insert(:site, site_changeset) + |> Ecto.Multi.run(:site_membership, fn repo, %{site: site} -> + membership_changeset = Plausible.Site.Membership.changeset(%Plausible.Site.Membership{}, %{ + site_id: site.id, + user_id: user_id + }) + repo.insert(membership_changeset) + end) + |> Repo.transaction + end + +end diff --git a/lib/plausible_web/controllers/stats_controller.ex b/lib/plausible_web/controllers/stats_controller.ex new file mode 100644 index 000000000..644dd191b --- /dev/null +++ b/lib/plausible_web/controllers/stats_controller.ex @@ -0,0 +1,292 @@ +defmodule PlausibleWeb.StatsController do + use PlausibleWeb, :controller + use Plausible.Repo + alias Plausible.Stats + + defp referrer_link(site, {name, count}, query) do + link = "/#{site.domain}/referrers/#{name}" <> PlausibleWeb.StatsView.query_params(query) + {{:link, name, link}, count} + end + + def stats(conn, %{"website" => website}) do + site = Repo.get_by(Plausible.Site, domain: website) + + if site && current_user_can_access?(conn, site) do + user = conn.assigns[:current_user] + if user && Plausible.Billing.needs_to_upgrade?(conn.assigns[:current_user]) do + redirect(conn, to: "/billing/upgrade") + else + if Plausible.Sites.has_pageviews?(site) do + demo = site.domain == "plausible.io" + + Plausible.Tracking.event(conn, "Site Analytics: Open", %{demo: demo}) + + query = Stats.Query.from(site.timezone, conn.params) + + conn + |> assign(:skip_plausible_tracking, !demo) + |> render("stats.html", + site: site, + query: query, + title: "Plausible · " <> site.domain + ) + else + conn + |> assign(:skip_plausible_tracking, true) + |> render("waiting_first_pageview.html", site: site) + end + end + else + render_error(conn, 404) + end + end + + def browsers_preview(conn, %{"domain" => domain}) do + site = Repo.get_by(Plausible.Site, domain: domain) + query = Stats.Query.from(site.timezone, conn.params) + + if site && current_user_can_access?(conn, site) do + render(conn, + "browsers_preview.html", + browsers: Stats.browsers(site, query), + site: site, + query: query, + layout: false + ) + else + render_error(conn, 404) + end + end + + def operating_systems_preview(conn, %{"domain" => domain}) do + site = Repo.get_by(Plausible.Site, domain: domain) + query = Stats.Query.from(site.timezone, conn.params) + + if site && current_user_can_access?(conn, site) do + render(conn, + "operating_systems_preview.html", + operating_systems: Stats.operating_systems(site, query), + site: site, + query: query, + layout: false + ) + else + render_error(conn, 404) + end + end + + def screen_sizes_preview(conn, %{"domain" => domain}) do + site = Repo.get_by(Plausible.Site, domain: domain) + query = Stats.Query.from(site.timezone, conn.params) + + if site && current_user_can_access?(conn, site) do + render(conn, + "screen_sizes_preview.html", + top_screen_sizes: Stats.top_screen_sizes(site, query), + site: site, + query: query, + layout: false + ) + else + render_error(conn, 404) + end + end + + def referrers_preview(conn, %{"domain" => domain}) do + site = Repo.get_by(Plausible.Site, domain: domain) + query = Stats.Query.from(site.timezone, conn.params) + + if site && current_user_can_access?(conn, site) do + render(conn, + "referrers_preview.html", + top_referrers: Stats.top_referrers(site, query) |> Enum.map(&(referrer_link(site, &1, query))), + site: site, + query: query, + layout: false + ) + else + render_error(conn, 404) + end + end + + def pages_preview(conn, %{"domain" => domain}) do + site = Repo.get_by(Plausible.Site, domain: domain) + query = Stats.Query.from(site.timezone, conn.params) + + if site && current_user_can_access?(conn, site) do + render(conn, + "pages_preview.html", + top_pages: Stats.top_pages(site, query), + site: site, + query: query, + layout: false + ) + else + render_error(conn, 404) + end + end + + def countries_preview(conn, %{"domain" => domain}) do + site = Repo.get_by(Plausible.Site, domain: domain) + query = Stats.Query.from(site.timezone, conn.params) + + if site && current_user_can_access?(conn, site) do + render(conn, + "countries_preview.html", + top_countries: Stats.countries(site, query), + site: site, + query: query, + layout: false + ) + else + render_error(conn, 404) + end + end + + def main_graph(conn, %{"domain" => domain}) do + site = Repo.get_by(Plausible.Site, domain: domain) + query = Stats.Query.from(site.timezone, conn.params) + + plot_task = Task.async(fn -> Stats.calculate_plot(site, query) end) + {pageviews, visitors} = Stats.pageviews_and_visitors(site, query) + {plot, labels, present_index} = Task.await(plot_task) + + json(conn, %{ + plot: plot, + labels: labels, + present_index: present_index, + pageviews: pageviews, + unique_visitors: visitors, + interval: query.step_type + }) + end + + def compare(conn, %{"domain" => domain}) do + site = Repo.get_by(Plausible.Site, domain: domain) + query = Stats.Query.from(site.timezone, conn.params) + {pageviews, ""} = Integer.parse(conn.params["pageviews"]) + {unique_visitors, ""} = Integer.parse(conn.params["unique_visitors"]) + + if site && current_user_can_access?(conn, site) do + {change_pageviews, change_visitors} = Stats.compare_pageviews_and_visitors(site, query, {pageviews, unique_visitors}) + + json(conn, %{ + change_pageviews: change_pageviews, + change_visitors: change_visitors + }) + end + end + + def referrers(conn, %{"domain" => domain}) do + site = Repo.get_by(Plausible.Site, domain: domain) + + if site && current_user_can_access?(conn, site) do + query = Stats.Query.from(site.timezone, conn.params) + referrers = Stats.top_referrers(site, query, 100) + + render(conn, "referrers.html", layout: false, site: site, top_referrers: referrers, query: query) + else + render_error(conn, 404) + end + end + + def referrer_drilldown(conn, %{"domain" => domain, "referrer" => "Google"}) do + site = Repo.get_by(Plausible.Site, domain: domain) + |> Repo.preload(:google_auth) + + if site && current_user_can_access?(conn, site) do + query = Stats.Query.from(site.timezone, conn.params) + total_visitors = Stats.visitors_from_referrer(site, query, "Google") + search_terms = if site.google_auth do + Plausible.Google.Api.fetch_stats(site, site.google_auth, query) + end + + render(conn, "google_referrer.html", + layout: false, + site: site, + search_terms: search_terms, + total_visitors: total_visitors, + query: query + ) + else + render_error(conn, 404) + end + end + + def referrer_drilldown(conn, %{"domain" => domain, "referrer" => referrer}) do + site = Repo.get_by(Plausible.Site, domain: domain) + + if site && current_user_can_access?(conn, site) do + query = Stats.Query.from(site.timezone, conn.params) + referrers = Stats.referrer_drilldown(site, query, referrer) + total_visitors = Stats.visitors_from_referrer(site, query, referrer) + + render(conn, "referrer_drilldown.html", layout: false, site: site, referrers: referrers, referrer: referrer, total_visitors: total_visitors, query: query) + else + render_error(conn, 404) + end + end + + def pages(conn, %{"domain" => domain}) do + site = Repo.get_by(Plausible.Site, domain: domain) + + if site && current_user_can_access?(conn, site) do + query = Stats.Query.from(site.timezone, conn.params) + pages = Stats.top_pages(site, query, 100) + + render(conn, "pages.html", layout: false, site: site, top_pages: pages) + else + render_error(conn, 404) + end + end + + def countries(conn, %{"domain" => domain}) do + site = Repo.get_by(Plausible.Site, domain: domain) + + if site && current_user_can_access?(conn, site) do + query = Stats.Query.from(site.timezone, conn.params) + countries = Stats.countries(site, query, 100) + + render(conn, "countries.html", layout: false, site: site, countries: countries) + else + render_error(conn, 404) + end + end + + def operating_systems(conn, %{"domain" => domain}) do + site = Repo.get_by(Plausible.Site, domain: domain) + + if site && current_user_can_access?(conn, site) do + query = Stats.Query.from(site.timezone, conn.params) + operating_systems = Stats.operating_systems(site, query, 100) + + render(conn, "operating_systems.html", layout: false, site: site, operating_systems: operating_systems) + else + render_error(conn, 404) + end + end + + def browsers(conn, %{"domain" => domain}) do + site = Repo.get_by(Plausible.Site, domain: domain) + + if site && current_user_can_access?(conn, site) do + query = Stats.Query.from(site.timezone, conn.params) + browsers = Stats.browsers(site, query, 100) + + render(conn, "browsers.html", layout: false, site: site, browsers: browsers) + else + render_error(conn, 404) + end + end + + defp current_user_can_access?(_conn, %Plausible.Site{public: true}) do + true + end + + defp current_user_can_access?(conn, site) do + case conn.assigns[:current_user] do + nil -> false + user -> Plausible.Sites.is_owner?(user.id, site) + end + end +end diff --git a/lib/plausible_web/email.ex b/lib/plausible_web/email.ex new file mode 100644 index 000000000..87a30bd40 --- /dev/null +++ b/lib/plausible_web/email.ex @@ -0,0 +1,90 @@ +defmodule PlausibleWeb.Email do + use Bamboo.Phoenix, view: PlausibleWeb.EmailView + import Bamboo.PostmarkHelper + + def welcome_email(user) do + new_email() + |> to(user) + |> from("Uku Taht ") + |> tag("welcome-email") + |> subject("Welcome to Plausible :) Plus, a quick question...") + |> render("welcome_email.html", user: user) + end + + def help_email(user) do + new_email() + |> to(user) + |> from("Uku Taht ") + |> tag("help-email") + |> subject("Your Plausible setup") + |> render("help_email.html", user: user) + end + + def password_reset_email(email, reset_link) do + new_email() + |> to(email) + |> from("Uku Taht ") + |> tag("password-reset-email") + |> subject("Plausible password reset") + |> render("password_reset_email.html", reset_link: reset_link) + end + + def activation_email(user, link) do + new_email() + |> to(user.email) + |> from("Uku Taht ") + |> tag("activation-email") + |> subject("Plausible activation link") + |> render("activation_email.html", name: user.name, link: link) + end + + def trial_two_week_reminder(user) do + new_email() + |> to(user) + |> bcc("uku@plausible.io") + |> from("Uku Taht ") + |> tag("trial-two-week-reminder") + |> subject("14 days left on your Plausible trial") + |> render("trial_two_week_reminder.html", user: user) + end + + def trial_upgrade_email(user, day, pageviews) do + new_email() + |> to(user) + |> bcc("uku@plausible.io") + |> from("Uku Taht ") + |> tag("trial-upgrade-email") + |> subject("Your Plausible trial ends #{day}") + |> render("trial_upgrade_email.html", user: user, day: day, pageviews: pageviews) + end + + def trial_over_email(user) do + new_email() + |> to(user) + |> from("Uku Taht ") + |> tag("trial-over-email") + |> subject("Your Plausible trial has ended") + |> render("trial_over_email.html", user: user) + end + + def feedback_survey_email(user) do + new_email() + |> to(user) + |> from("Uku Taht ") + |> tag("feedback-survey-email") + |> subject("Plausible feedback") + |> render("feedback_survey.html", user: user) + end + + def feedback(from, text) do + from = if from == "", do: "anonymous@plausible.io", else: from + + new_email() + |> to("uku@plausible.io") + |> from("feedback@plausible.io") + |> put_param("ReplyTo", from) + |> tag("feedback") + |> subject("New feedback submission") + |> text_body(text) + end +end diff --git a/lib/plausible_web/endpoint.ex b/lib/plausible_web/endpoint.ex new file mode 100644 index 000000000..be465e3ae --- /dev/null +++ b/lib/plausible_web/endpoint.ex @@ -0,0 +1,61 @@ +defmodule PlausibleWeb.Endpoint do + use Phoenix.Endpoint, otp_app: :plausible + use Sentry.Phoenix.Endpoint + use Appsignal.Phoenix + + socket "/socket", PlausibleWeb.UserSocket, + websocket: true, + longpoll: false + + # Serve at "/" the static files from "priv/static" directory. + # + # You should set gzip to true if you are running phx.digest + # when deploying your static files in production. + plug Plug.Static, + at: "/", + from: :plausible, + gzip: false, + only: ~w(css fonts images js favicon.ico robots.txt) + + # Code reloading can be explicitly enabled under the + # :code_reloader configuration of your endpoint. + if code_reloading? do + socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket + plug Phoenix.LiveReloader + plug Phoenix.CodeReloader + end + + plug Plug.RequestId + plug Plug.Logger + + plug Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + pass: ["*/*"], + json_decoder: Phoenix.json_library() + + plug Plug.MethodOverride + plug Plug.Head + + # The session will be stored in the cookie and signed, + # this means its contents can be read but not tampered with. + # Set :encryption_salt if you would also like to encrypt it. + plug Plug.Session, + store: :cookie, + key: "_plausible_key", + signing_salt: "3IL0ob4k", + max_age: 60*60*24*365*5 # 5 years, this is super long but the SlidingSessionTimeout will log people out if they don't return for 2 weeks + + + plug CORSPlug + plug PlausibleWeb.Router + + def clean_url() do + url = PlausibleWeb.Endpoint.url + + if Mix.env() == :prod do + URI.parse(url) |> Map.put(:port, nil) |> URI.to_string() + else + url + end + end +end diff --git a/lib/plausible_web/last_seen_plug.ex b/lib/plausible_web/last_seen_plug.ex new file mode 100644 index 000000000..be6f1c917 --- /dev/null +++ b/lib/plausible_web/last_seen_plug.ex @@ -0,0 +1,35 @@ +defmodule PlausibleWeb.LastSeenPlug do + import Plug.Conn + use Plausible.Repo + + @one_hour 60 * 60 + + def init(opts) do + opts + end + + def call(conn, _opts) do + last_seen = get_session(conn, :last_seen) + user = conn.assigns[:current_user] + + cond do + user && last_seen && last_seen < (unix_now() - @one_hour) -> + persist_last_seen(user) + put_session(conn, :last_seen, unix_now()) + user && !last_seen -> + put_session(conn, :last_seen, unix_now()) + true -> + conn + end + end + + defp persist_last_seen(user) do + q = from(u in Plausible.Auth.User, where: u.id == ^user.id) + + Repo.update_all(q, [set: [last_seen: DateTime.utc_now()]]) + end + + defp unix_now do + DateTime.utc_now() |> DateTime.to_unix + end +end diff --git a/lib/plausible_web/plugs/require_account.ex b/lib/plausible_web/plugs/require_account.ex new file mode 100644 index 000000000..de80e24ea --- /dev/null +++ b/lib/plausible_web/plugs/require_account.ex @@ -0,0 +1,18 @@ +defmodule PlausibleWeb.RequireAccountPlug do + import Plug.Conn + + def init(options) do + options + end + + def call(conn, _opts) do + case conn.assigns[:current_user] do + nil -> + Plug.Conn.put_session(conn, :login_dest, conn.request_path) + |> Phoenix.Controller.redirect(to: "/login") + |> halt + _email -> + conn + end + end +end diff --git a/lib/plausible_web/plugs/require_logged_out.ex b/lib/plausible_web/plugs/require_logged_out.ex new file mode 100644 index 000000000..6d5309936 --- /dev/null +++ b/lib/plausible_web/plugs/require_logged_out.ex @@ -0,0 +1,16 @@ +defmodule PlausibleWeb.RequireLoggedOutPlug do + def init(options) do + options + end + + def call(conn, _opts) do + cond do + conn.assigns[:current_user] -> + conn + |> Phoenix.Controller.redirect(to: "/") + |> Plug.Conn.halt + :else -> + conn + end + end +end diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex new file mode 100644 index 000000000..2a18895ba --- /dev/null +++ b/lib/plausible_web/router.ex @@ -0,0 +1,109 @@ +defmodule PlausibleWeb.Router do + use PlausibleWeb, :router + use Plug.ErrorHandler + use Sentry.Plug + @two_weeks_in_seconds 60 * 60 * 24 * 14 + + pipeline :browser do + plug :accepts, ["html"] + plug :fetch_session + plug :fetch_flash + plug :protect_from_forgery + plug :put_secure_browser_headers + plug :assign_device_id + plug PlausibleWeb.SessionTimeoutPlug, timeout_after_seconds: @two_weeks_in_seconds + plug PlausibleWeb.AuthPlug + plug PlausibleWeb.LastSeenPlug + end + + pipeline :api do + plug :accepts, ["application/json"] + plug :fetch_session + plug PlausibleWeb.AuthPlug + end + + if Mix.env == :dev do + forward "/sent-emails", Bamboo.SentEmailViewerPlug + end + + scope "/api", PlausibleWeb do + pipe_through :api + + post "/page", Api.ExternalController, :page + get "/error", Api.ExternalController, :error + + post "/paddle/webhook", Api.PaddleController, :webhook + + get "/:domain/status", Api.InternalController, :domain_status + get "/:domain/referrers", StatsController, :referrers + get "/:domain/referrers/:referrer", StatsController, :referrer_drilldown + get "/:domain/pages", StatsController, :pages + get "/:domain/countries", StatsController, :countries + get "/:domain/operating-systems", StatsController, :operating_systems + get "/:domain/browsers", StatsController, :browsers + get "/:domain/compare", StatsController, :compare + end + + scope "/", PlausibleWeb do + pipe_through :browser + + get "/register", AuthController, :register_form + post "/register", AuthController, :register + get "/claim-activation", AuthController, :claim_activation_link + get "/login", AuthController, :login_form + post "/login", AuthController, :login + get "/claim-login", AuthController, :claim_login_link + get "/password/request-reset", AuthController, :password_reset_request_form + post "/password/request-reset", AuthController, :password_reset_request + get "/password/reset", AuthController, :password_reset_form + post "/password/reset", AuthController, :password_reset + get "/password", AuthController, :password_form + post "/password", AuthController, :set_password + post "/logout", AuthController, :logout + get "/settings", AuthController, :user_settings + put "/settings", AuthController, :save_settings + delete "/me", AuthController, :delete_me + + get "/auth/google/callback", AuthController, :google_auth_callback + + get "/", PageController, :index + get "/privacy", PageController, :privacy + get "/terms", PageController, :terms + get "/data-policy", PageController, :data_policy + get "/feedback", PageController, :feedback + get "/roadmap", PageController, :roadmap + get "/contact", PageController, :contact_form + post "/contact", PageController, :submit_contact_form + + get "/billing/change-plan", BillingController, :change_plan_form + post "/billing/change-plan/:plan_name", BillingController, :change_plan + get "/billing/upgrade", BillingController, :upgrade + get "/billing/success", BillingController, :success + + get "/sites/new", SiteController, :new + post "/sites", SiteController, :create_site + post "/sites/:website/make-public", SiteController, :make_public + post "/sites/:website/make-private", SiteController, :make_private + get "/:website/snippet", SiteController, :add_snippet + get "/:website/settings", SiteController, :settings + put "/:website/settings", SiteController, :update_settings + delete "/:website", SiteController, :delete_site + + get "/stats/:domain/referrers", StatsController, :referrers_preview + get "/stats/:domain/pages", StatsController, :pages_preview + get "/stats/:domain/countries", StatsController, :countries_preview + get "/stats/:domain/screen-sizes", StatsController, :screen_sizes_preview + get "/stats/:domain/operating-systems", StatsController, :operating_systems_preview + get "/stats/:domain/browsers", StatsController, :browsers_preview + get "/stats/:domain/main-graph", StatsController, :main_graph + get "/:website/*path", StatsController, :stats + end + + def assign_device_id(conn, _opts) do + if is_nil(Plug.Conn.get_session(conn, :device_id)) do + Plug.Conn.put_session(conn, :device_id, UUID.uuid4()) + else + conn + end + end +end diff --git a/lib/plausible_web/session_timeout_plug.ex b/lib/plausible_web/session_timeout_plug.ex new file mode 100644 index 000000000..3b02d9878 --- /dev/null +++ b/lib/plausible_web/session_timeout_plug.ex @@ -0,0 +1,32 @@ +defmodule PlausibleWeb.SessionTimeoutPlug do + import Plug.Conn + + def init(opts \\ []) do + opts + end + + def call(conn, opts) do + timeout_at = get_session(conn, :session_timeout_at) + user_id = get_session(conn, :current_user_id) + + if user_id && timeout_at && now() > timeout_at do + logout_user(conn) + else + put_session(conn, :session_timeout_at, new_session_timeout_at(opts[:timeout_after_seconds])) + end + end + + defp logout_user(conn) do + conn + |> put_session(:current_user_id, nil) # Leave `device_id` in the session for accurate tracking + |> assign(:session_timeout, true) + end + + defp now do + DateTime.utc_now() |> DateTime.to_unix + end + + defp new_session_timeout_at(timeout_after_seconds) do + now() + timeout_after_seconds + end +end diff --git a/lib/plausible_web/templates/auth/login_form.html.eex b/lib/plausible_web/templates/auth/login_form.html.eex new file mode 100644 index 000000000..d7fdf526e --- /dev/null +++ b/lib/plausible_web/templates/auth/login_form.html.eex @@ -0,0 +1,22 @@ +<%= form_for @conn, "/login", [class: "w-full max-w-sm mx-auto bg-white shadow-md rounded px-8 py-6 mt-8"], fn f -> %> +

<%= get_flash(@conn, :login_title) || "Enter your email and password" %>

+ <%= if get_flash(@conn, :login_instructions) do %> +

<%= get_flash(@conn, :login_instructions) %>

+ <% end %> + <%= if @conn.assigns[:error] do %> +
<%= @conn.assigns[:error] %>
+ <% end %> +
+ <%= label f, :email, class: "block text-grey-darker text-sm font-bold mb-2" %> + <%= email_input f, :email, class: "bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:bg-white focus:border-grey-light", placeholder: "user@example.com" %> +
+
+ <%= label f, :password, class: "block text-grey-darker text-sm font-bold mb-2" %> + <%= password_input f, :password, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:bg-white focus:border-grey-light" %> +

Forgot password? Click here to reset it.

+
+ <%= submit "Login →", class: "button mt-4 w-full" %> +

+ Don't have an account? <%= link("Register", to: "/register") %> instead. +

+<% end %> diff --git a/lib/plausible_web/templates/auth/password_form.html.eex b/lib/plausible_web/templates/auth/password_form.html.eex new file mode 100644 index 000000000..c59575e94 --- /dev/null +++ b/lib/plausible_web/templates/auth/password_form.html.eex @@ -0,0 +1,14 @@ +<%= form_for @conn, "/password", [class: "bg-white max-w-sm w-full mx-auto shadow-md rounded px-8 py-6 mt-8"], fn f -> %> +

Set your password

+

Min 6 characters

+
+ <%= password_input f, :password, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:bg-white focus:border-grey-light" %> + <%= if @conn.assigns[:changeset] do %> + <%= error_tag @changeset, :password %> + <% end %> +
+ <%= submit "Set password →", class: "button mt-4 w-full" %> +

+ Don't have an account? <%= link("Register", to: "/register") %> instead. +

+<% end %> diff --git a/lib/plausible_web/templates/auth/password_reset_form.html.eex b/lib/plausible_web/templates/auth/password_reset_form.html.eex new file mode 100644 index 000000000..939936730 --- /dev/null +++ b/lib/plausible_web/templates/auth/password_reset_form.html.eex @@ -0,0 +1,15 @@ +<%= form_for @conn, "/password/reset", [class: "bg-white max-w-sm w-full mx-auto shadow-md rounded px-8 py-6 mt-8"], fn f -> %> +

Reset your password

+

Min 6 characters

+
+ <%= password_input f, :password, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:bg-white focus:border-grey-light" %> + <%= if @conn.assigns[:changeset] do %> + <%= error_tag @changeset, :password %> + <% end %> +
+ <%= hidden_input f, :token, value: @token %> + <%= submit "Set password →", class: "button mt-4 w-full" %> +

+ Don't have an account? <%= link("Register", to: "/register") %> instead. +

+<% end %> diff --git a/lib/plausible_web/templates/auth/password_reset_request_form.html.eex b/lib/plausible_web/templates/auth/password_reset_request_form.html.eex new file mode 100644 index 000000000..b491fbb28 --- /dev/null +++ b/lib/plausible_web/templates/auth/password_reset_request_form.html.eex @@ -0,0 +1,11 @@ +<%= form_for @conn, "/password/request-reset", [class: "max-w-sm w-full mx-auto bg-white shadow-md rounded px-8 py-6 mt-8"], fn f -> %> +

Reset your password

+
Enter your email so we can send a password reset link
+
+ <%= email_input f, :email, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:bg-white focus:border-grey-light", placeholder: "user@example.com" %> +
+ <%= if @conn.assigns[:error] do %> +
<%= @conn.assigns[:error] %>
+ <% end %> + <%= submit "Send reset link →", class: "button mt-4 w-full" %> +<% end %> diff --git a/lib/plausible_web/templates/auth/password_reset_request_success.html.eex b/lib/plausible_web/templates/auth/password_reset_request_success.html.eex new file mode 100644 index 000000000..e2a23fc16 --- /dev/null +++ b/lib/plausible_web/templates/auth/password_reset_request_success.html.eex @@ -0,0 +1,14 @@ +
+
+

Success!

+
+
+ We have sent an email with password reset instructions to <%= @email %> if it exists in our database. +
+
+ Didn't recieve an email? +
+
+ Please check your spam folder and contact uku@plausible.io if the problem persists +
+
diff --git a/lib/plausible_web/templates/auth/register_form.html.eex b/lib/plausible_web/templates/auth/register_form.html.eex new file mode 100644 index 000000000..b3473835e --- /dev/null +++ b/lib/plausible_web/templates/auth/register_form.html.eex @@ -0,0 +1,20 @@ +<%= form_for @changeset, "/register", [class: "max-w-sm w-full mx-auto bg-white shadow-md rounded px-8 py-6 mb-4 mt-8"], fn f -> %> +
+

Enter your details to get started

+
+
+ <%= label f, :name, "Full name", class: "block text-grey-darker text-sm font-bold mb-2" %> + <%= text_input f, :name, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:bg-white focus:border-grey-light", placeholder: "Jane Doe" %> + <%= error_tag f, :name %> +
+
+ <%= label f, :email, class: "block text-grey-darker text-sm font-bold mb-2" %> +

No spam, guaranteed.

+ <%= email_input f, :email, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:bg-white focus:border-grey-light", placeholder: "example@email.com" %> + <%= error_tag f, :email %> +
+ <%= submit "Send activation link →", class: "button mt-4 w-full" %> +

+ Already have an account? <%= link("Log in", to: "/login") %> instead. +

+<% end %> diff --git a/lib/plausible_web/templates/auth/register_success.html.eex b/lib/plausible_web/templates/auth/register_success.html.eex new file mode 100644 index 000000000..42df68006 --- /dev/null +++ b/lib/plausible_web/templates/auth/register_success.html.eex @@ -0,0 +1,14 @@ +
+
+

Success!

+
+
+ We've sent an activation link to <%= @email %>. Please click on the link to activate your account. +
+
+ Didn't recieve an email? +
+
+ Please check your spam folder and contact uku@plausible.io if the problem persists +
+
diff --git a/lib/plausible_web/templates/auth/user_settings.html.eex b/lib/plausible_web/templates/auth/user_settings.html.eex new file mode 100644 index 000000000..0dab73274 --- /dev/null +++ b/lib/plausible_web/templates/auth/user_settings.html.eex @@ -0,0 +1,82 @@ +
+

Subscription Plan

+ +
+ +
+
+

Current plan

+ <%= if @subscription do %> +
<%= subscription_name(@subscription) %>
+ <%= link("Change plan", to: "/billing/change-plan", class: "text-sm text-indigo font-medium") %> + <% else %> +
Free trial
+ <%= link("Upgrade", to: "/billing/upgrade", class: "text-sm text-indigo font-medium") %> + <% end %> +
+
+

Next bill amount

+ <%= if @subscription do %> +
$<%= @subscription.next_bill_amount %>
+ <%= link("Update billing info", to: @subscription.update_url, class: "text-sm text-indigo font-medium") %> + <% else %> +
$0
+ <% end %> +
+
+

Next bill date

+ + <%= if @subscription do %> +
<%= Timex.format!(@subscription.next_bill_date, "{Mshort} {D}, {YYYY}") %>
+ <% else %> +
+ <% end %> +
+
+ +

Your usage

+ +
+ <%= delimit_integer(Plausible.Billing.usage(@conn.assigns[:current_user])) %> + pageviews in the last 30 days +
+ + <%= if @subscription do %> +
+ <%= link("Cancel my subscription", to: @subscription.cancel_url, class: "text-indigo text-sm") %> +
+ <% end %> +
+ +
+
+

Account settings

+
+ +
+ + <%= form_for @changeset, "/settings", [class: "max-w-xs"], fn f -> %> +
+ <%= label f, :name, class: "block text-grey-darker text-sm font-bold mb-2" %> + <%= text_input f, :name, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:border-grey-light" %> + <%= error_tag f, :name %> +
+
+ <%= label f, :email, class: "block text-grey-darker text-sm font-bold mb-2" %> + <%= email_input f, :email, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:border-grey-light" %> + <%= error_tag f, :email %> +
+ <%= submit "Save changes", class: "button mt-4" %> + <% end %> +
+ +
+
+

Delete account

+
+ +
+ +

Deleting your account removes all sites and stats you've collected

+ <%= link "Delete my account ", to: "/me", method: :delete, class: "button bg-red-dark mt-4", data: [confirm: "Deleting your account cannot be reversed. Are you sure?"] %> +
diff --git a/lib/plausible_web/templates/billing/_checkout_button.html.eex b/lib/plausible_web/templates/billing/_checkout_button.html.eex new file mode 100644 index 000000000..3e9baffc4 --- /dev/null +++ b/lib/plausible_web/templates/billing/_checkout_button.html.eex @@ -0,0 +1 @@ + diff --git a/lib/plausible_web/templates/billing/change_plan.html.eex b/lib/plausible_web/templates/billing/change_plan.html.eex new file mode 100644 index 000000000..50204cbbb --- /dev/null +++ b/lib/plausible_web/templates/billing/change_plan.html.eex @@ -0,0 +1,56 @@ +
+

Change subscription plan

+ +
+ If you choose to change your plan, your current subscription will + be prorated. +
+ +
+ 10,000 / mo + $6 / mo + <%= if Plausible.Billing.Plans.is?(@subscription, :personal) do %> +
+ + Selected +
+ <% else %> + <%= button("Select", class: "button button-sm", method: :post, to: "/billing/change-plan/personal") %> + <% end %> +
+
+ 100,000 / mo + $12 / mo + <%= if Plausible.Billing.Plans.is?(@subscription, :startup) do %> +
+ + Selected +
+ <% else %> + <%= button("Select", class: "button button-sm", method: :post, to: "/billing/change-plan/startup") %> + <% end %> +
+
+ 1,000,000 / mo + $36 / mo + <%= if Plausible.Billing.Plans.is?(@subscription, :business) do %> +
+ + Selected +
+ <% else %> + <%= button("Select", class: "button button-sm", method: :post, to: "/billing/change-plan/business") %> + <% end %> +
+ + + + + + +
diff --git a/lib/plausible_web/templates/billing/upgrade.html.eex b/lib/plausible_web/templates/billing/upgrade.html.eex new file mode 100644 index 000000000..569cb00d8 --- /dev/null +++ b/lib/plausible_web/templates/billing/upgrade.html.eex @@ -0,0 +1,59 @@ +
+

Upgrade your free trial

+ +
+ <%= cond do %> + <% Timex.equal?(@trial_end_date, @today) -> %> + Today is the last day of your trial. Please select a plan and enter + your billing info to access your stats after the trial ends. + <% Timex.after?(@trial_end_date, @today) -> %> +

+ Your free trial ends on <%= Timex.format!(@trial_end_date, "{WDshort} {D} {Mshort}") %>. Please select a plan and enter + your billing info to access your stats after the trial ends. +

+ <% Timex.before?(@trial_end_date, @today) -> %> +

+ Your free trial ended on <%= Timex.format!(@trial_end_date, "{Mshort} {D}")%>. To access your stats going forward, please select a plan and enter your billing info. +

+ <% end %> + +

+ If you go over your limit in the future, we will contact you about upgrading. + Don't worry, we will never bill you unexpectedly or stop recording without telling you first. +

+

+ All plans are on a monthly basis, cancel any time. + <%= if Plausible.Billing.was_beta_user(@conn.assigns[:current_user]) do %> + As a beta user, you will get 33% off on all plans. The coupon will be applied at checkout. + <% end %> +

+
+
+ +
+ 10,000 / mo + $6 / mo + <%= render("_checkout_button.html", conn: @conn, plan: :personal) %> +
+
+ 100,000 / mo + $12 / mo + <%= render("_checkout_button.html", conn: @conn, plan: :startup) %> +
+
+ 1,000,000 / mo + $36 / mo + <%= render("_checkout_button.html", conn: @conn, plan: :business) %> +
+
+ Your usage: <%= PlausibleWeb.AuthView.delimit_integer(@usage) %> pageviews in the last 30 days +
+ +
+ Questions? Contact <%= link("uku@plausibile.io", to: "mailto: uku@plausibile.io", class: "text-indigo") %> +
+ + + + +
diff --git a/lib/plausible_web/templates/email/activation_email.html.eex b/lib/plausible_web/templates/email/activation_email.html.eex new file mode 100644 index 000000000..382107ea5 --- /dev/null +++ b/lib/plausible_web/templates/email/activation_email.html.eex @@ -0,0 +1,5 @@ +Hi <%= @name %>, +

+Thank you for signing up to Plausible. Click here to activate your account. +

+This link will expire in 24 hours. If you don't use it by then, you can request another activation link. diff --git a/lib/plausible_web/templates/email/feedback_survey.html.eex b/lib/plausible_web/templates/email/feedback_survey.html.eex new file mode 100644 index 000000000..31d06fda1 --- /dev/null +++ b/lib/plausible_web/templates/email/feedback_survey.html.eex @@ -0,0 +1,17 @@ +Hey <%= user_salutation(@user) %>, +

+Big thanks for giving Plausible a try at such an early stage. +

+Now that you've had some time to use the product, I'd love to know how it's working for you. I've created a Typeform with 4 quick questions about your experience; it would help a lot if you could fill it out. +

+<%= link("Take the survey", to: "https://plausible.typeform.com/to/szHj56") %> +

+I will use your response to help prioritise the roadmap for the next months. You can request features and let me know about any annoyances/bugs you've encountered. Be completely honest, I can take it :) +

+Thanks,
+Uku Taht
+Founder, Plausible Insights +

+-- +

+https://plausible.io diff --git a/lib/plausible_web/templates/email/help_email.html.eex b/lib/plausible_web/templates/email/help_email.html.eex new file mode 100644 index 000000000..3c855ea09 --- /dev/null +++ b/lib/plausible_web/templates/email/help_email.html.eex @@ -0,0 +1,13 @@ +Hey <%= user_salutation(@user) %>, +

+I saw that you signed up for Plausible but the setup isn't working completely. Is there anything I can do to help? +

+If you have any questions let me know. +

+Thanks,
+Uku Taht
+Founder, Plausible Insights +

+-- +

+https://plausible.io diff --git a/lib/plausible_web/templates/email/password_reset_email.html.eex b/lib/plausible_web/templates/email/password_reset_email.html.eex new file mode 100644 index 000000000..612c7f117 --- /dev/null +++ b/lib/plausible_web/templates/email/password_reset_email.html.eex @@ -0,0 +1,2 @@ +Click here to reset your Plausible password.

+This link will expire in 1 hour. If you don't use it by then, you can request another login link. diff --git a/lib/plausible_web/templates/email/trial_over_email.html.eex b/lib/plausible_web/templates/email/trial_over_email.html.eex new file mode 100644 index 000000000..b6ff317a8 --- /dev/null +++ b/lib/plausible_web/templates/email/trial_over_email.html.eex @@ -0,0 +1,16 @@ +Hey <%= user_salutation(@user) %>, +

+Your free 30-day trial has come to an end. Upgrade to a paid plan now to access to your stats again.

+ +<%= link("Upgrade now", to: "https://plausible.io/billing/upgrade") %> + +We will keep recording stats for another month to give you time to upgrade. If you still haven't upgraded by then, we will stop recording. + +

+Thanks,
+Uku Taht
+Founder, Plausible Insights +

+-- +

+https://plausible.io diff --git a/lib/plausible_web/templates/email/trial_two_week_reminder.html.eex b/lib/plausible_web/templates/email/trial_two_week_reminder.html.eex new file mode 100644 index 000000000..7cadb1b1e --- /dev/null +++ b/lib/plausible_web/templates/email/trial_two_week_reminder.html.eex @@ -0,0 +1,13 @@ +Hey <%= user_salutation(@user) %>, +

+Thank you so much for trying out Plausible! +

+This is your friendly reminder that you have 14 days left on your Plausible free trial. +

+I will contact you towards the end of the trial with instructions to upgrade from your free trial to a paid plan. +

+If you have any questions or feedback for me, feel free to reply to this email. +

+Thanks,
+Uku Taht
+https://plausible.io diff --git a/lib/plausible_web/templates/email/trial_upgrade_email.html.eex b/lib/plausible_web/templates/email/trial_upgrade_email.html.eex new file mode 100644 index 000000000..277f453bd --- /dev/null +++ b/lib/plausible_web/templates/email/trial_upgrade_email.html.eex @@ -0,0 +1,18 @@ +Hey <%= user_salutation(@user) %>, +

+Your free 30-day trial is ending <%= @day %>, but you can keep using Plausible by upgrading to a paid plan. +

+In the last month, your account has used <%= PlausibleWeb.AuthView.delimit_integer(@pageviews) %> pageviews. +Based on that we recommend you select the <%= suggested_plan_name(@pageviews) %> plan which runs at <%= suggested_plan_cost(@pageviews) %>. +

+<%= link("Upgrade now", to: "https://plausible.io/billing/upgrade") %> +

+<%= if Plausible.Billing.was_beta_user(@user) do %> + PS: Since you joined during the beta, you'll get 33% off at checkout.

+<% end %> +Have any questions? Just reply to this email to get in touch! +

+

+Thanks,
+Uku Taht
+https://plausible.io diff --git a/lib/plausible_web/templates/email/welcome_email.html.eex b/lib/plausible_web/templates/email/welcome_email.html.eex new file mode 100644 index 000000000..af75adf73 --- /dev/null +++ b/lib/plausible_web/templates/email/welcome_email.html.eex @@ -0,0 +1,17 @@ +Hey <%= user_salutation(@user) %>, +

+I'm building Plausible to help create a more ethical approach to tracking website visitors. I really +appreciate you joining, and I hope you'll love the simple analytics we provide. +

+If you wouldn't mind, I'd love it if you answered one quick question: why did you sign up for Plausible? +

+I'm asking because knowing what made you sign up is really helpful for us in making sure that we're +delivering on what our users want. Just hit 'reply' and let me know. +

+Thanks,
+Uku Taht,
+CEO, Plausible Insights +

+-- +

+https://plausible.io diff --git a/lib/plausible_web/templates/error/error.html.eex b/lib/plausible_web/templates/error/error.html.eex new file mode 100644 index 000000000..98054ff61 --- /dev/null +++ b/lib/plausible_web/templates/error/error.html.eex @@ -0,0 +1,19 @@ + + + + + + + + "> + <%= assigns[:title] || "Plausible · Web analytics" %> + "/> + + +
+

<%= @status %>

+
<%= @message %>
+ <%= link("Go to the homepage", to: "/", class: "button mt-4") %> +
+ + diff --git a/lib/plausible_web/templates/layout/_footer.html.eex b/lib/plausible_web/templates/layout/_footer.html.eex new file mode 100644 index 000000000..265d4b348 --- /dev/null +++ b/lib/plausible_web/templates/layout/_footer.html.eex @@ -0,0 +1,18 @@ + diff --git a/lib/plausible_web/templates/layout/_svg_icons.html.eex b/lib/plausible_web/templates/layout/_svg_icons.html.eex new file mode 100644 index 000000000..75f0d5750 --- /dev/null +++ b/lib/plausible_web/templates/layout/_svg_icons.html.eex @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/plausible_web/templates/layout/_tracking.html.eex b/lib/plausible_web/templates/layout/_tracking.html.eex new file mode 100644 index 000000000..c756bdcba --- /dev/null +++ b/lib/plausible_web/templates/layout/_tracking.html.eex @@ -0,0 +1,3 @@ +<%= if Mix.env() == :prod && !@conn.assigns[:skip_plausible_tracking] do %> + +<% end %> diff --git a/lib/plausible_web/templates/layout/app.html.eex b/lib/plausible_web/templates/layout/app.html.eex new file mode 100644 index 000000000..e715c19a4 --- /dev/null +++ b/lib/plausible_web/templates/layout/app.html.eex @@ -0,0 +1,71 @@ + + + + + + + + "> + <%= assigns[:title] || "Plausible · Simple web analytics" %> + "/> + <%= render("_tracking.html", assigns) %> + + + <%= render("_svg_icons.html") %> + + + <%= if get_flash(@conn, :success) do %> + + <% end %> + + <%= if get_flash(@conn, :error) do %> + + <% end %> + +
+ <%= render @view_module, @view_template, assigns %> +
+ <%= render("_footer.html", assigns) %> + + + + + diff --git a/lib/plausible_web/templates/layout/focus.html.eex b/lib/plausible_web/templates/layout/focus.html.eex new file mode 100644 index 000000000..3abe2e267 --- /dev/null +++ b/lib/plausible_web/templates/layout/focus.html.eex @@ -0,0 +1,27 @@ + + + + + + + + "> + <%= assigns[:title] || "Plausible · Web analytics" %> + "/> + <%= render("_tracking.html", assigns) %> + + + <%= render("_svg_icons.html") %> + + + <%= render @view_module, @view_template, assigns %> + +

+ ©2019 Plausible Insights. All rights reserved. +

+ + diff --git a/lib/plausible_web/templates/page/contact_form.html.eex b/lib/plausible_web/templates/page/contact_form.html.eex new file mode 100644 index 000000000..f8ee188b7 --- /dev/null +++ b/lib/plausible_web/templates/page/contact_form.html.eex @@ -0,0 +1,16 @@ +
+ <%= form_for @conn, "/contact", [class: "bg-white shadow-md rounded px-8 py-6 mb-4 mt-16"], fn f -> %> +

Contact us

+

Please get in touch with any questions and thoughts, we'll get back to you as soon as possible!

+ <%= textarea f, :text, rows: 4, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:bg-white focus:border-grey-light text-xs mt-4" %> + <%= if @conn.assigns[:current_user] do %> + <%= hidden_input f, :email, value: @conn.assigns[:current_user].email %> + <% else %> +
+ <%= label f, :email, "Your email (optional)", class: "block text-grey-darker text-sm font-bold mb-2" %> + <%= email_input f, :email, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:bg-white focus:border-grey-light", placeholder: "example@email.com" %> +
+ <% end %> + <%= submit "Send", class: "button mt-4 w-full" %> + <% end %> +
diff --git a/lib/plausible_web/templates/page/contact_thanks.html.eex b/lib/plausible_web/templates/page/contact_thanks.html.eex new file mode 100644 index 000000000..71f511b0e --- /dev/null +++ b/lib/plausible_web/templates/page/contact_thanks.html.eex @@ -0,0 +1,6 @@ +
+
+

Message submitted!

+
+
Thank you for getting in touch.
+
diff --git a/lib/plausible_web/templates/page/data_policy.html.eex b/lib/plausible_web/templates/page/data_policy.html.eex new file mode 100644 index 000000000..90e794cf7 --- /dev/null +++ b/lib/plausible_web/templates/page/data_policy.html.eex @@ -0,0 +1,40 @@ +
+ +

Data policy

+ +
+Plausible aims to track overall trends in your website traffic, not indivual visitors. With privacy in mind, we don’t collect or store any data beyond what is absolutely necessary. Here is a list of what we collect and store about your website visitors. +
+ +
+

We don’t store IP addresses

+ We never store IP addresses in our database or logs. The IP address is used to look up the visitor country by Cloudflare, which we store to show the Top Countries report. +
+ +
+

We partially store the user-agent

+ We use the UserAgent header to figure out what browsers and operating systems your visitors are using. However, the raw user-agent string is discarded and we only store the browser and operating system information. +
+ +
+

We partially store the referrer

+ Plausible uses the referrer string to show which other websites are driving traffic to the site you’re tracking. We store the referrer but we discard all query parameters. +
+ +
+

We partially store the page URLs

+ We track the URL of each pageview to be able to show the Top Pages report. However, we do not store query strings from the URL. +
+ +
+

We store the screen width

+ We use window.innerWidth to determine the actual size of the open browser window, rather than the full width of the device. This is more accurate and makes fingerprinting harder. +
+ +
+

Cookie policy

+ + Plausible installs a first-party cooky to be able to differentiate between a new visitor and a returning visitor. This cookie contains a random identifier that is sent along with each pageview request. On the backend, this allows us to aggregate and show unique visitors and total pageviews separately. +
+ <%= link("Back to homepage", to: "/", class: "text-indigo") %> +
diff --git a/lib/plausible_web/templates/page/index.html.eex b/lib/plausible_web/templates/page/index.html.eex new file mode 100644 index 000000000..aaa03ffb1 --- /dev/null +++ b/lib/plausible_web/templates/page/index.html.eex @@ -0,0 +1,239 @@ +
+
+ +
+
+

+ Simple analytics for your website +

+

+ Plausible is a lightweight, non-intrusive alternative to Google Analytics +

+ <%= link("Start free trial", to: "/register", class: "button w-full sm:w-auto mt-6 mr-2") %> + <%= link("View live demo", to: "/plausible.io", class: "button button-outline w-full sm:w-auto mt-4 md:mt-0") %> +
+
+
+
+
+

Why Plausible?

+

+ Plausible is built by and for privacy-conscious minimalists
+ Here’s what makes it different from other solutions +

+
+
+
+
+ + + +

Clutter-free

+

+ Stop digging through complex reports to find what you’re looking for. Plausible presents the most important information to you on a single page. +

+
+ +
+ + + +

Anonymous

+

+ Measure traffic, not individuals. No personal data or IP addresses are ever stored in our database. <%= link("Read more about our data policy", to: "/data-policy", class: "text-indigo") %> +

+
+ +
+ + + +

Lightweight

+

+ Plausible works by loading a script on your website, like Google Analytics. Our script is 14x smaller, making your website quicker to load. +

+
+
+
+
+

Check out our analytics

+ <%= link("View live demo →", to: "/plausible.io", class: "button mt-6") %> +
+
+
+

Simple, traffic based pricing

+

Try Plausible free for 30 days

+
+
+ +
+
+
Pricing Plans
+
+
+
+ Track unlimited websites with all plans. Billing is based on the total monthly pageviews of the website(s) you're tracking +
+
+ Plan + Pageviews + Price +
+
+ Personal + 10k / mo + $6 / mo +
+
+ Startup + 100k / mo + $12 / mo +
+
+ Business + 1m / mo + $36 / mo +
+ <%= link("Start free trial →", to: "/register", class: "button mt-6") %> +
No credit card required up front. Cancel anytime.
+
+
+ + + +
+
+
+
+

What people are saying

+
+
+ + + +

Plausible is focused on exactly what I need: clear insights into my site visitors, without getting in my way

+ +
+ +
+ + + +

All the stats you need in a single page. Plausible's user experience and user interface are the stark opposite of Google Analytics.

+ +
+ +
+ + + +

I love Plausible because it is lightweight and looks beautiful. It shows me all the statistics that I need in a simple and unique style.

+
+ + <%= img_tag(PlausibleWeb.Router.Helpers.static_path(@conn, "/images/testimonials/markus.jpg"), class: "inline w-12 h-12 rounded-full mr-3") %> + +
+

Markus Schranz

+ <%= link("@ma_schranz", to: "https://twitter.com/ma_schranz", class: "text-sm text-grey-dark") %> +
+
+
+
+
+ +
+

Frequently asked questions

+ +
+

1. Can I use Plausible on more than one website?

+
+ Yes. You can add as many websites as you want under a single account. You will be charged for the total pageviews on all of your websites combined. For example, on the 10k plan you can either have 10 websites that each get 1000 pageviews per month, or one website that gets 10,000 pageviews per month. +
+
+ +
+

2. What happens if I go over my plan limit?

+
+ You don't have to pay for a one-time spike in traffic, but if you go over your plan limit for 2 months in a row, we will + contact you to discuss upgrade options. +
+
+ +
+

3. How do you make sure my visitors' privacy is protected?

+
+ We take painstaking care to not collect or store any personal information that could be tied back to the visitor. You own your data and we will never sell it or use it in nefarious ways. <%= link("Read more about our data policy", to: "/data-policy", class: "text-indigo") %>. +
+
+ +
+ <%= link("Ask a question from the founder", to: "mailto: uku@plausible.io", class: "text-indigo") %> +
+
+
diff --git a/lib/plausible_web/templates/page/privacy.html.eex b/lib/plausible_web/templates/page/privacy.html.eex new file mode 100644 index 000000000..bdb9c8d15 --- /dev/null +++ b/lib/plausible_web/templates/page/privacy.html.eex @@ -0,0 +1,11 @@ +
+

Privacy Policy

+

Your privacy is important to us. It is Plausible Insights' policy to respect your privacy regarding any information we may collect from you across our website, http://plausible.io, and other sites we own and operate.

+

We only ask for personal information when we truly need it to provide a service to you. We collect it by fair and lawful means, with your knowledge and consent. We also let you know why we’re collecting it and how it will be used.

+

We only retain collected information for as long as necessary to provide you with your requested service. What data we store, we’ll protect within commercially acceptable means to prevent loss and theft, as well as unauthorised access, disclosure, copying, use or modification.

+

We don’t share any personally identifying information publicly or with third-parties, except when required to by law.

+

Our website may link to external sites that are not operated by us. Please be aware that we have no control over the content and practices of these sites, and cannot accept responsibility or liability for their respective privacy policies.

+

You are free to refuse our request for your personal information, with the understanding that we may be unable to provide you with some of your desired services.

+

Your continued use of our website will be regarded as acceptance of our practices around privacy and personal information. If you have any questions about how we handle user data and personal information, feel free to contact us.

+

This policy is effective as of 25 January 2019.

+
diff --git a/lib/plausible_web/templates/page/sites.html.eex b/lib/plausible_web/templates/page/sites.html.eex new file mode 100644 index 000000000..cc70bbc2b --- /dev/null +++ b/lib/plausible_web/templates/page/sites.html.eex @@ -0,0 +1,22 @@ +
+
+

My websites

+ + Add a website +
+
+
+ <%= for site <- @sites do %> +
+ +

<%= site.domain %>

+
+ <%= link(to: "/#{site.domain}/settings", class: "flex absolute hover:bg-grey-lighter transition rounded py-3 px-5", style: "top: 6px; right: 6px;") do %> +
+ +
+

Settings

+ <% end %> +
+ <% end %> +
+
diff --git a/lib/plausible_web/templates/page/terms.html.eex b/lib/plausible_web/templates/page/terms.html.eex new file mode 100644 index 000000000..e6a2e60bc --- /dev/null +++ b/lib/plausible_web/templates/page/terms.html.eex @@ -0,0 +1,33 @@ +
+

Terms of Service

+

1. Terms

+

By accessing the website at http://plausible.io, you are agreeing to be bound by these terms of service, all applicable laws and regulations, and agree that you are responsible for compliance with any applicable local laws. If you do not agree with any of these terms, you are prohibited from using or accessing this site. The materials contained in this website are protected by applicable copyright and trademark law.

+

2. Use License

+
    +
  1. Permission is granted to temporarily download one copy of the materials (information or software) on Plausible Insights' website for personal, non-commercial transitory viewing only. This is the grant of a license, not a transfer of title, and under this license you may not: +
      +
    1. modify or copy the materials;
    2. +
    3. use the materials for any commercial purpose, or for any public display (commercial or non-commercial);
    4. +
    5. attempt to decompile or reverse engineer any software contained on Plausible Insights' website;
    6. +
    7. remove any copyright or other proprietary notations from the materials; or
    8. +
    9. transfer the materials to another person or "mirror" the materials on any other server.
    10. +
    +
  2. +
  3. This license shall automatically terminate if you violate any of these restrictions and may be terminated by Plausible Insights at any time. Upon terminating your viewing of these materials or upon the termination of this license, you must destroy any downloaded materials in your possession whether in electronic or printed format.
  4. +
+

3. Disclaimer

+
    +
  1. The materials on Plausible Insights' website are provided on an 'as is' basis. Plausible Insights makes no warranties, expressed or implied, and hereby disclaims and negates all other warranties including, without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights.
  2. +
  3. Further, Plausible Insights does not warrant or make any representations concerning the accuracy, likely results, or reliability of the use of the materials on its website or otherwise relating to such materials or on any sites linked to this site.
  4. +
+

4. Limitations

+

In no event shall Plausible Insights or its suppliers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption) arising out of the use or inability to use the materials on Plausible Insights' website, even if Plausible Insights or a Plausible Insights authorized representative has been notified orally or in writing of the possibility of such damage. Because some jurisdictions do not allow limitations on implied warranties, or limitations of liability for consequential or incidental damages, these limitations may not apply to you.

+

5. Accuracy of materials

+

The materials appearing on Plausible Insights' website could include technical, typographical, or photographic errors. Plausible Insights does not warrant that any of the materials on its website are accurate, complete or current. Plausible Insights may make changes to the materials contained on its website at any time without notice. However Plausible Insights does not make any commitment to update the materials.

+

6. Links

+

Plausible Insights has not reviewed all of the sites linked to its website and is not responsible for the contents of any such linked site. The inclusion of any link does not imply endorsement by Plausible Insights of the site. Use of any such linked website is at the user's own risk.

+

7. Modifications

+

Plausible Insights may revise these terms of service for its website at any time without notice. By using this website you are agreeing to be bound by the then current version of these terms of service.

+

8. Governing Law

+

These terms and conditions are governed by and construed in accordance with the laws of Estonia and you irrevocably submit to the exclusive jurisdiction of the courts in that State or location.

+
diff --git a/lib/plausible_web/templates/site/new.html.eex b/lib/plausible_web/templates/site/new.html.eex new file mode 100644 index 000000000..a2b79d428 --- /dev/null +++ b/lib/plausible_web/templates/site/new.html.eex @@ -0,0 +1,29 @@ +<%= form_for @changeset, "/sites", [class: "max-w-sm w-full mx-auto bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %> +
+

Your website details

+
+
+ <%= label f, :domain, class: "block text-grey-darker text-sm font-bold" %> +

Just the naked domain without www or https://

+ <%= text_input f, :domain, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:bg-white focus:border-grey-light", placeholder: "example.com" %> + <%= error_tag f, :domain %> +
+
+ <%= label f, :timezone, "Reporting Timezone", class: "block text-grey-darker text-sm font-bold mb-2" %> +

To make sure we agree on what 'today' means

+ +
+ <%= select f, :timezone, Plausible.Timezones.options(), id: "tz-select", selected: "Etc/Greenwich", class: "block appearance-none w-full bg-grey-lighter text-grey-darker cursor-pointer hover:border-grey p-2 pr-8 rounded leading-normal focus:outline-none" %> +
+ +
+
+
+ + + <%= submit "Add snippet →", class: "button mt-4 w-full" %> +<% end %> diff --git a/lib/plausible_web/templates/site/settings.html.eex b/lib/plausible_web/templates/site/settings.html.eex new file mode 100644 index 000000000..2db8a33fc --- /dev/null +++ b/lib/plausible_web/templates/site/settings.html.eex @@ -0,0 +1,125 @@ +
+ +
+
+

General

+
+ +
+ + <%= form_for @changeset, "/#{@site.domain}/settings", [class: "max-w-xs"], fn f -> %> +
+ <%= label f, :domain, class: "block text-grey-darker text-sm font-bold mb-2" %> + <%= text_input f, :domain, class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:border-grey-light ", disabled: "disabled" %> + <%= error_tag f, :domain %> +
+
+ <%= label f, :timezone, "Reporting Timezone", class: "block text-grey-darker text-sm font-bold mb-2" %> +
+ <%= select f, :timezone, Plausible.Timezones.options(), class: "block appearance-none w-full bg-grey-lighter text-grey-darker cursor-pointer hover:border-grey p-2 pr-8 rounded leading-normal focus:outline-none" %> +
+ +
+
+
+ <%= submit "Save changes", class: "button mt-4" %> + <% end %> +
+ +
+
+

Visibility

+
+ +
+ <%= if @site.public do %> + Stats for <%= @site.domain %> are currently public. Anyone with the following link can view the stats: + + <%= button("Make stats private", to: "/sites/#{@site.domain}/make-private", method: "POST", class: "button mt-8") %> + <% else %> +
+ Stats for <%= @site.domain %> are currently private. You are the only person who can see them. + If you choose to make your stats public, anyone with the link will be able to view them. +
+ <%= button("Make stats public", to: "/sites/#{@site.domain}/make-public", method: "POST", class: "button mt-8") %> + <% end %> +
+ +
+
+

Google Integration

+
+ +
+
+ Integrating with your Google account allows Plausible to show more information about your websites' performance on their search engine. +
+ + <%= if @site.google_auth do %> +
+ + + + Connected Google account: <%= @site.google_auth.email %> + <%= if @google_search_console_verified do %> +
+ + + + Verified access to site https://<%= @site.domain %> +
+ <%= link("View google stats", to: "/#{@site.domain}/referrers/Google", class: "text-indigo") %> +
+ <% else %> +
+ + + + No access to site https://<%= @site.domain %> +
+ <%= link("Verify your site on Google Search Console to continue", to: "https://support.google.com/webmasters/answer/34592", class: "text-indigo") %> +
+ <% end %> + <% else %> + <%= button("Continue with Google", to: Plausible.Google.Api.authorize_url(@site.id), class: "button mt-4") %> + +
+ NB: You also need to set up your site on <%= link("Google Search Console", to: "https://search.google.com/search-console/about") %> for the integration to work. +
+ <% end %> +
+ +<%= form_for @conn, "/", [class: "bg-white max-w-md mx-auto shadow-md rounded rounded-t-none border-t-2 border-indigo-lightest px-8 pt-6 pb-8 mb-4 mt-16"], fn f -> %> +
+

Javascript snippet

+
+
+

Include this snippet in the <head> of your website.

+
+ <%= textarea f, :domain, id: "snippet_code", class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:bg-white focus:border-grey-light text-xs mt-2 resize-none", value: snippet(), rows: 9 %> + + + +
+
+ Is your website a single-page application? + <%= link("Read the docs", class: "text-indigo hover:underline", to: "https://docs.plausible.io/single-page-application-support", target: "_blank") %> +
+
+<% end %> + +
+
+

Delete site data

+
+ +
+ +

Deleting the site removes all stats you've collected

+ <%= link "Delete #{@site.domain}", to: "/#{@site.domain}", method: :delete, class: "button bg-red-dark mt-4", data: [confirm: "Deleting the site data cannot be reversed. Are you sure?"] %> +
diff --git a/lib/plausible_web/templates/site/snippet.html.eex b/lib/plausible_web/templates/site/snippet.html.eex new file mode 100644 index 000000000..5d02fe4a5 --- /dev/null +++ b/lib/plausible_web/templates/site/snippet.html.eex @@ -0,0 +1,19 @@ +<%= form_for @conn, "/", [class: "max-w-sm w-full mx-auto bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %> +
+

Add javascript snippet

+
+
+

Paste this snippet in the <head> of your website.

+
+ <%= textarea f, :domain, id: "snippet_code", class: "transition bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:bg-white focus:border-grey-light text-xs mt-4 resize-none", value: snippet(), rows: 9 %> + + + +
+
+ Is your website a single-page application? + <%= link("Read the docs", class: "text-indigo hover:underline", to: "https://docs.plausible.io/single-page-application-support", target: "_blank") %> +
+
+ <%= link("Start collecting data →", class: "button mt-4 w-full", to: "/#{@site.domain}") %> +<% end %> diff --git a/lib/plausible_web/templates/stats/_bar_graph.html.eex b/lib/plausible_web/templates/stats/_bar_graph.html.eex new file mode 100644 index 000000000..2d5373db3 --- /dev/null +++ b/lib/plausible_web/templates/stats/_bar_graph.html.eex @@ -0,0 +1,29 @@ +
+
+

<%= @title %>

+
by <%= @by %>
+
+ +
+ <%= for {key, count} <- @list do %> +
+ <%= case key do %> + <% {:link, name, to} -> %> + <%= link(name, to: to, "data-pushstate": true, class: "hover:underline") %> + <% key -> %> + <%= key %> + <% end %> + <%= count %> +
+ <%= bar(count, @list, @color) %> + <% end %> +
+ <%= if Enum.count(@list) >= 5 do %> + + <% end %> +
diff --git a/lib/plausible_web/templates/stats/_graph.html.eex b/lib/plausible_web/templates/stats/_graph.html.eex new file mode 100644 index 000000000..b55ea083e --- /dev/null +++ b/lib/plausible_web/templates/stats/_graph.html.eex @@ -0,0 +1,134 @@ +
+
+ <%= @pageviews %> Pageviews<%= @unique_visitors %> Unique visitors +
+
+ + + +
+
diff --git a/lib/plausible_web/templates/stats/browsers.html.eex b/lib/plausible_web/templates/stats/browsers.html.eex new file mode 100644 index 000000000..c998c3b8d --- /dev/null +++ b/lib/plausible_web/templates/stats/browsers.html.eex @@ -0,0 +1,17 @@ + +
by visitors
+
+
+
+ <%= for {browser, count} <- @browsers do %> +
+ <%= browser %> + <%= large_number_format(count) %> +
+ <%= bar(count, @browsers, :red) %> + <% end %> +
+
diff --git a/lib/plausible_web/templates/stats/browsers_preview.html.eex b/lib/plausible_web/templates/stats/browsers_preview.html.eex new file mode 100644 index 000000000..afd88614c --- /dev/null +++ b/lib/plausible_web/templates/stats/browsers_preview.html.eex @@ -0,0 +1,22 @@ +
+

Browsers

+
by visitors
+
+ +
+ <%= for {key, count} <- @browsers do %> +
+ <%= key %> + <%= large_number_format(count) %> +
+ <%= bar(count, @browsers, :red) %> + <% end %> +
+<%= if Enum.count(@browsers) >= 5 do %> + +<% end %> diff --git a/lib/plausible_web/templates/stats/countries.html.eex b/lib/plausible_web/templates/stats/countries.html.eex new file mode 100644 index 000000000..03a1356cd --- /dev/null +++ b/lib/plausible_web/templates/stats/countries.html.eex @@ -0,0 +1,17 @@ + +
by visitors
+
+
+
+ <%= for {country, count} <- @countries do %> +
+ <%= Plausible.Stats.CountryName.from_iso3166(country) %> + <%= large_number_format(count) %> +
+ <%= bar(count, @countries, :indigo) %> + <% end %> +
+
diff --git a/lib/plausible_web/templates/stats/countries_preview.html.eex b/lib/plausible_web/templates/stats/countries_preview.html.eex new file mode 100644 index 000000000..80b430fc6 --- /dev/null +++ b/lib/plausible_web/templates/stats/countries_preview.html.eex @@ -0,0 +1,22 @@ +
+

Top Countries

+
by visitors
+
+ +
+ <%= for {key, count} <- @top_countries do %> +
+ <%= key %> + <%= large_number_format(count) %> +
+ <%= bar(count, @top_countries, :indigo) %> + <% end %> +
+<%= if Enum.count(@top_countries) >= 5 do %> + +<% end %> diff --git a/lib/plausible_web/templates/stats/google_referrer.html.eex b/lib/plausible_web/templates/stats/google_referrer.html.eex new file mode 100644 index 000000000..fff84e45e --- /dev/null +++ b/lib/plausible_web/templates/stats/google_referrer.html.eex @@ -0,0 +1,33 @@ + +
+
+

<%= @total_visitors %> new visitors from Google

+

<%= timeframe_to_human(@query) %>

+ <%= case @search_terms do %> + <% {:ok, search_terms} -> %> +
+ Search term + Visitors +
+ <%= for {term, count} <- search_terms do %> +
+ <%= term %> + <%= count %> +
+ <%= bar(count, search_terms) %> + <% end %> + <% {:error, msg} -> %> +
Unable to show search terms. Google Search Console API returned the following error:
+
<%= msg %>
+ <% nil -> %> + <%= if @conn.assigns[:current_user] && Plausible.Sites.is_owner?(@conn.assigns[:current_user].id, @site) do %> +
+ You can link your Google account to see the search terms that lead to your website. +
+ <%= link("Connect with Google", to: "/#{@site.domain}/settings#google-auth", class: "button mt-4") %> + <% end %> + <% end %> +
diff --git a/lib/plausible_web/templates/stats/operating_systems.html.eex b/lib/plausible_web/templates/stats/operating_systems.html.eex new file mode 100644 index 000000000..6e4a64e96 --- /dev/null +++ b/lib/plausible_web/templates/stats/operating_systems.html.eex @@ -0,0 +1,17 @@ + +
by visitors
+
+
+
+ <%= for {os, count} <- @operating_systems do %> +
+ <%= os %> + <%= count %> +
+ <%= bar(count, @operating_systems, :blue) %> + <% end %> +
+
diff --git a/lib/plausible_web/templates/stats/operating_systems_preview.html.eex b/lib/plausible_web/templates/stats/operating_systems_preview.html.eex new file mode 100644 index 000000000..196cb1c3c --- /dev/null +++ b/lib/plausible_web/templates/stats/operating_systems_preview.html.eex @@ -0,0 +1,22 @@ +
+

Operating Systems

+
by visitors
+
+ +
+ <%= for {key, count} <- @operating_systems do %> +
+ <%= key %> + <%= large_number_format(count) %> +
+ <%= bar(count, @operating_systems, :blue) %> + <% end %> +
+<%= if Enum.count(@operating_systems) >= 5 do %> + +<% end %> diff --git a/lib/plausible_web/templates/stats/pages.html.eex b/lib/plausible_web/templates/stats/pages.html.eex new file mode 100644 index 000000000..e2a2dfbc2 --- /dev/null +++ b/lib/plausible_web/templates/stats/pages.html.eex @@ -0,0 +1,17 @@ + +
by pageviews
+
+
+
+ <%= for {page, count} <- @top_pages do %> +
+ <%= page %> + <%= large_number_format(count) %> +
+ <%= bar(count, @top_pages, :orange) %> + <% end %> +
+
diff --git a/lib/plausible_web/templates/stats/pages_preview.html.eex b/lib/plausible_web/templates/stats/pages_preview.html.eex new file mode 100644 index 000000000..70898df29 --- /dev/null +++ b/lib/plausible_web/templates/stats/pages_preview.html.eex @@ -0,0 +1,22 @@ +
+

Top Pages

+
by pageviews
+
+ +
+ <%= for {key, count} <- @top_pages do %> +
+ <%= key %> + <%= large_number_format(count) %> +
+ <%= bar(count, @top_pages, :orange) %> + <% end %> +
+<%= if Enum.count(@top_pages) >= 5 do %> + +<% end %> diff --git a/lib/plausible_web/templates/stats/referrer_drilldown.html.eex b/lib/plausible_web/templates/stats/referrer_drilldown.html.eex new file mode 100644 index 000000000..02f3d2c59 --- /dev/null +++ b/lib/plausible_web/templates/stats/referrer_drilldown.html.eex @@ -0,0 +1,22 @@ + +
+
+

<%= @total_visitors %> new visitors from <%= @referrer %>

+

<%= timeframe_to_human(@query) %>

+
+ <%= for {one_referrer, count} <- @referrers do %> +
+ <%= if one_referrer do %> + <%= link(one_referrer, to: "//" <> one_referrer, target: "_blank", class: "hover:underline") %> + <% else %> + unknown + <% end %> + <%= large_number_format(count) %> +
+ <%= bar(count, @referrers) %> + <% end %> +
+
diff --git a/lib/plausible_web/templates/stats/referrers.html.eex b/lib/plausible_web/templates/stats/referrers.html.eex new file mode 100644 index 000000000..4af892957 --- /dev/null +++ b/lib/plausible_web/templates/stats/referrers.html.eex @@ -0,0 +1,17 @@ + +
by new visitors
+
+
+
+ <%= for {referrer, count} <- @top_referrers do %> +
+ <%= link(referrer, to: "/#{@site.domain}/referrers/#{referrer}" <> query_params(@query), "data-pushstate": true, class: "hover:underline") %> + <%= large_number_format(count) %> +
+ <%= bar(count, @top_referrers) %> + <% end %> +
+
diff --git a/lib/plausible_web/templates/stats/referrers_preview.html.eex b/lib/plausible_web/templates/stats/referrers_preview.html.eex new file mode 100644 index 000000000..9bc431b6b --- /dev/null +++ b/lib/plausible_web/templates/stats/referrers_preview.html.eex @@ -0,0 +1,27 @@ +
+

Top Referrers

+
by new visitors
+
+ +
+ <%= for {key, count} <- @top_referrers do %> +
+ <%= case key do %> + <% {:link, name, to} -> %> + <%= link(name, to: to, "data-pushstate": true, class: "hover:underline") %> + <% key -> %> + <%= key %> + <% end %> + <%= large_number_format(count) %> +
+ <%= bar(count, @top_referrers, :blue) %> + <% end %> +
+<%= if Enum.count(@top_referrers) >= 5 do %> + +<% end %> diff --git a/lib/plausible_web/templates/stats/screen_sizes_preview.html.eex b/lib/plausible_web/templates/stats/screen_sizes_preview.html.eex new file mode 100644 index 000000000..775e9662b --- /dev/null +++ b/lib/plausible_web/templates/stats/screen_sizes_preview.html.eex @@ -0,0 +1,16 @@ +
+

Screen Sizes

+
by visitors
+
+
+ <%= for {screen_size, count} <- @top_screen_sizes do %> +
+ + <%= icon_for(screen_size) %> + <%= screen_size %> + + <%= large_number_format(count) %> +
+ <%= bar(count, @top_screen_sizes, :teal) %> + <% end %> +
diff --git a/lib/plausible_web/templates/stats/stats.html.eex b/lib/plausible_web/templates/stats/stats.html.eex new file mode 100644 index 000000000..8e376182b --- /dev/null +++ b/lib/plausible_web/templates/stats/stats.html.eex @@ -0,0 +1,93 @@ +
+
+
+

Analytics for <%= link(@site.domain, to: "//" <> @site.domain, target: "_blank") %>

+
+ + + +
+ <%= if @query.period == "month" do %> +
+ <%= link(to: "/#{@site.domain}?period=month&date=#{@query.date_range.first |> Timex.shift(months: -1) |> Timex.format!("{ISOdate}")}", class: "flex items-center px-2 border-r border-grey-light") do %> + + + + <% end %> + <%= link(to: "/#{@site.domain}?period=month&date=#{@query.date_range.first |> Timex.shift(months: 1) |> Timex.format!("{ISOdate}")}", class: "flex items-center px-2") do %> + + + + <% end %> +
+ <% end %> + + <%= if @query.period == "day" do %> +
+ <%= link(to: "/#{@site.domain}?period=day&date=#{@query.date_range.first |> Timex.shift(days: -1) |> Timex.format!("{ISOdate}")}", class: "flex items-center px-2 border-r border-grey-light") do %> + + + + <% end %> + <%= link(to: "/#{@site.domain}?period=day&date=#{@query.date_range.first |> Timex.shift(days: 1) |> Timex.format!("{ISOdate}")}", class: "flex items-center px-2") do %> + + + + <% end %> +
+ <% end %> + +
+
+ <%= timeframe_text(@query) %> + + + +
+ + +
+
+ +
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/plausible_web/templates/stats/waiting_first_pageview.html.eex b/lib/plausible_web/templates/stats/waiting_first_pageview.html.eex new file mode 100644 index 000000000..16d42bd40 --- /dev/null +++ b/lib/plausible_web/templates/stats/waiting_first_pageview.html.eex @@ -0,0 +1,28 @@ + + + +
+
+

Waiting for first pageview

+

on <%= @site.domain %>

+
+
+

+ Need to see the snippet again? <%= link("Click here", to: "/#{@site.domain}/snippet")%>
+ Not working? Contact uku@plausible.io to get set up +

+
+
+
diff --git a/lib/plausible_web/views/auth_view.ex b/lib/plausible_web/views/auth_view.ex new file mode 100644 index 000000000..292439b8f --- /dev/null +++ b/lib/plausible_web/views/auth_view.ex @@ -0,0 +1,31 @@ +defmodule PlausibleWeb.AuthView do + use PlausibleWeb, :view + + @subscription_names %{ + "558018" => "Personal", + "558745" => "Startup", + "558746" => "Business", + "558156" => "Personal (T)", + "558199" => "Startup (T)", + "558200" => "Business (T)" + } + + def subscription_name(subscription) do + @subscription_names[subscription.paddle_plan_id] + end + + def delimit_integer(number) do + Integer.to_charlist(number) + |> :lists.reverse() + |> delimit_integer([]) + |> String.Chars.to_string() + end + + defp delimit_integer([a, b, c, d | tail], acc) do + delimit_integer([d | tail], [",", c, b, a | acc]) + end + + defp delimit_integer(list, acc) do + :lists.reverse(list) ++ acc + end +end diff --git a/lib/plausible_web/views/billing_view.ex b/lib/plausible_web/views/billing_view.ex new file mode 100644 index 000000000..d3fe4a254 --- /dev/null +++ b/lib/plausible_web/views/billing_view.ex @@ -0,0 +1,16 @@ +defmodule PlausibleWeb.BillingView do + use PlausibleWeb, :view + + def reccommended_plan(usage) do + cond do + usage < 9000 -> + "10k / mo" + usage < 90_000 -> + "100k / mo" + usage < 900_000 -> + "1m / mo" + true -> + "custom" + end + end +end diff --git a/lib/plausible_web/views/email_view.ex b/lib/plausible_web/views/email_view.ex new file mode 100644 index 000000000..38962ea2e --- /dev/null +++ b/lib/plausible_web/views/email_view.ex @@ -0,0 +1,39 @@ +defmodule PlausibleWeb.EmailView do + use PlausibleWeb, :view + + def user_salutation(user) do + if user.name do + String.split(user.name) |> List.first + else + "" + end + end + + def suggested_plan_name(usage) do + cond do + usage < 9_000 -> + "Personal" + usage < 90_000 -> + "Startup" + usage < 900_000 -> + "Business" + true -> + throw "Huge account" + + end + end + + def suggested_plan_cost(usage) do + cond do + usage < 9_000 -> + "$6/mo" + usage < 90_000 -> + "$12/mo" + usage < 900_000 -> + "$32/mo" + true -> + throw "Huge account" + + end + end +end diff --git a/lib/plausible_web/views/error_helpers.ex b/lib/plausible_web/views/error_helpers.ex new file mode 100644 index 000000000..044b294c5 --- /dev/null +++ b/lib/plausible_web/views/error_helpers.ex @@ -0,0 +1,9 @@ +defmodule PlausibleWeb.ErrorHelpers do + use Phoenix.HTML + + def error_tag(form, field) do + Enum.map(Keyword.get_values(form.errors, field), fn error -> + content_tag(:div, elem(error, 0), class: "text-red text-xs italic mt-3") + end) + end +end diff --git a/lib/plausible_web/views/error_view.ex b/lib/plausible_web/views/error_view.ex new file mode 100644 index 000000000..c704b404a --- /dev/null +++ b/lib/plausible_web/views/error_view.ex @@ -0,0 +1,29 @@ +defmodule PlausibleWeb.ErrorView do + use PlausibleWeb, :view + + def render("404.html", assigns) do + render("error.html", Map.merge(%{ + layout: false, + status: 404, + message: "Oops! There's nothing here" + }, assigns)) + end + + def render("500.html", assigns) do + render("error.html", Map.merge(%{ + layout: false, + status: 500, + message: "Oops! Looks like we're having server issues" + }, assigns)) + end + + def template_not_found(template, assigns) do + status = String.trim_trailing(template, ".html") + + render("error.html", Map.merge(%{ + layout: false, + status: status, + message: Phoenix.Controller.status_message_from_template(template) + }, assigns)) + end +end diff --git a/lib/plausible_web/views/layout_view.ex b/lib/plausible_web/views/layout_view.ex new file mode 100644 index 000000000..7e7699b11 --- /dev/null +++ b/lib/plausible_web/views/layout_view.ex @@ -0,0 +1,3 @@ +defmodule PlausibleWeb.LayoutView do + use PlausibleWeb, :view +end diff --git a/lib/plausible_web/views/page_view.ex b/lib/plausible_web/views/page_view.ex new file mode 100644 index 000000000..c4ace790b --- /dev/null +++ b/lib/plausible_web/views/page_view.ex @@ -0,0 +1,3 @@ +defmodule PlausibleWeb.PageView do + use PlausibleWeb, :view +end diff --git a/lib/plausible_web/views/site_view.ex b/lib/plausible_web/views/site_view.ex new file mode 100644 index 000000000..3e824e555 --- /dev/null +++ b/lib/plausible_web/views/site_view.ex @@ -0,0 +1,17 @@ +defmodule PlausibleWeb.SiteView do + use PlausibleWeb, :view + + def snippet() do + """ + \ + """ + end +end diff --git a/lib/plausible_web/views/stats_view.ex b/lib/plausible_web/views/stats_view.ex new file mode 100644 index 000000000..a65f62ee3 --- /dev/null +++ b/lib/plausible_web/views/stats_view.ex @@ -0,0 +1,131 @@ +defmodule PlausibleWeb.StatsView do + use PlausibleWeb, :view + + def large_number_format(n) do + cond do + n >= 1_000 && n < 1_000_000 -> + thousands = trunc(n / 100) / 10 + if thousands == trunc(thousands) || n >= 100_000 do + "#{trunc(thousands)}k" + else + "#{thousands}k" + end + n >= 1_000_000 && n < 100_000_000 -> + millions = trunc(n / 100_000) / 10 + if millions == trunc(millions) do + "#{trunc(millions)}m" + else + "#{millions}m" + end + true -> + Integer.to_string(n) + end + end + + def bar(count, all, color \\ :blue) do + ~E""" +
+
+
+ """ + end + + def timeframe_to_human(query) do + case query.period do + "day" -> + "on #{Timex.format!(query.date_range.first, "{Mfull} {D}")}" + "month" -> + "in #{Timex.format!(query.date_range.first, "{Mfull} {YYYY}")}" + "3mo" -> + "in the last 3 months" + "6mo" -> + "in the last 6 months" + end + end + + def query_params(query) do + case query.period do + "day" -> + date = Date.to_iso8601(query.date_range.first) + "?period=day&date=#{date}" + "month" -> + date = Date.to_iso8601(query.date_range.first) + "?period=month&date=#{date}" + "3mo" -> + "?period=3mo" + "6mo" -> + "?period=6mo" + end + end + + def this_month(site) do + Timex.now(site.timezone) + |> DateTime.to_date + |> Timex.beginning_of_month + end + + def last_month(site) do + this_month(site) + |> Timex.shift(months: -1) + end + + def timeframe_text(query) do + case query.period do + "6mo" -> + "Last 6 months" + "3mo" -> + "Last 3 months" + "month" -> + Timex.format!(query.date_range.first, "{Mfull} {YYYY}") + "day" -> + Timex.format!(query.date_range.first, "{D} {Mfull} {YYYY}") + _ -> + "wat" + end + end + + defp bar_width(count, all) do + max = Enum.max_by(all, fn {_, count} -> count end) |> elem(1) + count / max * 100 + end + + def icon_for("Mobile") do + ~E""" + + """ + end + + def icon_for("Tablet") do + ~E""" + + """ + end + + def icon_for("Laptop") do + ~E""" + + """ + end + + def icon_for("Desktop") do + ~E""" + + """ + end + + def explanation_for("Mobile") do + "up to 576px" + end + + def explanation_for("Tablet") do + "576px to 992px" + end + + def explanation_for("Laptop") do + "992px to 1440px" + end + + def explanation_for("Desktop") do + "above 1440px" + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 000000000..c1bc0af0f --- /dev/null +++ b/mix.exs @@ -0,0 +1,78 @@ +defmodule Plausible.MixProject do + use Mix.Project + + def project do + [ + app: :plausible, + version: "0.1.0", + elixir: "~> 1.5", + elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), + start_permanent: Mix.env() == :prod, + aliases: aliases(), + deps: deps() + ] + end + + # Configuration for the OTP application. + # + # Type `mix help compile.app` for more information. + def application do + [ + mod: {Plausible.Application, []}, + extra_applications: [:logger, :sentry, :runtime_tools, :timex, :ua_inspector, :ref_inspector, :bamboo] + ] + end + + # Specifies which paths to compile per environment. + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + + # Specifies your project dependencies. + # + # Type `mix help deps` for examples and options. + defp deps do + [ + {:browser, "~> 0.4.3"}, + {:bcrypt_elixir, "~> 2.0"}, + {:cors_plug, "~> 1.5"}, + {:ecto_sql, "~> 3.0"}, + {:elixir_uuid, "~> 1.2"}, + {:gettext, "~> 0.11"}, + {:jason, "~> 1.0"}, + {:phoenix, "~> 1.4.0"}, + {:phoenix_active_link, "~> 0.2.1"}, + {:phoenix_ecto, "~> 4.0"}, + {:phoenix_html, "~> 2.11"}, + {:phoenix_live_reload, "~> 1.2", only: :dev}, + {:phoenix_pubsub, "~> 1.1"}, + {:plug_cowboy, "~> 2.0"}, + {:postgrex, ">= 0.0.0"}, + {:ref_inspector, "~> 1.0"}, + {:timex, "~> 3.5"}, + {:ua_inspector, "~> 0.18"}, + {:bamboo, "~> 1.2"}, + {:bamboo_postmark, "~> 0.5"}, + {:poison, ">= 1.5.0"}, # For bamboo_postmark + {:sentry, "~> 7.0"}, + {:httpoison, "~> 1.4"}, + {:ex_machina, "~> 2.3", only: :test}, + {:appsignal, "~> 1.0"}, + {:joken, "~> 2.0"}, + {:php_serializer, "~> 0.9.0"} + ] + end + + # Aliases are shortcuts or tasks specific to the current project. + # For example, to create, migrate and run the seeds file at once: + # + # $ mix ecto.setup + # + # See the documentation for `Mix` for more info on aliases. + defp aliases do + [ + "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], + "ecto.reset": ["ecto.drop", "ecto.setup"], + test: ["ecto.create --quiet", "ecto.migrate", "test"] + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 000000000..02da57b55 --- /dev/null +++ b/mix.lock @@ -0,0 +1,66 @@ +%{ + "appsignal": {:hex, :appsignal, "1.10.6", "3b84ab3a3f7390a4295d883e0e5a3615d57161c2f8eec1957ca6fef193263010", [:make, :mix], [{:decorator, "~> 1.2.3", [hex: :decorator, repo: "hexpm", optional: false]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, ">= 1.3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "bamboo": {:hex, :bamboo, "1.2.0", "8aebd24f7c606c32d0163c398004a11608ca1028182a169b2e527793bfab7561", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, + "bamboo_postmark": {:hex, :bamboo_postmark, "0.5.0", "a0f238fd19cd178bc503dd8c40b46819b9cc57472c274a5e5bb3d9c0d7444cff", [:mix], [{:bamboo, ">= 1.2.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:hackney, ">= 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, + "bcrypt_elixir": {:hex, :bcrypt_elixir, "2.0.1", "1061e2114aaac554c12e5c1e4608bf4aadaca839f30d1b85224272facd5e6427", [:make, :mix], [{:comeonin, "~> 5.1", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, + "browser": {:hex, :browser, "0.4.3", "e6ec846464d11dcd24f3ff34e891f995637c85fa77a1452f26b8ec58e990bc35", [:mix], [{:plug, "~> 1.2", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"}, + "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, + "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, + "comeonin": {:hex, :comeonin, "5.1.1", "0abd6bae41acc01c369bb3eafe46399f301bf4e1bacebafdb89252bbb8a1a32d", [:mix], [], "hexpm"}, + "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, + "cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "cowboy": {:hex, :cowboy, "2.6.1", "f2e06f757c337b3b311f9437e6e072b678fcd71545a7b2865bdaa154d078593f", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, + "cowlib": {:hex, :cowlib, "2.7.0", "3ef16e77562f9855a2605900cedb15c1462d76fb1be6a32fc3ae91973ee543d2", [:rebar3], [], "hexpm"}, + "db_connection": {:hex, :db_connection, "2.0.6", "bde2f85d047969c5b5800cb8f4b3ed6316c8cb11487afedac4aa5f93fd39abfa", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, + "decimal": {:hex, :decimal, "1.7.0", "30d6b52c88541f9a66637359ddf85016df9eb266170d53105f02e4a67e00c5aa", [:mix], [], "hexpm"}, + "decorator": {:hex, :decorator, "1.2.4", "31dfff6143d37f0b68d0bffb3b9f18ace14fea54d4f1b5e4f86ead6f00d9ff6e", [:mix], [], "hexpm"}, + "ecto": {:hex, :ecto, "3.0.8", "9eb6a1fcfc593e6619d45ef51afe607f1554c21ca188a1cd48eecc27223567f1", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, + "ecto_sql": {:hex, :ecto_sql, "3.0.5", "7e44172b4f7aca4469f38d7f6a3da394dbf43a1bcf0ca975e958cb957becd74e", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.6", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, + "elixir_make": {:hex, :elixir_make, "0.5.2", "96a28c79f5b8d34879cd95ebc04d2a0d678cfbbd3e74c43cb63a76adf0ee8054", [:mix], [], "hexpm"}, + "elixir_uuid": {:hex, :elixir_uuid, "1.2.0", "ff26e938f95830b1db152cb6e594d711c10c02c6391236900ddd070a6b01271d", [:mix], [], "hexpm"}, + "ex_crypto": {:hex, :ex_crypto, "0.10.0", "af600a89b784b36613a989da6e998c1b200ff1214c3cfbaf8deca4aa2f0a1739", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"}, + "file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"}, + "gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"}, + "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "httpoison": {:hex, :httpoison, "1.5.0", "71ae9f304bdf7f00e9cd1823f275c955bdfc68282bc5eb5c85c3a9ade865d68e", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, + "joken": {:hex, :joken, "2.0.1", "ec9ab31bf660f343380da033b3316855197c8d4c6ef597fa3fcb451b326beb14", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"}, + "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, + "jsone": {:git, "https://github.com/sile/jsone.git", "b0b037df7c1a8ee99c9f1bd124cd72e550a87936", [tag: "1.4.5"]}, + "liburi": {:git, "https://github.com/silviucpp/liburi.git", "1f4033fbff1ec3efb55fbf296087e7ed42a64916", [ref: "1f4033fbff1ec3efb55fbf296087e7ed42a64916"]}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, + "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, + "phoenix": {:hex, :phoenix, "1.4.0", "56fe9a809e0e735f3e3b9b31c1b749d4b436e466d8da627b8d82f90eaae714d2", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, + "phoenix_active_link": {:hex, :phoenix_active_link, "0.2.1", "2f51a08c24872350fb6da33e49c489f2dd37ffd22e912254f6e2d777b25df228", [:mix], [{:phoenix_html, "~> 2.10", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "phoenix_html": {:hex, :phoenix_html, "2.13.1", "fa8f034b5328e2dfa0e4131b5569379003f34bc1fafdaa84985b0b9d2f12e68b", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.0", "3bb31a9fbd40ffe8652e60c8660dffd72dd231efcdf49b744fb75b9ef7db5dd2", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.1", "6668d787e602981f24f17a5fbb69cc98f8ab085114ebfac6cc36e10a90c8e93c", [:mix], [], "hexpm"}, + "php_serializer": {:hex, :php_serializer, "0.9.2", "59c5fd6bd3096671fd89358fb8229341ac7423b50ad8d45a15213b02ea2edab2", [:mix], [], "hexpm"}, + "plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.0.1", "d798f8ee5acc86b7d42dbe4450b8b0dadf665ce588236eb0a751a132417a980e", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, + "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, + "postgrex": {:hex, :postgrex, "0.14.1", "63247d4a5ad6b9de57a0bac5d807e1c32d41e39c04b8a4156a26c63bcd8a2e49", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, + "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, + "rebar_vsn_plugin": {:git, "https://github.com/erlware/rebar_vsn_plugin.git", "fd40c960c7912193631d948fe962e1162a8d1334", [branch: "master"]}, + "ref_inspector": {:hex, :ref_inspector, "1.0.0", "695b99b273caab9bee017cd3e2cdfdfa19fe117bf1547017caf54c0506b70b5e", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm"}, + "referrerparser": {:git, "https://github.com/silviucpp/refererparser.git", "3e0efdd5da6588972552bffb4bd6f1ccf7faadfd", [branch: "master"]}, + "sentry": {:hex, :sentry, "7.0.3", "093fa4b6937760afb9a5fcb0e4a9092a305b6c0ff26a710e977614b201feab75", [:mix], [{:hackney, "~> 1.8 or 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, + "telemetry": {:hex, :telemetry, "0.3.0", "099a7f3ce31e4780f971b4630a3c22ec66d22208bc090fe33a2a3a6a67754a73", [:rebar3], [], "hexpm"}, + "timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, + "tzdata": {:hex, :tzdata, "0.5.21", "8cbf3607fcce69636c672d5be2bbb08687fe26639a62bdcc283d267277db7cf0", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "ua_inspector": {:hex, :ua_inspector, "0.19.1", "cb5d894b16ca60bd88bae09a90ba344f6f7346c22238312b2f92d7a10b82c52c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.0", [hex: :poolboy, repo: "hexpm", optional: false]}, {:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm"}, + "ua_parser": {:hex, :ua_parser, "1.5.0", "a6964fd35b6d79ab3860ca83709274cb20fc533c4638d276fc356f7e8f0a5d68", [:mix], [{:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm"}, + "ua_parser2": {:git, "https://github.com/nazipov/ua_parser2-elixir.git", "aabd8dce449726e1e4fe825f148ed5c0c8b806ed", []}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, + "yamerl": {:hex, :yamerl, "0.7.0", "e51dba652dce74c20a88294130b48051ebbbb0be7d76f22de064f0f3ccf0aaf5", [:rebar3], [], "hexpm"}, + "yaml_elixir": {:hex, :yaml_elixir, "1.0.0", "a0b703148aa8926a08fc14619c83e9cdf7555ff7e8bb9e74c08aa716eaf8c609", [:mix], [], "hexpm"}, +} diff --git a/priv/paddle.pem b/priv/paddle.pem new file mode 100644 index 000000000..110be6cc1 --- /dev/null +++ b/priv/paddle.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvn1//X4/nkeKtF8aFPi5 +1ngjAvb68oGnzBmft0dOUxl6t4wSEl93efWI9xC24qPDGxippROPqRlelUne96zO +ZdAj3JZT8K77m0lS/EbtHBF6GSvETSr/UceOcF//pbG8Q7EQDN3+/VfiTlHpzG/s +V9gaOGNYJ28dlaS7gNQZYO+eeoWA92RLLk7X73F/lPQtJ70rxop+kLyAA4fDr08k +TR4gWBAyAkBsyC8W0v9b0zHBqxiiGdS2wQRWzMGrPHti//LGiR92PVxvP3QQd8iW +RELlcXOKLOhPX3LzU9DAWNFlRmtdQ4J3F7t642VIFWaAo2zOdeb4J8cmcoViYNp3 +7tWUQPyVQnm2V8gjfIDNYIG1pMjleikCcsL823rDNuV1LWzsxnZHihzX2KvUhdVY +Js2N89w1YbeOEcvO8d6fFYEkSckOkkrhK9MLGla6OY+nHds0/Clykq0BSD2kbSpr +mZ3qWiBZtbR7PEiEGr6pNbupFWVsV7OyOXR4MmU89Ml7bqnlGOIUy+o9cG7E3FfJ +klALjZIpf7FCZyQH7b6KU6IZlZK+hdXEScx2gL3dyQAzNXNV+hFAyvCKhqns7lQO +u6yrbmj0tDHUN6ing4K3UQ01zmfSv5s83usrqRud27ZSnInYXKeKzcj1mYgDELOS +GNGaLnkTNF9TKBP8qYz++/sCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/priv/ref_inspector/ref_inspector.readme.md b/priv/ref_inspector/ref_inspector.readme.md new file mode 100644 index 000000000..e69de29bb diff --git a/priv/ref_inspector/referers.yml b/priv/ref_inspector/referers.yml new file mode 100644 index 000000000..35577b65d --- /dev/null +++ b/priv/ref_inspector/referers.yml @@ -0,0 +1,3919 @@ +# ####################################################################################################### +# +# ALL SUPPORTED REFERERS +# +# Broken down into: +# +# 1. Medium-unknown providers +# 2. Email providers +# 3. Social providers +# 4. Search providers +# 5. Paid media + + +# ####################################################################################################### +# +# MEDIUM-UNKNOWN PROVIDERS +# +# We know the source, but not the medium. +# This section is useful for reducing false positives in the other sections + +unknown: + + Google: + domains: + - support.google.com + - developers.google.com + - maps.google.com + - accounts.google.com + - drive.google.com + - sites.google.com + - groups.google.com + - groups.google.co.uk + + Yahoo!: + domains: + - finance.yahoo.com + - news.yahoo.com + - eurosport.yahoo.com + - sports.yahoo.com + - astrology.yahoo.com + - travel.yahoo.com + - answers.yahoo.com + - screen.yahoo.com + - weather.yahoo.com + - messenger.yahoo.com + - games.yahoo.com + - shopping.yahoo.net + - movies.yahoo.com + - cars.yahoo.com + - lifestyle.yahoo.com + - omg.yahoo.com + - match.yahoo.net + + + +# ####################################################################################################### +# +# EMAIL PROVIDERS + +email: + + 126 Mail: + domains: + - mail.126.com + + 163 Mail: + domains: + - mail.163.com + + 2degrees: + domains: + - webmail.2degreesbroadband.co.nz + + Adam Internet: + domains: + - webmail.adam.com.au + + AOL Mail: + domains: + - mail.aol.com + + Bigpond: + domains: + - webmail.bigpond.com + - webmail2.bigpond.com + - email.telstra.com + - basic.messaging.bigpond.com + + Commander: + domains: + - webmail.commander.net.au + + Daum Mail: + domains: + - mail2.daum.net + - mail.daum.net + + Dodo: + domains: + - webmail.dodo.com.au + + Freenet: + domains: + - webmail.freenet.de + + Gmail: + domains: + - mail.google.com + - inbox.google.com + + iiNet: + domains: + - webmail.iinet.net.au + - mail.iinet.net.au + + Inbox.com: + domains: + - inbox.com + + iPrimus: + domains: + - webmail.iprimus.com.au + + Mynet Mail: + domains: + - mail.mynet.com + + Naver Mail: + domains: + - mail.naver.com + + Netspace: + domains: + - webmail.netspace.net.au + + Optus Zoo: + domains: + - webmail.optuszoo.com.au + - webmail.optusnet.com.au + + Orange Webmail: + domains: + - orange.fr/webmail + + Outlook.com: + domains: + - mail.live.com + - outlook.live.com + + QQ Mail: + domains: + - mail.qq.com + + Seznam Mail: + domains: + - email.seznam.cz + + Virgin: + domains: + - webmail.virginbroadband.com.au + + Vodafone: + domains: + - webmail.vodafone.co.nz + + Westnet: + domains: + - webmail.westnet.com.au + + Yahoo! Mail: + domains: + - mail.yahoo.net + - mail.yahoo.com + - mail.yahoo.co.uk + - mail.yahoo.co.jp + + Zoho: + domains: + - mail.zoho.com + +# ####################################################################################################### +# +# SOCIAL PROVIDERS + +social: + + Facebook: + domains: + - facebook.com + - fb.me + - m.facebook.com + - l.facebook.com + - lm.facebook.com + + Qzone: + domains: + - qzone.qq.com + + Habbo: + domains: + - habbo.com + + Twitter: + domains: + - twitter.com + - t.co + + Instagram: + domains: + - instagram.com + + Youtube: + domains: + - youtube.com + - youtu.be + + Vimeo: + domains: + - vimeo.com + + Renren: + domains: + - renren.com + + Windows Live Spaces: + domains: + - login.live.com + + LinkedIn: + domains: + - linkedin.com + - lnkd.in + + Bebo: + domains: + - bebo.com + + Vkontakte: + domains: + - vk.com + - vkontakte.ru + + Tagged: + domains: + - login.tagged.com + + Orkut: + domains: + - orkut.com + + Myspace: + domains: + - myspace.com + + Friendster: + domains: + - friendster.com + + Badoo: + domains: + - badoo.com + + hi5: + domains: + - hi5.com + + Netlog: + domains: + - netlog.com + + Flixster: + domains: + - flixster.com + + MyLife: + domains: + - mylife.ru + + Paper.li: + domains: + - paper.li + + Classmates: + domains: + - classmates.com + + GitHub: + domains: + - github.com + + Google+: + domains: + - url.google.com + - plus.google.com + + Douban: + domains: + - douban.com + + Odnoklassniki: + domains: + - odnoklassniki.ru + + Viadeo: + domains: + - viadeo.com + + Flickr: + domains: + - flickr.com + + WeeWorld: + domains: + - weeworld.com + + Last.fm: + domains: + - lastfm.ru + + MyHeritage: + domains: + - myheritage.com + + Xanga: + domains: + - xanga.com + + Mixi: + domains: + - mixi.jp + + Cyworld: + domains: + - global.cyworld.com + + Gaia Online: + domains: + - gaiaonline.com + + Skyrock: + domains: + - skyrock.com + + BlackPlanet: + domains: + - blackplanet.com + + myYearbook: + domains: + - myyearbook.com + + Fotolog: + domains: + - fotolog.com + + Friends Reunited: + domains: + - friendsreunited.com + + LiveJournal: + domains: + - livejournal.ru + + StudiVZ: + domains: + - studivz.net + + StackOverflow: + domains: + - stackoverflow.com + + Sonico.com: + domains: + - sonico.com + + Pinterest: + domains: + - pinterest.com + + Plaxo: + domains: + - plaxo.com + + Geni: + domains: + - geni.com + + Tuenti: + domains: + - tuenti.com + + XING: + domains: + - xing.com + + Taringa!: + domains: + - taringa.net + + Tumblr: + domains: + - tumblr.com + - t.umblr.com + + Nasza-klasa.pl: + domains: + - nk.pl + + StumbleUpon: + domains: + - stumbleupon.com + + SourceForge: + domains: + - sourceforge.net + + Hyves: + domains: + - hyves.nl + + WAYN: + domains: + - wayn.com + + Buzznet: + domains: + - buzznet.com + + Multiply: + domains: + - multiply.com + + Foursquare: + domains: + - foursquare.com + + vKruguDruzei.ru: + domains: + - vkrugudruzei.ru + + Mail.ru: + domains: + - my.mail.ru + + MoiKrug.ru: + domains: + - moikrug.ru + + Reddit: + domains: + - reddit.com + + Hacker News: + domains: + - news.ycombinator.com + + Identi.ca: + domains: + - identi.ca + + Weibo: + domains: + - weibo.com + - t.cn + + Delicious: + domains: + - delicious.com + + Pocket: + domains: + - getpocket.com + + ITU Sozluk: + domains: + - itusozluk.com + + Instela: + domains: + - instela.com + + Eksi Sozluk: + domains: + - Sozluk.com + - sourtimes.org + + Uludag Sozluk: + domains: + - uludagsozluk.com + - ulusozluk.com + + Inci Sozluk: + domains: + - inci.sozlukspot.com + - incisozluk.com + - incisozluk.cc + + Hocam.com: + domains: + - hocam.com + + Donanimhaber: + domains: + - donanimhaber.com + + Disqus: + domains: + - redirect.disqus.com + - disq.us + - disqus.com + + Quora: + domains: + - quora.com + + Whirlpool: + domains: + - forums.whirlpool.net.au + +# ####################################################################################################### +# +# SEARCH PROVIDERS + +search: + + 1.cz: + parameters: + - q + domains: + - 1.cz + + # 123people TODO + + 1&1: + parameters: + - q + domains: + - search.1and1.com + + 1und1: + parameters: + - su + domains: + - search.1und1.de + + 360.cn: + parameters: + - q + domains: + - so.360.cn + - www.so.com + + Abacho: + parameters: + - q + domains: + - www.abacho.de + - www.abacho.com + - www.abacho.co.uk + - www.se.abacho.com + - www.tr.abacho.com + - www.abacho.at + - www.abacho.fr + - www.abacho.es + - www.abacho.ch + - www.abacho.it + + ABCsøk: + parameters: + - q + domains: + - abcsolk.no + - verden.abcsok.no + + Acoon: + parameters: + - begriff + domains: + - www.acoon.de + + Alexa: + parameters: + - q + domains: + - alexa.com + - search.toolbars.alexa.com + + Alice Adsl: + parameters: + - q + domains: + - rechercher.aliceadsl.fr + + AllTheWeb: + parameters: + - q + domains: + - www.alltheweb.com + + all.by: + parameters: + - query + domains: + - all.by + + Altavista: + parameters: + - q + domains: + - www.altavista.com + - search.altavista.com + - listings.altavista.com + - altavista.de + - altavista.fr + - be-nl.altavista.com + - be-fr.altavista.com + + Amazon: + parameters: + - keywords + domains: + - amazon.com + - www.amazon.com + + AOL: + parameters: + - q + - query + domains: + - search.aol.com + - search.aol.it + - aolsearch.aol.com + - aolsearch.com + - www.aolrecherche.aol.fr + - www.aolrecherches.aol.fr + - www.aolimages.aol.fr + - aim.search.aol.com + - www.recherche.aol.fr + - recherche.aol.fr + - find.web.aol.com + - recherche.aol.ca + - aolsearch.aol.co.uk + - search.aol.co.uk + - aolrecherche.aol.fr + - sucheaol.aol.de + - suche.aol.de + - suche.aolsvc.de + - aolbusqueda.aol.com.mx + - alicesuche.aol.de + - alicesuchet.aol.de + - suchet2.aol.de + - search.hp.my.aol.com.au + - search.hp.my.aol.de + - search.hp.my.aol.it + - search-intl.netscape.com + + Apollo Latvia: + parameters: + - q + domains: + - apollo.lv/portal/search/ + + APOLL07: + parameters: + - query + domains: + - apollo7.de + + Apontador: + parameters: + - q + domains: + - apontador.com.br + - www.apontador.com.br + + Aport: + parameters: + - r + domains: + - sm.aport.ru + + arama: + parameters: + - q + domains: + - arama.com + + Arcor: + parameters: + - Keywords + domains: + - www.arcor.de + + Arianna: + parameters: + - query + domains: + - arianna.libero.it + - www.arianna.com + + Ask: + parameters: + - q + domains: + - ask.com + - www.ask.com + - web.ask.com + - int.ask.com + - mws.ask.com + - uk.ask.com + - images.ask.com + - ask.reference.com + - www.askkids.com + - iwon.ask.com + - www.ask.co.uk + - www.qbyrd.com + - search-results.com + - uk.search-results.com + - www.search-results.com + - int.search-results.com + + Ask Toolbar: + parameters: + - searchfor + domains: + - search.tb.ask.com + + Atlas: + parameters: + - q + domains: + - searchatlas.centrum.cz + + Austronaut: + parameters: + - q + domains: + - www2.austronaut.at + - www1.astronaut.at + + Babylon: + parameters: + - q + domains: + - search.babylon.com + - searchassist.babylon.com + + Baidu: + parameters: + - wd + - word + - kw + - k + domains: + - www.baidu.com + - www1.baidu.com + - zhidao.baidu.com + - tieba.baidu.com + - news.baidu.com + - web.gougou.com + - m.baidu.com + + Biglobe: + parameters: + - q + domains: + - cgi.search.biglobe.ne.jp + + Bing: + parameters: + - q + - Q + domains: + - bing.com + - www.bing.com + - msnbc.msn.com + - dizionario.it.msn.com + - cc.bingj.com + - m.bing.com + + Bing Images: + parameters: + - q + - Q + domains: + - bing.com/images/search + - www.bing.com/images/search + + blekko: + parameters: + - q + domains: + - blekko.com + + Blogdigger: + parameters: + - q + domains: + - www.blogdigger.com + + Blogpulse: + parameters: + - query + domains: + - www.blogpulse.com + + Bluewin: + parameters: + - searchTerm + domains: + - search.bluewin.ch + + British Telecommunications: + parameters: + - p + domains: + - search.bt.com + + canoe.ca: + parameters: + - q + domains: + - web.canoe.ca + + Centrum: + parameters: + - q + domains: + - serach.centrum.cz + - morfeo.centrum.cz + + Certified-Toolbar: + parameters: + - q + domains: + - search.certified-toolbar.com + + Charter: + parameters: + - q + domains: + - www.charter.net + + Clix: + parameters: + - question + domains: + - pesquisa.clix.pt + + Conduit: + parameters: + - q + domains: + - search.conduit.com + + Comcast: + parameters: + - q + domains: + - serach.comcast.net + + Crawler: + parameters: + - q + domains: + - www.crawler.com + + Compuserve: + parameters: + - query + domains: + - websearch.cs.com + + Cuil: + parameters: + - q + domains: + - www.cuil.com + + Daemon search: + parameters: + - q + domains: + - daemon-search.com + - my.daemon-search.com + + Dalesearch: + parameters: + - q + domains: + - www.dalesearch.com + + DasOertliche: + parameters: + - kw + domains: + - www.dasoertliche.de + + DasTelefonbuch: + parameters: + - kw + domains: + - www1.dastelefonbuch.de + + Daum: + parameters: + - q + domains: + - search.daum.net + + Delfi latvia: + parameters: + - q + domains: + - smart.delfi.lv + + Delfi: + parameters: + - q + domains: + - otsing.delfi.ee + + Digg: + parameters: + - s + domains: + - digg.com + + dmoz: + parameters: + - q + domains: + - dmoz.org + - editors.dmoz.org + + Dodo: + parameters: + - q + domains: + - google.dodo.com.au + + DuckDuckGo: + parameters: + - q + domains: + - duckduckgo.com + + earthlink: + parameters: + - q + domains: + - search.earthlink.net + + Ecosia: + parameters: + - q + domains: + - ecosia.org + + Eniro: + parameters: + - q + - search_word + domains: + - www.eniro.se + + Eurip: + parameters: + - q + domains: + - www.eurip.com + + Euroseek: + parameters: + - string + domains: + - www.euroseek.com + + Everyclick: + parameters: + - keyword + domains: + - www.everyclick.com + + Excite: + parameters: + - q + - search + domains: + - search.excite.it + - search.excite.fr + - search.excite.de + - search.excite.co.uk + - serach.excite.es + - search.excite.nl + - msxml.excite.com + - www.excite.co.jp + + Exalead: + parameters: + - q + domains: + - www.exalead.fr + - www.exalead.com + + eo: + parameters: + - x_query + domains: + - eo.st + + Fast Browser Search: + parameters: + - q + domains: + - www.fastbrowsersearch.com + + Francite: + parameters: + - name + domains: + - recherche.francite.com + + Finderoo: + parameters: + - q + domains: + - www.finderoo.com + + Findwide: + parameters: + - k + domains: + - search.findwide.com + + Fireball: + parameters: + - q + domains: + - www.fireball.de + + Firstfind: + parameters: + - qry + domains: + - www.firstsfind.com + + Fixsuche: + parameters: + - q + domains: + - www.fixsuche.de + + Flix: + parameters: + - keyword + domains: + - www.flix.de + + Forestle: + parameters: + - q + domains: + - forestle.org + - www.forestle.org + - forestle.mobi + + Free: + parameters: + - q + domains: + - search.free.fr + - search1-2.free.fr + - search1-1.free.fr + + Freecause: + parameters: + - p + domains: + - search.freecause.com + + Freenet: + parameters: + - query + - Keywords + domains: + - suche.freenet.de + + Freshweather: + parameters: + - q + domains: + - www.fresh-weather.com + + FriendFeed: + parameters: + - q + domains: + - friendfeed.com + + GAIS: + parameters: + - q + domains: + - gais.cs.ccu.edu.tw + + Geona: + parameters: + - q + domains: + - geona.net + + Genieo: + parameters: + - q + domains: + - search.genieo.com + + Gigablast: + parameters: + - q + domains: + - www.gigablast.com + - dir.gigablast.com + + Globososo: + parameters: + - q + domains: + - searches.globososo.com + - search.globososo.com + + GMX: + parameters: + - su + domains: + - suche.gmx.net + + Gnadenmeer: + parameters: + - keyword + domains: + - www.gnadenmeer.de + + Gomeo: + parameters: + - Keywords + domains: + - www.gomeo.com + + goo: + parameters: + - MT + domains: + - search.goo.ne.jp + - ocnsearch.goo.ne.jp + + Google: + parameters: + - q + - query # For www.cnn.com (powered by Google) + - Keywords # For gooofullsearch.com (powered by Google) + domains: + - www.google.com + - www.google.ac + - www.google.ad + - www.google.com.af + - www.google.com.ag + - www.google.com.ai + - www.google.am + - www.google.it.ao + - www.google.com.ar + - www.google.as + - www.google.at + - www.google.com.au + - www.google.az + - www.google.ba + - www.google.com.bd + - www.google.be + - www.google.bf + - www.google.bg + - www.google.com.bh + - www.google.bi + - www.google.bj + - www.google.com.bn + - www.google.com.bo + - www.google.com.br + - www.google.bs + - www.google.co.bw + - www.google.com.by + - www.google.by + - www.google.com.bz + - www.google.ca + - www.google.com.kh + - www.google.cc + - www.google.cd + - www.google.cf + - www.google.cat + - www.google.cg + - www.google.ch + - www.google.ci + - www.google.co.ck + - www.google.cl + - www.google.cm + - www.google.cn + - www.google.com.co + - www.google.co.cr + - www.google.com.cu + - www.google.cv + - www.google.com.cy + - www.google.cz + - www.google.de + - www.google.dj + - www.google.dk + - www.google.dm + - www.google.com.do + - www.google.dz + - www.google.com.ec + - www.google.ee + - www.google.com.eg + - www.google.es + - www.google.com.et + - www.google.fi + - www.google.com.fj + - www.google.fm + - www.google.fr + - www.google.ga + - www.google.gd + - www.google.ge + - www.google.gf + - www.google.gg + - www.google.com.gh + - www.google.com.gi + - www.google.gl + - www.google.gm + - www.google.gp + - www.google.gr + - www.google.com.gt + - www.google.gy + - www.google.com.hk + - www.google.hn + - www.google.hr + - www.google.ht + - www.google.hu + - www.google.co.id + - www.google.iq + - www.google.ie + - www.google.co.il + - www.google.im + - www.google.co.in + - www.google.io + - www.google.is + - www.google.it + - www.google.je + - www.google.com.jm + - www.google.jo + - www.google.co.jp + - www.google.co.ke + - www.google.ki + - www.google.kg + - www.google.co.kr + - www.google.com.kw + - www.google.kz + - www.google.la + - www.google.com.lb + - www.google.com.lc + - www.google.li + - www.google.lk + - www.google.co.ls + - www.google.lt + - www.google.lu + - www.google.lv + - www.google.com.ly + - www.google.co.ma + - www.google.md + - www.google.me + - www.google.mg + - www.google.mk + - www.google.ml + - www.google.mn + - www.google.ms + - www.google.com.mt + - www.google.mu + - www.google.mv + - www.google.mw + - www.google.com.mx + - www.google.com.my + - www.google.co.mz + - www.google.com.na + - www.google.ne + - www.google.com.nf + - www.google.com.ng + - www.google.com.ni + - www.google.nl + - www.google.no + - www.google.com.np + - www.google.nr + - www.google.nu + - www.google.co.nz + - www.google.com.om + - www.google.com.pa + - www.google.com.pe + - www.google.com.ph + - www.google.com.pk + - www.google.pl + - www.google.pn + - www.google.com.pr + - www.google.ps + - www.google.pt + - www.google.com.py + - www.google.com.qa + - www.google.ro + - www.google.rs + - www.google.ru + - www.google.rw + - www.google.com.sa + - www.google.com.sb + - www.google.sc + - www.google.se + - www.google.com.sg + - www.google.sh + - www.google.si + - www.google.sk + - www.google.com.sl + - www.google.sn + - www.google.sm + - www.google.so + - www.google.st + - www.google.com.sv + - www.google.td + - www.google.tg + - www.google.co.th + - www.google.com.tj + - www.google.tk + - www.google.tl + - www.google.tm + - www.google.to + - www.google.com.tn + - www.google.tn + - www.google.com.tr + - www.google.tt + - www.google.com.tw + - www.google.co.tz + - www.google.com.ua + - www.google.co.ug + - www.google.ae + - www.google.co.uk + - www.google.us + - www.google.com.uy + - www.google.co.uz + - www.google.com.vc + - www.google.co.ve + - www.google.vg + - www.google.co.vi + - www.google.com.vn + - www.google.vu + - www.google.ws + - www.google.co.za + - www.google.co.zm + - www.google.co.zw + - google.com + - google.ac + - google.ad + - google.com.af + - google.com.ag + - google.com.ai + - google.am + - google.it.ao + - google.com.ar + - google.as + - google.at + - google.com.au + - google.az + - google.ba + - google.com.bd + - google.be + - google.bf + - google.bg + - google.com.bh + - google.bi + - google.bj + - google.com.bn + - google.com.bo + - google.com.br + - google.bs + - google.co.bw + - google.com.by + - google.by + - google.com.bz + - google.ca + - google.com.kh + - google.cc + - google.cd + - google.cf + - google.cat + - google.cg + - google.ch + - google.ci + - google.co.ck + - google.cl + - google.cm + - google.cn + - google.com.co + - google.co.cr + - google.com.cu + - google.cv + - google.com.cy + - google.cz + - google.de + - google.dj + - google.dk + - google.dm + - google.com.do + - google.dz + - google.com.ec + - google.ee + - google.com.eg + - google.es + - google.com.et + - google.fi + - google.com.fj + - google.fm + - google.fr + - google.ga + - google.gd + - google.ge + - google.gf + - google.gg + - google.com.gh + - google.com.gi + - google.gl + - google.gm + - google.gp + - google.gr + - google.com.gt + - google.gy + - google.com.hk + - google.hn + - google.hr + - google.ht + - google.hu + - google.co.id + - google.iq + - google.ie + - google.co.il + - google.im + - google.co.in + - google.io + - google.is + - google.it + - google.je + - google.com.jm + - google.jo + - google.co.jp + - google.co.ke + - google.ki + - google.kg + - google.co.kr + - google.com.kw + - google.kz + - google.la + - google.com.lb + - google.com.lc + - google.li + - google.lk + - google.co.ls + - google.lt + - google.lu + - google.lv + - google.com.ly + - google.co.ma + - google.md + - google.me + - google.mg + - google.mk + - google.ml + - google.mn + - google.ms + - google.com.mt + - google.mu + - google.mv + - google.mw + - google.com.mx + - google.com.my + - google.co.mz + - google.com.na + - google.ne + - google.com.nf + - google.com.ng + - google.com.ni + - google.nl + - google.no + - google.com.np + - google.nr + - google.nu + - google.co.nz + - google.com.om + - google.com.pa + - google.com.pe + - google.com.ph + - google.com.pk + - google.pl + - google.pn + - google.com.pr + - google.ps + - google.pt + - google.com.py + - google.com.qa + - google.ro + - google.rs + - google.ru + - google.rw + - google.com.sa + - google.com.sb + - google.sc + - google.se + - google.com.sg + - google.sh + - google.si + - google.sk + - google.com.sl + - google.sn + - google.sm + - google.so + - google.st + - google.com.sv + - google.td + - google.tg + - google.co.th + - google.com.tj + - google.tk + - google.tl + - google.tm + - google.to + - google.com.tn + - google.com.tr + - google.tt + - google.com.tw + - google.co.tz + - google.com.ua + - google.co.ug + - google.ae + - google.co.uk + - google.us + - google.com.uy + - google.co.uz + - google.com.vc + - google.co.ve + - google.vg + - google.co.vi + - google.com.vn + - google.vu + - google.ws + - google.co.za + - google.co.zm + - google.co.zw + - google.tn + # powered by Google + - search.avg.com + - isearch.avg.com + - www.cnn.com + - darkoogle.com + - search.darkoogle.com + - search.foxtab.com + - www.gooofullsearch.com + - search.hiyo.com + - search.incredimail.com + - search1.incredimail.com + - search2.incredimail.com + - search3.incredimail.com + - search4.incredimail.com + - search.incredibar.com + - search.sweetim.com + - www.fastweb.it + - search.juno.com + - find.tdc.dk + - searchresults.verizon.com + - search.walla.co.il + - search.alot.com + # Google Earch + - www.googleearth.de + - www.googleearth.fr + # Google Cache + - webcache.googleusercontent.com + # Google SSL + - encrypted.google.com + # Syndicated search + - googlesyndicatedsearch.com + + Google Blogsearch: + parameters: + - q + domains: + - blogsearch.google.ac + - blogsearch.google.ad + - blogsearch.google.ae + - blogsearch.google.am + - blogsearch.google.as + - blogsearch.google.at + - blogsearch.google.az + - blogsearch.google.ba + - blogsearch.google.be + - blogsearch.google.bf + - blogsearch.google.bg + - blogsearch.google.bi + - blogsearch.google.bj + - blogsearch.google.bs + - blogsearch.google.by + - blogsearch.google.ca + - blogsearch.google.cat + - blogsearch.google.cc + - blogsearch.google.cd + - blogsearch.google.cf + - blogsearch.google.cg + - blogsearch.google.ch + - blogsearch.google.ci + - blogsearch.google.cl + - blogsearch.google.cm + - blogsearch.google.cn + - blogsearch.google.co.bw + - blogsearch.google.co.ck + - blogsearch.google.co.cr + - blogsearch.google.co.id + - blogsearch.google.co.il + - blogsearch.google.co.in + - blogsearch.google.co.jp + - blogsearch.google.co.ke + - blogsearch.google.co.kr + - blogsearch.google.co.ls + - blogsearch.google.co.ma + - blogsearch.google.co.mz + - blogsearch.google.co.nz + - blogsearch.google.co.th + - blogsearch.google.co.tz + - blogsearch.google.co.ug + - blogsearch.google.co.uk + - blogsearch.google.co.uz + - blogsearch.google.co.ve + - blogsearch.google.co.vi + - blogsearch.google.co.za + - blogsearch.google.co.zm + - blogsearch.google.co.zw + - blogsearch.google.com + - blogsearch.google.com.af + - blogsearch.google.com.ag + - blogsearch.google.com.ai + - blogsearch.google.com.ar + - blogsearch.google.com.au + - blogsearch.google.com.bd + - blogsearch.google.com.bh + - blogsearch.google.com.bn + - blogsearch.google.com.bo + - blogsearch.google.com.br + - blogsearch.google.com.by + - blogsearch.google.com.bz + - blogsearch.google.com.co + - blogsearch.google.com.cu + - blogsearch.google.com.cy + - blogsearch.google.com.do + - blogsearch.google.com.ec + - blogsearch.google.com.eg + - blogsearch.google.com.et + - blogsearch.google.com.fj + - blogsearch.google.com.gh + - blogsearch.google.com.gi + - blogsearch.google.com.gt + - blogsearch.google.com.hk + - blogsearch.google.com.jm + - blogsearch.google.com.kh + - blogsearch.google.com.kw + - blogsearch.google.com.lb + - blogsearch.google.com.lc + - blogsearch.google.com.ly + - blogsearch.google.com.mt + - blogsearch.google.com.mx + - blogsearch.google.com.my + - blogsearch.google.com.na + - blogsearch.google.com.nf + - blogsearch.google.com.ng + - blogsearch.google.com.ni + - blogsearch.google.com.np + - blogsearch.google.com.om + - blogsearch.google.com.pa + - blogsearch.google.com.pe + - blogsearch.google.com.ph + - blogsearch.google.com.pk + - blogsearch.google.com.pr + - blogsearch.google.com.py + - blogsearch.google.com.qa + - blogsearch.google.com.sa + - blogsearch.google.com.sb + - blogsearch.google.com.sg + - blogsearch.google.com.sl + - blogsearch.google.com.sv + - blogsearch.google.com.tj + - blogsearch.google.com.tn + - blogsearch.google.com.tr + - blogsearch.google.com.tw + - blogsearch.google.com.ua + - blogsearch.google.com.uy + - blogsearch.google.com.vc + - blogsearch.google.com.vn + - blogsearch.google.cv + - blogsearch.google.cz + - blogsearch.google.de + - blogsearch.google.dj + - blogsearch.google.dk + - blogsearch.google.dm + - blogsearch.google.dz + - blogsearch.google.ee + - blogsearch.google.es + - blogsearch.google.fi + - blogsearch.google.fm + - blogsearch.google.fr + - blogsearch.google.ga + - blogsearch.google.gd + - blogsearch.google.ge + - blogsearch.google.gf + - blogsearch.google.gg + - blogsearch.google.gl + - blogsearch.google.gm + - blogsearch.google.gp + - blogsearch.google.gr + - blogsearch.google.gy + - blogsearch.google.hn + - blogsearch.google.hr + - blogsearch.google.ht + - blogsearch.google.hu + - blogsearch.google.ie + - blogsearch.google.im + - blogsearch.google.io + - blogsearch.google.iq + - blogsearch.google.is + - blogsearch.google.it + - blogsearch.google.it.ao + - blogsearch.google.je + - blogsearch.google.jo + - blogsearch.google.kg + - blogsearch.google.ki + - blogsearch.google.kz + - blogsearch.google.la + - blogsearch.google.li + - blogsearch.google.lk + - blogsearch.google.lt + - blogsearch.google.lu + - blogsearch.google.lv + - blogsearch.google.md + - blogsearch.google.me + - blogsearch.google.mg + - blogsearch.google.mk + - blogsearch.google.ml + - blogsearch.google.mn + - blogsearch.google.ms + - blogsearch.google.mu + - blogsearch.google.mv + - blogsearch.google.mw + - blogsearch.google.ne + - blogsearch.google.nl + - blogsearch.google.no + - blogsearch.google.nr + - blogsearch.google.nu + - blogsearch.google.pl + - blogsearch.google.pn + - blogsearch.google.ps + - blogsearch.google.pt + - blogsearch.google.ro + - blogsearch.google.rs + - blogsearch.google.ru + - blogsearch.google.rw + - blogsearch.google.sc + - blogsearch.google.se + - blogsearch.google.sh + - blogsearch.google.si + - blogsearch.google.sk + - blogsearch.google.sm + - blogsearch.google.sn + - blogsearch.google.so + - blogsearch.google.st + - blogsearch.google.td + - blogsearch.google.tg + - blogsearch.google.tk + - blogsearch.google.tl + - blogsearch.google.tm + - blogsearch.google.to + - blogsearch.google.tt + - blogsearch.google.us + - blogsearch.google.vg + - blogsearch.google.vu + - blogsearch.google.ws + + Google Images: + parameters: + - q + domains: + - google.ac/imgres + - google.ad/imgres + - google.ae/imgres + - google.am/imgres + - google.as/imgres + - google.at/imgres + - google.az/imgres + - google.ba/imgres + - google.be/imgres + - google.bf/imgres + - google.bg/imgres + - google.bi/imgres + - google.bj/imgres + - google.bs/imgres + - google.by/imgres + - google.ca/imgres + - google.cat/imgres + - google.cc/imgres + - google.cd/imgres + - google.cf/imgres + - google.cg/imgres + - google.ch/imgres + - google.ci/imgres + - google.cl/imgres + - google.cm/imgres + - google.cn/imgres + - google.co.bw/imgres + - google.co.ck/imgres + - google.co.cr/imgres + - google.co.id/imgres + - google.co.il/imgres + - google.co.in/imgres + - google.co.jp/imgres + - google.co.ke/imgres + - google.co.kr/imgres + - google.co.ls/imgres + - google.co.ma/imgres + - google.co.mz/imgres + - google.co.nz/imgres + - google.co.th/imgres + - google.co.tz/imgres + - google.co.ug/imgres + - google.co.uk/imgres + - google.co.uz/imgres + - google.co.ve/imgres + - google.co.vi/imgres + - google.co.za/imgres + - google.co.zm/imgres + - google.co.zw/imgres + - google.com/imgres + - google.com.af/imgres + - google.com.ag/imgres + - google.com.ai/imgres + - google.com.ar/imgres + - google.com.au/imgres + - google.com.bd/imgres + - google.com.bh/imgres + - google.com.bn/imgres + - google.com.bo/imgres + - google.com.br/imgres + - google.com.by/imgres + - google.com.bz/imgres + - google.com.co/imgres + - google.com.cu/imgres + - google.com.cy/imgres + - google.com.do/imgres + - google.com.ec/imgres + - google.com.eg/imgres + - google.com.et/imgres + - google.com.fj/imgres + - google.com.gh/imgres + - google.com.gi/imgres + - google.com.gt/imgres + - google.com.hk/imgres + - google.com.jm/imgres + - google.com.kh/imgres + - google.com.kw/imgres + - google.com.lb/imgres + - google.com.lc/imgres + - google.com.ly/imgres + - google.com.mt/imgres + - google.com.mx/imgres + - google.com.my/imgres + - google.com.na/imgres + - google.com.nf/imgres + - google.com.ng/imgres + - google.com.ni/imgres + - google.com.np/imgres + - google.com.om/imgres + - google.com.pa/imgres + - google.com.pe/imgres + - google.com.ph/imgres + - google.com.pk/imgres + - google.com.pr/imgres + - google.com.py/imgres + - google.com.qa/imgres + - google.com.sa/imgres + - google.com.sb/imgres + - google.com.sg/imgres + - google.com.sl/imgres + - google.com.sv/imgres + - google.com.tj/imgres + - google.com.tn/imgres + - google.com.tr/imgres + - google.com.tw/imgres + - google.com.ua/imgres + - google.com.uy/imgres + - google.com.vc/imgres + - google.com.vn/imgres + - google.cv/imgres + - google.cz/imgres + - google.de/imgres + - google.dj/imgres + - google.dk/imgres + - google.dm/imgres + - google.dz/imgres + - google.ee/imgres + - google.es/imgres + - google.fi/imgres + - google.fm/imgres + - google.fr/imgres + - google.ga/imgres + - google.gd/imgres + - google.ge/imgres + - google.gf/imgres + - google.gg/imgres + - google.gl/imgres + - google.gm/imgres + - google.gp/imgres + - google.gr/imgres + - google.gy/imgres + - google.hn/imgres + - google.hr/imgres + - google.ht/imgres + - google.hu/imgres + - google.ie/imgres + - google.im/imgres + - google.io/imgres + - google.iq/imgres + - google.is/imgres + - google.it/imgres + - google.it.ao/imgres + - google.je/imgres + - google.jo/imgres + - google.kg/imgres + - google.ki/imgres + - google.kz/imgres + - google.la/imgres + - google.li/imgres + - google.lk/imgres + - google.lt/imgres + - google.lu/imgres + - google.lv/imgres + - google.md/imgres + - google.me/imgres + - google.mg/imgres + - google.mk/imgres + - google.ml/imgres + - google.mn/imgres + - google.ms/imgres + - google.mu/imgres + - google.mv/imgres + - google.mw/imgres + - google.ne/imgres + - google.nl/imgres + - google.no/imgres + - google.nr/imgres + - google.nu/imgres + - google.pl/imgres + - google.pn/imgres + - google.ps/imgres + - google.pt/imgres + - google.ro/imgres + - google.rs/imgres + - google.ru/imgres + - google.rw/imgres + - google.sc/imgres + - google.se/imgres + - google.sh/imgres + - google.si/imgres + - google.sk/imgres + - google.sm/imgres + - google.sn/imgres + - google.so/imgres + - google.st/imgres + - google.td/imgres + - google.tg/imgres + - google.tk/imgres + - google.tl/imgres + - google.tm/imgres + - google.to/imgres + - google.tt/imgres + - google.us/imgres + - google.vg/imgres + - google.vu/imgres + - images.google.ws + - images.google.ac + - images.google.ad + - images.google.ae + - images.google.am + - images.google.as + - images.google.at + - images.google.az + - images.google.ba + - images.google.be + - images.google.bf + - images.google.bg + - images.google.bi + - images.google.bj + - images.google.bs + - images.google.by + - images.google.ca + - images.google.cat + - images.google.cc + - images.google.cd + - images.google.cf + - images.google.cg + - images.google.ch + - images.google.ci + - images.google.cl + - images.google.cm + - images.google.cn + - images.google.co.bw + - images.google.co.ck + - images.google.co.cr + - images.google.co.id + - images.google.co.il + - images.google.co.in + - images.google.co.jp + - images.google.co.ke + - images.google.co.kr + - images.google.co.ls + - images.google.co.ma + - images.google.co.mz + - images.google.co.nz + - images.google.co.th + - images.google.co.tz + - images.google.co.ug + - images.google.co.uk + - images.google.co.uz + - images.google.co.ve + - images.google.co.vi + - images.google.co.za + - images.google.co.zm + - images.google.co.zw + - images.google.com + - images.google.com.af + - images.google.com.ag + - images.google.com.ai + - images.google.com.ar + - images.google.com.au + - images.google.com.bd + - images.google.com.bh + - images.google.com.bn + - images.google.com.bo + - images.google.com.br + - images.google.com.by + - images.google.com.bz + - images.google.com.co + - images.google.com.cu + - images.google.com.cy + - images.google.com.do + - images.google.com.ec + - images.google.com.eg + - images.google.com.et + - images.google.com.fj + - images.google.com.gh + - images.google.com.gi + - images.google.com.gt + - images.google.com.hk + - images.google.com.jm + - images.google.com.kh + - images.google.com.kw + - images.google.com.lb + - images.google.com.lc + - images.google.com.ly + - images.google.com.mt + - images.google.com.mx + - images.google.com.my + - images.google.com.na + - images.google.com.nf + - images.google.com.ng + - images.google.com.ni + - images.google.com.np + - images.google.com.om + - images.google.com.pa + - images.google.com.pe + - images.google.com.ph + - images.google.com.pk + - images.google.com.pr + - images.google.com.py + - images.google.com.qa + - images.google.com.sa + - images.google.com.sb + - images.google.com.sg + - images.google.com.sl + - images.google.com.sv + - images.google.com.tj + - images.google.com.tn + - images.google.com.tr + - images.google.com.tw + - images.google.com.ua + - images.google.com.uy + - images.google.com.vc + - images.google.com.vn + - images.google.cv + - images.google.cz + - images.google.de + - images.google.dj + - images.google.dk + - images.google.dm + - images.google.dz + - images.google.ee + - images.google.es + - images.google.fi + - images.google.fm + - images.google.fr + - images.google.ga + - images.google.gd + - images.google.ge + - images.google.gf + - images.google.gg + - images.google.gl + - images.google.gm + - images.google.gp + - images.google.gr + - images.google.gy + - images.google.hn + - images.google.hr + - images.google.ht + - images.google.hu + - images.google.ie + - images.google.im + - images.google.io + - images.google.iq + - images.google.is + - images.google.it + - images.google.it.ao + - images.google.je + - images.google.jo + - images.google.kg + - images.google.ki + - images.google.kz + - images.google.la + - images.google.li + - images.google.lk + - images.google.lt + - images.google.lu + - images.google.lv + - images.google.md + - images.google.me + - images.google.mg + - images.google.mk + - images.google.ml + - images.google.mn + - images.google.ms + - images.google.mu + - images.google.mv + - images.google.mw + - images.google.ne + - images.google.nl + - images.google.no + - images.google.nr + - images.google.nu + - images.google.pl + - images.google.pn + - images.google.ps + - images.google.pt + - images.google.ro + - images.google.rs + - images.google.ru + - images.google.rw + - images.google.sc + - images.google.se + - images.google.sh + - images.google.si + - images.google.sk + - images.google.sm + - images.google.sn + - images.google.so + - images.google.st + - images.google.td + - images.google.tg + - images.google.tk + - images.google.tl + - images.google.tm + - images.google.to + - images.google.tt + - images.google.us + - images.google.vg + - images.google.vu + + Google News: + parameters: + - q + domains: + - news.google.ac + - news.google.ad + - news.google.ae + - news.google.am + - news.google.as + - news.google.at + - news.google.az + - news.google.ba + - news.google.be + - news.google.bf + - news.google.bg + - news.google.bi + - news.google.bj + - news.google.bs + - news.google.by + - news.google.ca + - news.google.cat + - news.google.cc + - news.google.cd + - news.google.cf + - news.google.cg + - news.google.ch + - news.google.ci + - news.google.cl + - news.google.cm + - news.google.cn + - news.google.co.bw + - news.google.co.ck + - news.google.co.cr + - news.google.co.id + - news.google.co.il + - news.google.co.in + - news.google.co.jp + - news.google.co.ke + - news.google.co.kr + - news.google.co.ls + - news.google.co.ma + - news.google.co.mz + - news.google.co.nz + - news.google.co.th + - news.google.co.tz + - news.google.co.ug + - news.google.co.uk + - news.google.co.uz + - news.google.co.ve + - news.google.co.vi + - news.google.co.za + - news.google.co.zm + - news.google.co.zw + - news.google.com + - news.google.com.af + - news.google.com.ag + - news.google.com.ai + - news.google.com.ar + - news.google.com.au + - news.google.com.bd + - news.google.com.bh + - news.google.com.bn + - news.google.com.bo + - news.google.com.br + - news.google.com.by + - news.google.com.bz + - news.google.com.co + - news.google.com.cu + - news.google.com.cy + - news.google.com.do + - news.google.com.ec + - news.google.com.eg + - news.google.com.et + - news.google.com.fj + - news.google.com.gh + - news.google.com.gi + - news.google.com.gt + - news.google.com.hk + - news.google.com.jm + - news.google.com.kh + - news.google.com.kw + - news.google.com.lb + - news.google.com.lc + - news.google.com.ly + - news.google.com.mt + - news.google.com.mx + - news.google.com.my + - news.google.com.na + - news.google.com.nf + - news.google.com.ng + - news.google.com.ni + - news.google.com.np + - news.google.com.om + - news.google.com.pa + - news.google.com.pe + - news.google.com.ph + - news.google.com.pk + - news.google.com.pr + - news.google.com.py + - news.google.com.qa + - news.google.com.sa + - news.google.com.sb + - news.google.com.sg + - news.google.com.sl + - news.google.com.sv + - news.google.com.tj + - news.google.com.tn + - news.google.com.tr + - news.google.com.tw + - news.google.com.ua + - news.google.com.uy + - news.google.com.vc + - news.google.com.vn + - news.google.cv + - news.google.cz + - news.google.de + - news.google.dj + - news.google.dk + - news.google.dm + - news.google.dz + - news.google.ee + - news.google.es + - news.google.fi + - news.google.fm + - news.google.fr + - news.google.ga + - news.google.gd + - news.google.ge + - news.google.gf + - news.google.gg + - news.google.gl + - news.google.gm + - news.google.gp + - news.google.gr + - news.google.gy + - news.google.hn + - news.google.hr + - news.google.ht + - news.google.hu + - news.google.ie + - news.google.im + - news.google.io + - news.google.iq + - news.google.is + - news.google.it + - news.google.it.ao + - news.google.je + - news.google.jo + - news.google.kg + - news.google.ki + - news.google.kz + - news.google.la + - news.google.li + - news.google.lk + - news.google.lt + - news.google.lu + - news.google.lv + - news.google.md + - news.google.me + - news.google.mg + - news.google.mk + - news.google.ml + - news.google.mn + - news.google.ms + - news.google.mu + - news.google.mv + - news.google.mw + - news.google.ne + - news.google.nl + - news.google.no + - news.google.nr + - news.google.nu + - news.google.pl + - news.google.pn + - news.google.ps + - news.google.pt + - news.google.ro + - news.google.rs + - news.google.ru + - news.google.rw + - news.google.sc + - news.google.se + - news.google.sh + - news.google.si + - news.google.sk + - news.google.sm + - news.google.sn + - news.google.so + - news.google.st + - news.google.td + - news.google.tg + - news.google.tk + - news.google.tl + - news.google.tm + - news.google.to + - news.google.tt + - news.google.us + - news.google.vg + - news.google.vu + - news.google.ws + + Google Product Search: + parameters: + - q + domains: + - google.ac/products + - google.ad/products + - google.ae/products + - google.am/products + - google.as/products + - google.at/products + - google.az/products + - google.ba/products + - google.be/products + - google.bf/products + - google.bg/products + - google.bi/products + - google.bj/products + - google.bs/products + - google.by/products + - google.ca/products + - google.cat/products + - google.cc/products + - google.cd/products + - google.cf/products + - google.cg/products + - google.ch/products + - google.ci/products + - google.cl/products + - google.cm/products + - google.cn/products + - google.co.bw/products + - google.co.ck/products + - google.co.cr/products + - google.co.id/products + - google.co.il/products + - google.co.in/products + - google.co.jp/products + - google.co.ke/products + - google.co.kr/products + - google.co.ls/products + - google.co.ma/products + - google.co.mz/products + - google.co.nz/products + - google.co.th/products + - google.co.tz/products + - google.co.ug/products + - google.co.uk/products + - google.co.uz/products + - google.co.ve/products + - google.co.vi/products + - google.co.za/products + - google.co.zm/products + - google.co.zw/products + - google.com/products + - google.com.af/products + - google.com.ag/products + - google.com.ai/products + - google.com.ar/products + - google.com.au/products + - google.com.bd/products + - google.com.bh/products + - google.com.bn/products + - google.com.bo/products + - google.com.br/products + - google.com.by/products + - google.com.bz/products + - google.com.co/products + - google.com.cu/products + - google.com.cy/products + - google.com.do/products + - google.com.ec/products + - google.com.eg/products + - google.com.et/products + - google.com.fj/products + - google.com.gh/products + - google.com.gi/products + - google.com.gt/products + - google.com.hk/products + - google.com.jm/products + - google.com.kh/products + - google.com.kw/products + - google.com.lb/products + - google.com.lc/products + - google.com.ly/products + - google.com.mt/products + - google.com.mx/products + - google.com.my/products + - google.com.na/products + - google.com.nf/products + - google.com.ng/products + - google.com.ni/products + - google.com.np/products + - google.com.om/products + - google.com.pa/products + - google.com.pe/products + - google.com.ph/products + - google.com.pk/products + - google.com.pr/products + - google.com.py/products + - google.com.qa/products + - google.com.sa/products + - google.com.sb/products + - google.com.sg/products + - google.com.sl/products + - google.com.sv/products + - google.com.tj/products + - google.com.tn/products + - google.com.tr/products + - google.com.tw/products + - google.com.ua/products + - google.com.uy/products + - google.com.vc/products + - google.com.vn/products + - google.cv/products + - google.cz/products + - google.de/products + - google.dj/products + - google.dk/products + - google.dm/products + - google.dz/products + - google.ee/products + - google.es/products + - google.fi/products + - google.fm/products + - google.fr/products + - google.ga/products + - google.gd/products + - google.ge/products + - google.gf/products + - google.gg/products + - google.gl/products + - google.gm/products + - google.gp/products + - google.gr/products + - google.gy/products + - google.hn/products + - google.hr/products + - google.ht/products + - google.hu/products + - google.ie/products + - google.im/products + - google.io/products + - google.iq/products + - google.is/products + - google.it/products + - google.it.ao/products + - google.je/products + - google.jo/products + - google.kg/products + - google.ki/products + - google.kz/products + - google.la/products + - google.li/products + - google.lk/products + - google.lt/products + - google.lu/products + - google.lv/products + - google.md/products + - google.me/products + - google.mg/products + - google.mk/products + - google.ml/products + - google.mn/products + - google.ms/products + - google.mu/products + - google.mv/products + - google.mw/products + - google.ne/products + - google.nl/products + - google.no/products + - google.nr/products + - google.nu/products + - google.pl/products + - google.pn/products + - google.ps/products + - google.pt/products + - google.ro/products + - google.rs/products + - google.ru/products + - google.rw/products + - google.sc/products + - google.se/products + - google.sh/products + - google.si/products + - google.sk/products + - google.sm/products + - google.sn/products + - google.so/products + - google.st/products + - google.td/products + - google.tg/products + - google.tk/products + - google.tl/products + - google.tm/products + - google.to/products + - google.tt/products + - google.us/products + - google.vg/products + - google.vu/products + - google.ws/products + - www.google.ac/products + - www.google.ad/products + - www.google.ae/products + - www.google.am/products + - www.google.as/products + - www.google.at/products + - www.google.az/products + - www.google.ba/products + - www.google.be/products + - www.google.bf/products + - www.google.bg/products + - www.google.bi/products + - www.google.bj/products + - www.google.bs/products + - www.google.by/products + - www.google.ca/products + - www.google.cat/products + - www.google.cc/products + - www.google.cd/products + - www.google.cf/products + - www.google.cg/products + - www.google.ch/products + - www.google.ci/products + - www.google.cl/products + - www.google.cm/products + - www.google.cn/products + - www.google.co.bw/products + - www.google.co.ck/products + - www.google.co.cr/products + - www.google.co.id/products + - www.google.co.il/products + - www.google.co.in/products + - www.google.co.jp/products + - www.google.co.ke/products + - www.google.co.kr/products + - www.google.co.ls/products + - www.google.co.ma/products + - www.google.co.mz/products + - www.google.co.nz/products + - www.google.co.th/products + - www.google.co.tz/products + - www.google.co.ug/products + - www.google.co.uk/products + - www.google.co.uz/products + - www.google.co.ve/products + - www.google.co.vi/products + - www.google.co.za/products + - www.google.co.zm/products + - www.google.co.zw/products + - www.google.com/products + - www.google.com.af/products + - www.google.com.ag/products + - www.google.com.ai/products + - www.google.com.ar/products + - www.google.com.au/products + - www.google.com.bd/products + - www.google.com.bh/products + - www.google.com.bn/products + - www.google.com.bo/products + - www.google.com.br/products + - www.google.com.by/products + - www.google.com.bz/products + - www.google.com.co/products + - www.google.com.cu/products + - www.google.com.cy/products + - www.google.com.do/products + - www.google.com.ec/products + - www.google.com.eg/products + - www.google.com.et/products + - www.google.com.fj/products + - www.google.com.gh/products + - www.google.com.gi/products + - www.google.com.gt/products + - www.google.com.hk/products + - www.google.com.jm/products + - www.google.com.kh/products + - www.google.com.kw/products + - www.google.com.lb/products + - www.google.com.lc/products + - www.google.com.ly/products + - www.google.com.mt/products + - www.google.com.mx/products + - www.google.com.my/products + - www.google.com.na/products + - www.google.com.nf/products + - www.google.com.ng/products + - www.google.com.ni/products + - www.google.com.np/products + - www.google.com.om/products + - www.google.com.pa/products + - www.google.com.pe/products + - www.google.com.ph/products + - www.google.com.pk/products + - www.google.com.pr/products + - www.google.com.py/products + - www.google.com.qa/products + - www.google.com.sa/products + - www.google.com.sb/products + - www.google.com.sg/products + - www.google.com.sl/products + - www.google.com.sv/products + - www.google.com.tj/products + - www.google.com.tn/products + - www.google.com.tr/products + - www.google.com.tw/products + - www.google.com.ua/products + - www.google.com.uy/products + - www.google.com.vc/products + - www.google.com.vn/products + - www.google.cv/products + - www.google.cz/products + - www.google.de/products + - www.google.dj/products + - www.google.dk/products + - www.google.dm/products + - www.google.dz/products + - www.google.ee/products + - www.google.es/products + - www.google.fi/products + - www.google.fm/products + - www.google.fr/products + - www.google.ga/products + - www.google.gd/products + - www.google.ge/products + - www.google.gf/products + - www.google.gg/products + - www.google.gl/products + - www.google.gm/products + - www.google.gp/products + - www.google.gr/products + - www.google.gy/products + - www.google.hn/products + - www.google.hr/products + - www.google.ht/products + - www.google.hu/products + - www.google.ie/products + - www.google.im/products + - www.google.io/products + - www.google.iq/products + - www.google.is/products + - www.google.it/products + - www.google.it.ao/products + - www.google.je/products + - www.google.jo/products + - www.google.kg/products + - www.google.ki/products + - www.google.kz/products + - www.google.la/products + - www.google.li/products + - www.google.lk/products + - www.google.lt/products + - www.google.lu/products + - www.google.lv/products + - www.google.md/products + - www.google.me/products + - www.google.mg/products + - www.google.mk/products + - www.google.ml/products + - www.google.mn/products + - www.google.ms/products + - www.google.mu/products + - www.google.mv/products + - www.google.mw/products + - www.google.ne/products + - www.google.nl/products + - www.google.no/products + - www.google.nr/products + - www.google.nu/products + - www.google.pl/products + - www.google.pn/products + - www.google.ps/products + - www.google.pt/products + - www.google.ro/products + - www.google.rs/products + - www.google.ru/products + - www.google.rw/products + - www.google.sc/products + - www.google.se/products + - www.google.sh/products + - www.google.si/products + - www.google.sk/products + - www.google.sm/products + - www.google.sn/products + - www.google.so/products + - www.google.st/products + - www.google.td/products + - www.google.tg/products + - www.google.tk/products + - www.google.tl/products + - www.google.tm/products + - www.google.to/products + - www.google.tt/products + - www.google.us/products + - www.google.vg/products + - www.google.vu/products + - www.google.ws/products + + Google Video: + parameters: + - q + domains: + - video.google.com + + Goyellow.de: + parameters: + - MDN + domains: + - www.goyellow.de + + Gule Sider: + parameters: + - q + domains: + - www.gulesider.no + + HighBeam: + parameters: + - q + domains: + - www.highbeam.com + + Hit-Parade: + parameters: + - p7 + domains: + - req.-hit-parade.com + - class.hit-parade.com + - www.hit-parade.com + + Holmes: + parameters: + - q + domains: + - holmes.ge + + Hooseek.com: + parameters: + - recherche + domains: + - www.hooseek.com + + Hotbot: + parameters: + - query + domains: + - www.hotbot.com + + Icerockeet: + parameters: + - q + domains: + - blogs.icerocket.com + + ICQ: + parameters: + - q + domains: + - www.icq.com + - search.icq.com + + Ilse: + parameters: + - search_for + domains: + - www.ilse.nl + + Inbox.com: + parameters: + - q + domains: + - inbox.com/search/ + + InfoSpace: + parameters: + - q + - s + domains: + - infospace.com + - dogpile.com + - www.dogpile.com + - metacrawler.com + - webfetch.com + - webcrawler.com + - search.kiwee.com + # powered by InfoSpace + - isearch.babylon.com + - start.facemoods.com + - search.magnetic.com + - search.searchcompletion.com + - clusty.com + + Flyingbird: + parameters: + - q + domains: + - inspsearch.com + - viview.inspsearch.com + + Interia: + parameters: + - q + domains: + - www.google.interia.pl + + I-play: + parameters: + - q + domains: + - start.iplay.com + + I.ua: + parameters: + - q + domains: + - search.i.ua + + IXquick: + parameters: + - query + domains: + - ixquick.com + - www.eu.ixquick.com + - ixquick.de + - www.ixquick.de + - us.ixquick.com + - s1.us.ixquick.com + - s2.us.ixquick.com + - s3.us.ixquick.com + - s4.us.ixquick.com + - s5.us.ixquick.com + - eu.ixquick.com + - s8-eu.ixquick.com + - s1-eu.ixquick.de + + Jyxo: + parameters: + - q + domains: + - jyxo.1188.cz + + Jungle Spider: + parameters: + - q + domains: + - www.jungle-spider.de + + Jungle Key: + parameters: + - query + domains: + - junglekey.com + - junglekey.fr + + Kataweb: + parameters: + - q + domains: + - www.kataweb.it + + Kvasir: + parameters: + - q + domains: + - www.kvasir.no + + kununu: + parameters: + - q + domains: + - kununu.com + + Latne: + parameters: + - q + domains: + - www.latne.lv + + La Toile Du Quebec Via Google: + parameters: + - q + domains: + - www.toile.com + - web.toile.com + + Looksmart: + parameters: + - key + domains: + - www.looksmart.com + + Lo.st: + parameters: + - x_query + domains: + - lo.st + + Lycos: + parameters: + - query + domains: + - search.lycos.com + - www.lycos.com + - lycos.com + + maailm: + parameters: + - tekst + domains: + - www.maailm.com + + Mail.ru: + parameters: + - q + domains: + - go.mail.ru + + Mamma: + parameters: + - query + domains: + - www.mamma.com + - mamma75.mamma.com + + Marktplaats: + parameters: + - query + domains: + - www.marktplaats.nl + + Maxwebsearch: + parameters: + - query + domains: + - maxwebsearch.com + + Meta: + parameters: + - q + domains: + - meta.ua + + MetaCrawler.de: + parameters: + - qry + domains: + - s1.metacrawler.de + - s2.metacrawler.de + - s3.metacrawler.de + + Metager: + parameters: + - eingabe + domains: + - meta.rrzn.uni-hannover.de + - www.metager.de + + Metager2: + parameters: + - q + domains: + - metager2.de + + Meinestadt: + parameters: + - words + domains: + - www.meinestadt.de + + Mister Wong: + parameters: + - Keywords + domains: + - www.mister-wong.com + - www.mister-wong.de + + Monstercrawler: + parameters: + - qry + domains: + - www.monstercrawler.com + + Mozbot: + parameters: + - q + domains: + - www.mozbot.fr + - www.mozbot.co.uk + - www.mozbot.com + + El Mundo: + parameters: + - q + domains: + - ariadna.elmundo.es + + MySearch: + parameters: + - searchfor + - searchFor + domains: + - www.mysearch.com + - ms114.mysearch.com + - ms146.mysearch.com + - kf.mysearch.myway.com + - ki.mysearch.myway.com + - search.myway.com + - search.mywebsearch.com + + Najdi: + parameters: + - q + domains: + - www.najdi.si + + Nate: + parameters: + - q + domains: + - search.nate.com + + Naver: + parameters: + - query + domains: + - search.naver.com + + Naver Images: + parameters: + - query + domains: + - image.search.naver.com + - imagesearch.naver.com + + Needtofind: + parameters: + - searchfor + domains: + - ko.search.need2find.com + + Neti: + parameters: + - query + domains: + - www.neti.ee + + Nifty: + parameters: + - q + domains: + - search.nifty.com + + Nigma: + parameters: + - s + domains: + - nigma.ru + + Onet: + parameters: + - qt + domains: + - szukaj.onet.pl + + Online.no: + parameters: + - q + domains: + - online.no + + Opplysningen 1881: + parameters: + - Query + domains: + - www.1881.no + + Orange: + parameters: + - q + - kw + domains: + - busca.orange.es + - search.orange.co.uk + - lemoteur.orange.fr + + Paperball: + parameters: + - q + domains: + - www.paperball.de + + PeoplePC: + parameters: + - q + domains: + - search.peoplepc.com + + Picsearch: + parameters: + - q + domains: + - www.picsearch.com + + Plazoo: + parameters: + - q + domains: + - www.plazoo.com + + Poisk.ru: + parameters: + - q + domains: + - poisk.ru + + PriceRunner: + parameters: + - q + domains: + - www.pricerunner.co.uk + + qip: + parameters: + - query + domains: + - search.qip.ru + + Qualigo: + parameters: + - q + domains: + - www.qualigo.at + - www.qualigo.ch + - www.qualigo.de + - www.qualigo.nl + + Rakuten: + parameters: + - qt + domains: + - websearch.rakuten.co.jp + + Rambler: + parameters: + - query + - words + domains: + - nova.rambler.ru + + RPMFind: + parameters: + - query + domains: + - rpmfind.net + - fr2.rpmfind.net + + Road Runner Search: + parameters: + - q + domains: + - search.rr.com + + Sapo: + parameters: + - q + domains: + - pesquisa.sapo.pt + + # Add Scour.com + + Search This: + parameters: + - q + domains: + - www.searchthis.com + + Search.com: + parameters: + - q + domains: + - www.search.com + + Search.ch: + parameters: + - q + domains: + - www.search.ch + + Searchalot: + parameters: + - q + domains: + - searchalot.com + + SearchCanvas: + parameters: + - q + domains: + - www.searchcanvas.com + + Searchy: + parameters: + - q + domains: + - www.searchy.co.uk + + # Add setooz.com + + Seznam: + parameters: + - q + domains: + - search.seznam.cz + + Sharelook: + parameters: + - keyword + domains: + - www.sharelook.fr + + Skynet: + parameters: + - q + domains: + - www.skynet.be + + The Smart Search: + parameters: + - q + domains: + - thesmartsearch.net + - www.thesmartsearch.net + + Sogou: + parameters: + - query + - w + domains: + - www.sougou.com + - www.soso.com + + Softonic: + parameters: + - q + domains: + - search.softonic.com + + SoSoDesk: + parameters: + - q + domains: + - sosodesktop.com + - search.sosodesktop.com + + Snapdo: + parameters: + - q + domains: + - search.snapdo.com + + Startpagina: + parameters: + - q + domains: + - startgoogle.startpagina.nl + + Startsiden: + parameters: + - q + domains: + - www.startsiden.no + + suche.info: + parameters: + - q + domains: + - suche.info + + Suchmaschine.com: + parameters: + - suchstr + domains: + - www.suchmaschine.com + + Suchnase: + parameters: + - q + domains: + - www.suchnase.de + + TalkTalk: + parameters: + - query + domains: + - www.talktalk.co.uk + + Technorati: + parameters: + - q + domains: + - technorati.com + + Telstra: + parameters: + - find + domains: + - search.media.telstra.com.au + + Teoma: + parameters: + - q + domains: + - www.teoma.com + + Terra: + parameters: + - query + domains: + - buscador.terra.es + - buscador.terra.cl + - buscador.terra.com.br + + Tiscali: + parameters: + - q + - key + domains: + - search.tiscali.it + - search-dyn.tiscali.it + - hledani.tiscali.cz + + Tixuma: + parameters: + - sc + domains: + - www.tixuma.de + + T-Online: + parameters: + - q + domains: + - suche.t-online.de + - brisbane.t-online.de + - navigationshilfe.t-online.de + + Toolbarhome: + parameters: + - q + domains: + - www.toolbarhome.com + - vshare.toolbarhome.com + + Trouvez.com: + parameters: + - query + domains: + - www.trouvez.com + + TrovaRapido: + parameters: + - q + domains: + - www.trovarapido.com + + Trusted-Search: + parameters: + - w + domains: + - www.trusted--search.com + + Tut.by: + parameters: + - query + domains: + - search.tut.by + + Twingly: + parameters: + - q + domains: + - www.twingly.com + + UKR.net: + parameters: + - q + domains: + - search.ukr.net + + uol.com.br: + parameters: + - q + domains: + - busca.uol.com.br + + URL.ORGanizier: + parameters: + - q + domains: + - www.url.org + + Vinden: + parameters: + - q + domains: + - www.vinden.nl + + Vindex: + parameters: + - search_for + domains: + - www.vindex.nl + - search.vindex.nl + + Virgilio: + parameters: + - qs + domains: + - ricerca.virgilio.it + - ricercaimmagini.virgilio.it + - ricercavideo.virgilio.it + - ricercanews.virgilio.it + - mobile.virgilio.it + + Voila: + parameters: + - rdata + - kw + domains: + - search.ke.voila.fr + - www.lemoteur.fr + + Volny: + parameters: + - search + domains: + - web.volny.cz + + Walhello : + parameters: + - key + domains: + - www.walhello.info + - www.walhello.com + - www.walhello.de + - www.walhello.nl + + Web.de: + parameters: + - su + domains: + - suche.web.de + + Web.nl: + parameters: + - zoekwoord + domains: + - www.web.nl + + Weborama: + parameters: + - QUERY + domains: + - www.weborama.com + + WebSearch: + parameters: + - qkw + - q + domains: + - www.websearch.com + + Winamp: + parameters: + - q + domains: + - search.winamp.com + + Witch: + parameters: + - search + domains: + - www.witch.de + + Wirtualna Polska: + parameters: + - szukaj + domains: + - szukaj.wp.pl + + WWW: + parameters: + - query + domains: + - search.www.ee + + X-recherche: + parameters: + - MOTS + domains: + - www.x-recherche.com + + Yahoo!: + parameters: + - p + - q + domains: + - search.yahoo.com + - yahoo.com + - ar.search.yahoo.com + - ar.yahoo.com + - au.search.yahoo.com + - au.yahoo.com + - br.search.yahoo.com + - br.yahoo.com + - cade.searchde.yahoo.com + - cade.yahoo.com + - chinese.searchinese.yahoo.com + - chinese.yahoo.com + - cn.search.yahoo.com + - cn.yahoo.com + - de.search.yahoo.com + - de.yahoo.com + - dk.search.yahoo.com + - dk.yahoo.com + - es.search.yahoo.com + - es.yahoo.com + - espanol.searchpanol.yahoo.com + - espanol.yahoo.com + - fr.search.yahoo.com + - fr.yahoo.com + - ie.search.yahoo.com + - ie.yahoo.com + - it.search.yahoo.com + - it.yahoo.com + - kr.search.yahoo.com + - kr.yahoo.com + - mx.search.yahoo.com + - mx.yahoo.com + - no.search.yahoo.com + - no.yahoo.com + - nz.search.yahoo.com + - nz.yahoo.com + - one.cn.yahoo.com + - one.searchn.yahoo.com + - qc.search.yahoo.com + - qc.yahoo.com + - se.search.yahoo.com + - se.yahoo.com + - search.searcharch.yahoo.com + - uk.search.yahoo.com + - uk.yahoo.com + - www.yahoo.co.jp + - search.yahoo.co.jp + # powered by Yahoo + - www.cercato.it + - search.offerbox.com + - ys.mirostart.com + + Yahoo! Images: + parameters: + - p + - q + domains: + - image.yahoo.cn + - images.search.yahoo.com + + Yam: + parameters: + - k + domains: + - search.yam.com + + Yandex: + parameters: + - text + domains: + - yandex.ru + - yandex.ua + - yandex.com + - yandex.by + - www.yandex.ru + - www.yandex.ua + - www.yandex.com + - www.yandex.by + + Yandex Images: + parameters: + - text + domains: + - images.yandex.ru + - images.yandex.ua + - images.yandex.com + + Yasni: + parameters: + - query + domains: + - www.yasni.de + - www.yasni.com + - www.yasni.co.uk + - www.yasni.ch + - www.yasni.at + + Yatedo: + parameters: + - q + domains: + - www.yatedo.com + - www.yatedo.fr + + # Add Yellowmap: + + Yippy: + parameters: + - q + - query + domains: + - search.yippy.com + + YouGoo: + parameters: + - q + domains: + - www.yougoo.fr + + Zapmeta: + parameters: + - q + - query + domains: + - www.zapmeta.com + - www.zapmeta.nl + - www.zapmeta.de + - uk.zapmeta.com + + Zoek: + parameters: + - q + domains: + - www3.zoek.nl + + Zhongsou: + parameters: + - w + domains: + - p.zhongsou.com + + Zoeken: + parameters: + - q + domains: + - www.zoeken.nl + + Zoohoo: + parameters: + - q + domains: + - zoohoo.cz + + + +# ####################################################################################################### +# +# PAID MEDIA + +paid: + + Acuity Ads: + domains: + - acuityplatform.com + + Adform: + domains: + - adform.net + + AdRoll: + domains: + - adroll.com + + AppNexus: + domains: + - ib.adnxs.com + - adnxs.com + - 247realmedia.com + + AudienceScience: + domains: + - wunderloop.net + + BidSwitch: + domains: + - bidswitch.net + + Casale Media: + domains: + - casalemedia.com + + Criteo: + domains: + - cas.jp.as.criteo.com + - cas.criteo.com + + Doubleclick: + domains: + - ad.doubleclick.net + - ad-apac.doubleclick.net + - s0.2mdn.net + - s1.2mdn.net + - dp.g.doubleclick.net + - pubads.g.doubleclick.net + + Eyeota: + domains: + - eyeota.net + + Flashtalking: + domains: + - flashtalking.com + - servedby.flashtalking.com + + Fluct: + domains: + - adingo.jp + + Google: + domains: + - www.googleadservices.com + - partner.googleadservices.com + - googleads.g.doubleclick.net + - tpc.googlesyndication.com + - googleadservices.com + - imasdk.googleapis.com + + LifeStreet: + domains: + - lfstmedia.com + + Jivox: + domains: + - jivox.com + + MicroAd: + domains: + - microad.jp + + Mixpo: + domains: + - mixpo.com + + Mozo: + domains: + - mozo.com.au + - a.mozo.com.au + + Neustar AdAdvisor: + domains: + - adadvisor.net + + ONE by AOL: + domains: + - nexage.com + + OpenX: + domains: + - us-ads.openx.net + - openx.net + - servedbyopenx.com + - openxenterprise.com + + Outbrain: + domains: + - paid.outbrain.com + + Plista: + domains: + - farm.plista.com + + PubMatic: + domains: + - sshowads.pubmatic.com + + Rubicon Project: + domains: + - optimized-by.rubiconproject.com + + Sizmek: + domains: + - bs.serving-sys.com + + Sociomantic Labs: + domains: + - sociomantic.com + + Sonobi: + domains: + - sonobi.com + + Sovrn: + domains: + - lijit.com + + SteelHouse: + domains: + - steelhousemedia.com + + StickyADS.tv: + domains: + - stickyadstv.com + - sfx.stickyadstv.com + + Taboola: + domains: + - trc.taboola.com + - api.taboola.com + - taboola.com + + Tribal Fusion: + domains: + - cdnx.tribalfusion.com + + White Pages: + domains: + - www.whitepages.com.au + - mobile.whitepages.com.au + + Yieldmo: + domains: + - yieldmo.com + + ZEDO: + domains: + - zedo.com + - z1.zedo.com diff --git a/priv/repo/migrations/.formatter.exs b/priv/repo/migrations/.formatter.exs new file mode 100644 index 000000000..49f9151ed --- /dev/null +++ b/priv/repo/migrations/.formatter.exs @@ -0,0 +1,4 @@ +[ + import_deps: [:ecto_sql], + inputs: ["*.exs"] +] diff --git a/priv/repo/migrations/20181201181549_add_pageviews.exs b/priv/repo/migrations/20181201181549_add_pageviews.exs new file mode 100644 index 000000000..f728dfbb0 --- /dev/null +++ b/priv/repo/migrations/20181201181549_add_pageviews.exs @@ -0,0 +1,16 @@ +defmodule Plausible.Repo.Migrations.AddPageviews do + use Ecto.Migration + + def change do + create table(:pageviews) do + add :hostname, :text, null: false + add :pathname, :text, null: false + add :referrer, :text + add :user_agent, :text + add :screen_width, :integer + add :screen_height, :integer + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20181214201821_add_new_visitor_to_pageviews.exs b/priv/repo/migrations/20181214201821_add_new_visitor_to_pageviews.exs new file mode 100644 index 000000000..1b91b26c5 --- /dev/null +++ b/priv/repo/migrations/20181214201821_add_new_visitor_to_pageviews.exs @@ -0,0 +1,16 @@ +defmodule Plausible.Repo.Migrations.AddNewVisitorToPageviews do + use Ecto.Migration + use Plausible.Repo + + def change do + alter table(:pageviews) do + add :new_visitor, :boolean + end + flush() + Plausible.Repo.update_all(Plausible.Pageview, [set: [new_visitor: true]]) + flush() + alter table(:pageviews) do + modify :new_visitor, :boolean, null: false + end + end +end diff --git a/priv/repo/migrations/20181215140923_add_session_id_to_pageviews.exs b/priv/repo/migrations/20181215140923_add_session_id_to_pageviews.exs new file mode 100644 index 000000000..37144338d --- /dev/null +++ b/priv/repo/migrations/20181215140923_add_session_id_to_pageviews.exs @@ -0,0 +1,9 @@ +defmodule Plausible.Repo.Migrations.AddSessionIdToPageviews do + use Ecto.Migration + + def change do + alter table(:pageviews) do + add :session_id, :string + end + end +end diff --git a/priv/repo/migrations/20190109173917_create_sites.exs b/priv/repo/migrations/20190109173917_create_sites.exs new file mode 100644 index 000000000..564ed7a3f --- /dev/null +++ b/priv/repo/migrations/20190109173917_create_sites.exs @@ -0,0 +1,30 @@ +defmodule Plausible.Repo.Migrations.CreateSites do + use Ecto.Migration + + def change do + create table(:users) do + add :email, :string, null: false + + timestamps() + end + + create unique_index(:users, :email) + + create table(:sites) do + add :domain, :string, null: false + + timestamps() + end + + create unique_index(:sites, :domain) + + create table(:site_memberships) do + add :site_id, references(:sites), null: false + add :user_id, references(:users), null: false + + timestamps() + end + + create unique_index(:site_memberships, [:site_id, :user_id]) + end +end diff --git a/priv/repo/migrations/20190117135714_add_uid_to_pageviews.exs b/priv/repo/migrations/20190117135714_add_uid_to_pageviews.exs new file mode 100644 index 000000000..3876b8bc0 --- /dev/null +++ b/priv/repo/migrations/20190117135714_add_uid_to_pageviews.exs @@ -0,0 +1,18 @@ +defmodule Plausible.Repo.Migrations.AddUidToPageviews do + use Ecto.Migration + use Plausible.Repo + + def change do + alter table(:pageviews) do + add :user_id, :binary_id + end + + flush() + + Repo.update_all(Plausible.Pageview, set: [user_id: "00029281-7f8b-462d-a9f0-0d2ddfc6ea02"]) + + alter table(:pageviews) do + modify :user_id, :string, null: false + end + end +end diff --git a/priv/repo/migrations/20190118154210_add_derived_data_to_pageviews.exs b/priv/repo/migrations/20190118154210_add_derived_data_to_pageviews.exs new file mode 100644 index 000000000..a372c3406 --- /dev/null +++ b/priv/repo/migrations/20190118154210_add_derived_data_to_pageviews.exs @@ -0,0 +1,14 @@ +defmodule Plausible.Repo.Migrations.AddDerivedDataToPageviews do + use Ecto.Migration + use Plausible.Repo + + def change do + alter table(:pageviews) do + add :device_type, :string + add :browser, :string + add :operating_system, :string + add :referrer_source, :string + add :screen_size, :string + end + end +end diff --git a/priv/repo/migrations/20190126135857_add_name_to_users.exs b/priv/repo/migrations/20190126135857_add_name_to_users.exs new file mode 100644 index 000000000..92fbc0acb --- /dev/null +++ b/priv/repo/migrations/20190126135857_add_name_to_users.exs @@ -0,0 +1,9 @@ +defmodule Plausible.Repo.Migrations.AddNameToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add :name, :string + end + end +end diff --git a/priv/repo/migrations/20190127213938_add_tz_to_sites.exs b/priv/repo/migrations/20190127213938_add_tz_to_sites.exs new file mode 100644 index 000000000..570bb1b63 --- /dev/null +++ b/priv/repo/migrations/20190127213938_add_tz_to_sites.exs @@ -0,0 +1,18 @@ +defmodule Plausible.Repo.Migrations.AddTzToSites do + use Ecto.Migration + use Plausible.Repo + + def change do + alter table(:sites) do + add :timezone, :string + end + + flush() + + Repo.update_all(Plausible.Site, [set: [timezone: "UTC"]]) + + alter table(:sites) do + modify :timezone, :string, null: false + end + end +end diff --git a/priv/repo/migrations/20190205165931_add_last_seen_to_users.exs b/priv/repo/migrations/20190205165931_add_last_seen_to_users.exs new file mode 100644 index 000000000..24b556acb --- /dev/null +++ b/priv/repo/migrations/20190205165931_add_last_seen_to_users.exs @@ -0,0 +1,9 @@ +defmodule Plausible.Repo.Migrations.AddLastSeenToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add :last_seen, :naive_datetime, default: fragment("now()") + end + end +end diff --git a/priv/repo/migrations/20190213224404_add_intro_emails.exs b/priv/repo/migrations/20190213224404_add_intro_emails.exs new file mode 100644 index 000000000..ce2789002 --- /dev/null +++ b/priv/repo/migrations/20190213224404_add_intro_emails.exs @@ -0,0 +1,10 @@ +defmodule Plausible.Repo.Migrations.AddIntroEmails do + use Ecto.Migration + + def change do + create table(:intro_emails) do + add :user_id, references(:users), null: false + add :timestamp, :naive_datetime + end + end +end diff --git a/priv/repo/migrations/20190219130809_delete_intro_emails_when_user_is_deleted.exs b/priv/repo/migrations/20190219130809_delete_intro_emails_when_user_is_deleted.exs new file mode 100644 index 000000000..3490fc1ee --- /dev/null +++ b/priv/repo/migrations/20190219130809_delete_intro_emails_when_user_is_deleted.exs @@ -0,0 +1,9 @@ +defmodule Plausible.Repo.Migrations.DeleteIntroEmailsWhenUserIsDeleted do + use Ecto.Migration + + def change do + alter table(:intro_emails) do + modify :user_id, references(:users, on_delete: :delete_all), null: false, from: references(:users) + end + end +end diff --git a/priv/repo/migrations/20190301122344_add_country_code_to_pageviews.exs b/priv/repo/migrations/20190301122344_add_country_code_to_pageviews.exs new file mode 100644 index 000000000..287cfe9c8 --- /dev/null +++ b/priv/repo/migrations/20190301122344_add_country_code_to_pageviews.exs @@ -0,0 +1,9 @@ +defmodule Plausible.Repo.Migrations.AddCountryCodeToPageviews do + use Ecto.Migration + + def change do + alter table(:pageviews) do + add :country_code, :string, size: 2 + end + end +end diff --git a/priv/repo/migrations/20190324155606_add_password_hash_to_users.exs b/priv/repo/migrations/20190324155606_add_password_hash_to_users.exs new file mode 100644 index 000000000..73b844eaf --- /dev/null +++ b/priv/repo/migrations/20190324155606_add_password_hash_to_users.exs @@ -0,0 +1,9 @@ +defmodule Plausible.Repo.Migrations.AddPasswordHashToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add :password_hash, :string + end + end +end diff --git a/priv/repo/migrations/20190402145007_remove_device_type_from_pageviews.exs b/priv/repo/migrations/20190402145007_remove_device_type_from_pageviews.exs new file mode 100644 index 000000000..1cbdde2f5 --- /dev/null +++ b/priv/repo/migrations/20190402145007_remove_device_type_from_pageviews.exs @@ -0,0 +1,9 @@ +defmodule Plausible.Repo.Migrations.RemoveDeviceTypeFromPageviews do + use Ecto.Migration + + def change do + alter table(:pageviews) do + remove :device_type + end + end +end diff --git a/priv/repo/migrations/20190402145357_remove_screen_height_from_pageviews.exs b/priv/repo/migrations/20190402145357_remove_screen_height_from_pageviews.exs new file mode 100644 index 000000000..9797e00f4 --- /dev/null +++ b/priv/repo/migrations/20190402145357_remove_screen_height_from_pageviews.exs @@ -0,0 +1,9 @@ +defmodule Plausible.Repo.Migrations.RemoveScreenHeightFromPageviews do + use Ecto.Migration + + def change do + alter table(:pageviews) do + remove :screen_height + end + end +end diff --git a/priv/repo/migrations/20190402172423_add_index_to_pageviews.exs b/priv/repo/migrations/20190402172423_add_index_to_pageviews.exs new file mode 100644 index 000000000..3f394fcff --- /dev/null +++ b/priv/repo/migrations/20190402172423_add_index_to_pageviews.exs @@ -0,0 +1,7 @@ +defmodule Plausible.Repo.Migrations.AddIndexToPageviews do + use Ecto.Migration + + def change do + create index("pageviews", [:hostname]) + end +end diff --git a/priv/repo/migrations/20190410095248_add_feedback_emails.exs b/priv/repo/migrations/20190410095248_add_feedback_emails.exs new file mode 100644 index 000000000..bc1f62e45 --- /dev/null +++ b/priv/repo/migrations/20190410095248_add_feedback_emails.exs @@ -0,0 +1,10 @@ +defmodule Plausible.Repo.Migrations.AddFeedbackEmails do + use Ecto.Migration + + def change do + create table(:feedback_emails) do + add :user_id, references(:users), null: false + add :timestamp, :naive_datetime, null: false + end + end +end diff --git a/priv/repo/migrations/20190424162903_delete_feedback_emails_when_user_is_deleted.exs b/priv/repo/migrations/20190424162903_delete_feedback_emails_when_user_is_deleted.exs new file mode 100644 index 000000000..681644237 --- /dev/null +++ b/priv/repo/migrations/20190424162903_delete_feedback_emails_when_user_is_deleted.exs @@ -0,0 +1,9 @@ +defmodule Plausible.Repo.Migrations.DeleteFeedbackEmailsWhenUserIsDeleted do + use Ecto.Migration + + def change do + alter table(:feedback_emails) do + modify :user_id, references(:users, on_delete: :delete_all), null: false, from: references(:users) + end + end +end diff --git a/priv/repo/migrations/20190430140411_use_citext_for_email.exs b/priv/repo/migrations/20190430140411_use_citext_for_email.exs new file mode 100644 index 000000000..ecb6f805e --- /dev/null +++ b/priv/repo/migrations/20190430140411_use_citext_for_email.exs @@ -0,0 +1,11 @@ +defmodule Plausible.Repo.Migrations.UseCitextForEmail do + use Ecto.Migration + + def change do + execute "CREATE EXTENSION IF NOT EXISTS citext;" + + alter table(:users) do + modify :email, :citext, null: false + end + end +end diff --git a/priv/repo/migrations/20190430152923_create_subscriptions.exs b/priv/repo/migrations/20190430152923_create_subscriptions.exs new file mode 100644 index 000000000..fb44bfacd --- /dev/null +++ b/priv/repo/migrations/20190430152923_create_subscriptions.exs @@ -0,0 +1,20 @@ +defmodule Plausible.Repo.Migrations.CreateSubscriptions do + use Ecto.Migration + + def change do + create table(:subscriptions) do + add :paddle_subscription_id, :string, null: false + add :paddle_plan_id, :string, null: false + add :user_id, references(:users), null: false + add :update_url, :text, null: false + add :cancel_url, :text, null: false + add :status, :string, null: false + add :next_bill_amount, :string, null: false + add :next_bill_date, :date, null: false + + timestamps() + end + + create unique_index(:subscriptions, [:paddle_subscription_id]) + end +end diff --git a/priv/repo/migrations/20190516113517_remove_session_id_from_pageviews.exs b/priv/repo/migrations/20190516113517_remove_session_id_from_pageviews.exs new file mode 100644 index 000000000..af88e4b27 --- /dev/null +++ b/priv/repo/migrations/20190516113517_remove_session_id_from_pageviews.exs @@ -0,0 +1,9 @@ +defmodule Plausible.Repo.Migrations.RemoveSessionIdFromPageviews do + use Ecto.Migration + + def change do + alter table(:pageviews) do + remove :session_id + end + end +end diff --git a/priv/repo/migrations/20190520144229_change_user_id_to_uuid.exs b/priv/repo/migrations/20190520144229_change_user_id_to_uuid.exs new file mode 100644 index 000000000..90282aa15 --- /dev/null +++ b/priv/repo/migrations/20190520144229_change_user_id_to_uuid.exs @@ -0,0 +1,10 @@ +defmodule Plausible.Repo.Migrations.ChangeUserIdToUuid do + use Ecto.Migration + + def change do + execute("DELETE from pageviews where user_id='123'") + execute("UPDATE pageviews set user_id='de610e53-6ec6-4e33-be37-7adcc1bf13be' where user_id='dummy'") + flush() + execute("ALTER TABLE pageviews ALTER COLUMN user_id TYPE uuid USING user_id::uuid") + end +end diff --git a/priv/repo/migrations/20190523160838_add_raw_referrer.exs b/priv/repo/migrations/20190523160838_add_raw_referrer.exs new file mode 100644 index 000000000..46db66257 --- /dev/null +++ b/priv/repo/migrations/20190523160838_add_raw_referrer.exs @@ -0,0 +1,19 @@ +defmodule Plausible.Repo.Migrations.AddRawReferrer do + use Ecto.Migration + + def change do + alter table(:pageviews) do + add :raw_referrer, :text + end + + flush() + + execute "UPDATE pageviews set raw_referrer = referrer" + + flush() + + execute """ + UPDATE pageviews SET referrer = split_part(split_part(regexp_replace(regexp_replace(regexp_replace(raw_referrer, '^https://', ''), '^http://', ''), '^www\.', ''), '?', 1), '#', 1) + """ + end +end diff --git a/priv/repo/migrations/20190523171519_add_indices_to_referrers.exs b/priv/repo/migrations/20190523171519_add_indices_to_referrers.exs new file mode 100644 index 000000000..cde0af5d9 --- /dev/null +++ b/priv/repo/migrations/20190523171519_add_indices_to_referrers.exs @@ -0,0 +1,8 @@ +defmodule Plausible.Repo.Migrations.AddIndicesToReferrers do + use Ecto.Migration + + def change do + create index(:pageviews, :referrer_source) + create index(:pageviews, :referrer) + end +end diff --git a/priv/repo/migrations/20190618165016_add_public_sites.exs b/priv/repo/migrations/20190618165016_add_public_sites.exs new file mode 100644 index 000000000..a4bcc892c --- /dev/null +++ b/priv/repo/migrations/20190618165016_add_public_sites.exs @@ -0,0 +1,12 @@ +defmodule Plausible.Repo.Migrations.AddPublicSites do + use Ecto.Migration + alias Plausible.Repo + + def change do + alter table(:sites) do + add :public, :boolean, null: false, default: false + end + + execute "update sites set public=true where domain='plausible.io'" + end +end diff --git a/priv/repo/migrations/20190718160353_create_google_search_console_integration.exs b/priv/repo/migrations/20190718160353_create_google_search_console_integration.exs new file mode 100644 index 000000000..ed869236b --- /dev/null +++ b/priv/repo/migrations/20190718160353_create_google_search_console_integration.exs @@ -0,0 +1,17 @@ +defmodule Plausible.Repo.Migrations.CreateGoogleSearchConsoleIntegration do + use Ecto.Migration + + def change do + create table(:google_auth) do + add :user_id, references(:users), null: false + add :email, :string, null: false + add :refresh_token, :string, null: false + add :access_token, :string, null: false + add :expires, :naive_datetime, null: false + + timestamps() + end + + create unique_index(:google_auth, :user_id) + end +end diff --git a/priv/repo/migrations/20190723141824_associate_google_auth_with_site.exs b/priv/repo/migrations/20190723141824_associate_google_auth_with_site.exs new file mode 100644 index 000000000..d6ce39af0 --- /dev/null +++ b/priv/repo/migrations/20190723141824_associate_google_auth_with_site.exs @@ -0,0 +1,12 @@ +defmodule Plausible.Repo.Migrations.AssociateGoogleAuthWithSite do + use Ecto.Migration + + def change do + alter table(:google_auth) do + add :site_id, references(:sites), null: false + end + + drop unique_index(:google_auth, :user_id) + create unique_index(:google_auth, :site_id) + end +end diff --git a/priv/repo/migrations/20190730014913_add_monthly_stats.exs b/priv/repo/migrations/20190730014913_add_monthly_stats.exs new file mode 100644 index 000000000..ccb53eeaa --- /dev/null +++ b/priv/repo/migrations/20190730014913_add_monthly_stats.exs @@ -0,0 +1,17 @@ +defmodule Plausible.Repo.Migrations.AddMonthlyStats do + use Ecto.Migration + + def change do + create table(:monthly_stats) do + add :month, :date, null: false + add :visitors, :integer, null: false + add :pageviews, :integer, null: false + add :site_id, references(:sites), null: false + + timestamps() + end + + create index(:monthly_stats, :site_id) + create index(:monthly_stats, :month) + end +end diff --git a/priv/repo/migrations/20190730142200_add_weekly_stats.exs b/priv/repo/migrations/20190730142200_add_weekly_stats.exs new file mode 100644 index 000000000..05902f11f --- /dev/null +++ b/priv/repo/migrations/20190730142200_add_weekly_stats.exs @@ -0,0 +1,17 @@ +defmodule Plausible.Repo.Migrations.AddWeeklyStats do + use Ecto.Migration + + def change do + create table(:weekly_stats) do + add :week, :date, null: false + add :visitors, :integer, null: false + add :pageviews, :integer, null: false + add :site_id, references(:sites), null: false + + timestamps() + end + + create index(:weekly_stats, :site_id) + create index(:weekly_stats, :week) + end +end diff --git a/priv/repo/migrations/20190730144413_add_daily_stats.exs b/priv/repo/migrations/20190730144413_add_daily_stats.exs new file mode 100644 index 000000000..3da91cd6c --- /dev/null +++ b/priv/repo/migrations/20190730144413_add_daily_stats.exs @@ -0,0 +1,17 @@ +defmodule Plausible.Repo.Migrations.AddDailyStats do + use Ecto.Migration + + def change do + create table(:daily_stats) do + add :date, :date, null: false + add :visitors, :integer, null: false + add :pageviews, :integer, null: false + add :site_id, references(:sites), null: false + + timestamps() + end + + create index(:daily_stats, :site_id) + create index(:daily_stats, :date) + end +end diff --git a/priv/repo/migrations/20190809174105_calc_screen_size.exs b/priv/repo/migrations/20190809174105_calc_screen_size.exs new file mode 100644 index 000000000..19e789765 --- /dev/null +++ b/priv/repo/migrations/20190809174105_calc_screen_size.exs @@ -0,0 +1,17 @@ +defmodule Plausible.Repo.Migrations.CalcScreenSize do + use Ecto.Migration + + def change do + execute """ + UPDATE pageviews SET screen_size= ( + CASE + WHEN screen_width is null THEN null + WHEN screen_width < 576 THEN 'Mobile' + WHEN screen_width < 992 THEN 'Tablet' + WHEN screen_width < 1440 THEN 'Laptop' + ELSE 'Desktop' + END + ); + """ + end +end diff --git a/priv/repo/migrations/20190810145419_remove_unused_indices.exs b/priv/repo/migrations/20190810145419_remove_unused_indices.exs new file mode 100644 index 000000000..69ae01f90 --- /dev/null +++ b/priv/repo/migrations/20190810145419_remove_unused_indices.exs @@ -0,0 +1,8 @@ +defmodule Plausible.Repo.Migrations.RemoveUnusedIndices do + use Ecto.Migration + + def change do + drop index(:pageviews, [:referrer]) + drop index(:pageviews, [:referrer_source]) + end +end diff --git a/priv/repo/migrations/20190820140747_remove_rollup_tables.exs b/priv/repo/migrations/20190820140747_remove_rollup_tables.exs new file mode 100644 index 000000000..59aa930df --- /dev/null +++ b/priv/repo/migrations/20190820140747_remove_rollup_tables.exs @@ -0,0 +1,9 @@ +defmodule Plausible.Repo.Migrations.RemoveRollupTables do + use Ecto.Migration + + def change do + drop table(:daily_stats) + drop table(:weekly_stats) + drop table(:monthly_stats) + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs new file mode 100644 index 000000000..b4931d33b --- /dev/null +++ b/priv/repo/seeds.exs @@ -0,0 +1,11 @@ +# Script for populating the database. You can run it as: +# +# mix run priv/repo/seeds.exs +# +# Inside the script, you can read and write to any of your +# repositories directly: +# +# Plausible.Repo.insert!(%Plausible.SomeSchema{}) +# +# We recommend using the bang functions (`insert!`, `update!` +# and so on) as they will fail if something goes wrong. diff --git a/priv/ua_inspector/bot.bots.yml b/priv/ua_inspector/bot.bots.yml new file mode 100644 index 000000000..541d5e95c --- /dev/null +++ b/priv/ua_inspector/bot.bots.yml @@ -0,0 +1,1738 @@ +############### +# Device Detector - The Universal Device Detection library for parsing User Agents +# +# @link http://piwik.org +# @license http://www.gnu.org/licenses/lgpl.html LGPL v3 or later +############### + +- regex: '360Spider(-Image|-Video)?' + name: '360Spider' + category: 'Search bot' + url: 'http://www.so.com/help/help_3_2.html' + producer: + name: 'Online Media Group, Inc.' + url: '' + +- regex: 'Aboundex' + name: 'Aboundexbot' + category: 'Search bot' + url: 'http://www.aboundex.com/crawler/' + producer: + name: 'Aboundex.com' + url: 'http://www.aboundex.com' + +- regex: 'AcoonBot' + name: 'Acoon' + category: 'Search bot' + url: 'http://www.acoon.de/robot.asp' + producer: + name: 'Acoon GmbH' + url: 'http://www.acoon.de' + +- regex: 'AddThis\.com' + name: 'AddThis.com' + category: 'Social Media Agent' + url: '' + producer: + name: 'Clearspring Technologies, Inc.' + url: 'http://www.clearspring.com' + +- regex: 'AhrefsBot' + name: 'aHrefs Bot' + category: 'Crawler' + url: 'http://ahrefs.com/robot' + producer: + name: 'Ahrefs Pte Ltd' + url: 'http://ahrefs.com/robot' + +- regex: 'ia_archiver|alexabot|verifybot' + name: 'Alexa Crawler' + category: 'Search bot' + url: 'https://alexa.zendesk.com/hc/en-us/sections/200100794-Crawlers' + producer: + name: 'Alexa Internet' + url: 'http://www.alexa.com' + +- regex: 'alexa site audit' + name: 'Alexa Site Audit' + category: 'Site Monitor' + url: 'http://www.alexa.com/help/webmasters' + producer: + name: 'Alexa Internet' + url: 'http://www.alexa.com' + +- regex: 'AmorankSpider' + name: 'Amorank Spider' + category: 'Crawler' + url: 'http://amorank.com/webcrawler.html' + producer: + name: 'Amorank' + url: 'http://www.amorank.com' + +- regex: 'ApacheBench' + name: 'ApacheBench' + category: 'Benchmark' + url: 'https://httpd.apache.org/docs/2.4/programs/ab.html' + producer: + name: 'The Apache Software Foundation' + url: 'http://www.apache.org/foundation/' + +- regex: 'Applebot' + name: 'Applebot' + category: 'Crawler' + url: 'http://www.apple.com/go/applebot' + producer: + name: 'Apple Inc' + url: 'http://www.apple.com' + +- regex: 'Arachni' + name: 'Arachni' + category: 'Security Checker' + url: 'http://www.arachni-scanner.com' + producer: + name: 'Sarosys LLC' + url: 'http://www.sarosys.com/' + +- regex: 'Castro 2, Episode Duration Lookup' + name: 'Castro 2' + category: 'Service Agent' + url: 'http://supertop.co/castro/' + producer: + name: 'Supertop' + url: 'http://supertop.co' + +- regex: 'Curious George' + name: 'Analytics SEO Crawler' + category: 'Crawler' + url: 'http://www.analyticsseo.com/crawler' + producer: + name: 'Analytics SEO' + url: 'http://www.analyticsseo.com' + +- regex: 'archive\.org_bot|special_archiver' + name: 'archive.org bot' + category: 'Crawler' + url: 'http://www.archive.org/details/archive.org_bot' + producer: + name: 'The Internet Archive' + url: 'http://www.archive.org' + +- regex: 'Ask Jeeves/Teoma' + name: 'Ask Jeeves' + category: 'Search bot' + url: '' + producer: + name: 'Ask Jeeves Inc.' + url: 'http://www.ask.com' + +- regex: 'Backlink-Check\.de' + name: 'Backlink-Check.de' + category: 'Crawler' + url: 'http://www.backlink-check.de/bot.html' + producer: + name: 'Mediagreen Medienservice' + url: 'http://www.backlink-check.de' + +- regex: 'BacklinkCrawler' + name: 'BacklinkCrawler' + category: 'Crawler' + url: 'http://www.backlinktest.com/crawler.html' + producer: + name: '2.0Promotion GbR' + url: 'http://www.backlinktest.com' + +- regex: 'baiduspider(-image)?|baidu Transcoder|baidu.*spider' + name: 'Baidu Spider' + category: 'Search bot' + url: 'http://www.baidu.com/search/spider.htm' + producer: + name: 'Baidu' + url: 'http://www.baidu.com' + +- regex: 'BazQux' + name: 'BazQux Reader' + url: 'https://bazqux.com/fetcher' + category: 'Feed Fetcher' + producer: + name: '' + url: '' + +- regex: 'MSNBot|msrbot|bingbot|BingPreview|msnbot-(UDiscovery|NewsBlogs)|adidxbot' + name: 'BingBot' + category: 'Search bot' + url: 'http://search.msn.com/msnbot.htmn' + producer: + name: 'Microsoft Corporation' + url: 'http://www.microsoft.com' + +- regex: 'Blekkobot' + name: 'Blekkobot' + category: 'Search bot' + url: 'http://blekko.com/about/blekkobot' + producer: + name: 'Blekko' + url: 'http://blekko.com' + +- regex: 'BLEXBot(Test)?' + name: 'BLEXBot Crawler' + category: 'Crawler' + url: 'http://webmeup-crawler.com' + producer: + name: 'WebMeUp' + url: 'http://webmeup.com' + +- regex: 'Bloglovin' + name: 'Bloglovin' + url: 'http://www.bloglovin.com' + category: 'Feed Fetcher' + producer: + name: '' + url: '' + +- regex: 'Blogtrottr' + name: 'Blogtrottr' + url: '' + category: 'Feed Fetcher' + producer: + name: 'Blogtrottr Ltd' + url: 'https://blogtrottr.com/' + +- regex: 'BountiiBot' + name: 'Bountii Bot' + category: 'Search bot' + url: 'http://bountii.com/contact.php' + producer: + name: 'Bountii Inc.' + url: 'http://bountii.com' + +- regex: 'Browsershots' + name: 'Browsershots' + category: 'Service Agent' + url: 'http://browsershots.org/faq' + producer: + name: 'Browsershots.org' + url: 'http://browsershots.org' + +- regex: 'BUbiNG' + name: 'BUbiNG' + category: 'Crawler' + url: 'http://law.di.unimi.it/BUbiNG.html' + producer: + name: 'The Laboratory for Web Algorithmics (LAW)' + url: 'http://law.di.unimi.it/software.php#buging' + +- regex: '(? end of file + +########## +# webOS +########## +- regex: '(?:webOS|Palm webOS)(?:/(\d+[\.\d]+))?' + name: 'webOS' + version: '$1' + +- regex: '(?:PalmOS|Palm OS)(?:[/ ](\d+[\.\d]+))?|Palm' + name: 'palmOS' + version: '$1' + +- regex: 'Xiino(?:.*v\. (\d+[\.\d]+))?' # palmOS only browser + name: 'palmOS' + version: '$1' + + +- regex: 'MorphOS(?:[ /](\d+[\.\d]+))?' + name: 'MorphOS' + version: '$1' + + +########## +# Windows +########## +- regex: 'CYGWIN_NT-10.0|Windows NT 10.0|Windows 10' + name: 'Windows' + version: '10' + +- regex: 'CYGWIN_NT-6.4|Windows NT 6.4|Windows 10' + name: 'Windows' + version: '10' + +- regex: 'CYGWIN_NT-6.3|Windows NT 6.3|Windows 8.1' + name: 'Windows' + version: '8.1' + + +- regex: 'CYGWIN_NT-6.2|Windows NT 6.2|Windows 8' + name: 'Windows' + version: '8' + + +- regex: 'CYGWIN_NT-6.1|Windows NT 6.1|Windows 7' + name: 'Windows' + version: '7' + + +- regex: 'CYGWIN_NT-6.0|Windows NT 6.0|Windows Vista' + name: 'Windows' + version: 'Vista' + + +- regex: 'CYGWIN_NT-5.2|Windows NT 5.2|Windows Server 2003 / XP x64' + name: 'Windows' + version: 'Server 2003' + + +- regex: 'CYGWIN_NT-5.1|Windows NT 5.1|Windows XP' + name: 'Windows' + version: 'XP' + + +- regex: 'CYGWIN_NT-5.0|Windows NT 5.0|Windows 2000' + name: 'Windows' + version: '2000' + + +- regex: 'CYGWIN_NT-4.0|Windows NT 4.0|WinNT|Windows NT' + name: 'Windows' + version: 'NT' + + +- regex: 'CYGWIN_ME-4.90|Win 9x 4.90|Windows ME' + name: 'Windows' + version: 'ME' + + +- regex: 'CYGWIN_98-4.10|Win98|Windows 98' + name: 'Windows' + version: '98' + + +- regex: 'CYGWIN_95-4.0|Win32|Win95|Windows 95|Windows_95' + name: 'Windows' + version: '95' + + +- regex: 'Windows 3.1' + name: 'Windows' + version: '3.1' + + +- regex: 'Windows' + name: 'Windows' + version: '' + + +########## +# Haiku OS +########## +- regex: 'Haiku' + name: 'Haiku OS' + version: '' + + +########## +# iOS +########## +- regex: 'CFNetwork/889' + name: 'iOS' + version: '11.1' + +- regex: 'CFNetwork/887.*(x86_64)' + name: 'Mac' + version: '10.13' + +- regex: 'CFNetwork/887' + name: 'iOS' + version: '11.0' + +- regex: 'CFNetwork/811.*(x86_64)' + name: 'Mac' + version: '10.12' + +- regex: 'CFNetwork/811' + name: 'iOS' + version: '10.3' + +- regex: 'CFNetwork/808\.3' + name: 'iOS' + version: '10.3' + +- regex: 'CFNetwork/808\.2' + name: 'iOS' + version: '10.2' + +- regex: 'CFNetwork/808\.1' + name: 'iOS' + version: '10.1' + +- regex: 'CFNetwork/808\.0' + name: 'iOS' + version: '10.0' + +- regex: 'CFNetwork/808' + name: 'iOS' + version: '10' + +- regex: 'CFNetwork/758\.4\.3' + name: 'iOS' + version: '9.3.2' + +- regex: 'CFNetwork/758\.3\.15' + name: 'iOS' + version: '9.3' + +- regex: 'CFNetwork/758\.2\.[78]' + name: 'iOS' + version: '9.2' + +- regex: 'CFNetwork/758\.1\.6' + name: 'iOS' + version: '9.1' + +- regex: 'CFNetwork/758\.0\.2' + name: 'iOS' + version: '9.0' + +- regex: 'CFNetwork/711\.5\.6' + name: 'iOS' + version: '8.4.1' + +- regex: 'CFNetwork/711\.4\.6' + name: 'iOS' + version: '8.4' + +- regex: 'CFNetwork/711\.3\.18' + name: 'iOS' + version: '8.3' + +- regex: 'CFNetwork/711\.2\.23' + name: 'iOS' + version: '8.2' + +- regex: 'CFNetwork/711\.1\.1[26]' + name: 'iOS' + version: '8.1' + +- regex: 'CFNetwork/711\.0\.6' + name: 'iOS' + version: '8.0' + +- regex: 'CFNetwork/672\.1' + name: 'iOS' + version: '7.1' + +- regex: 'CFNetwork/672\.0' + name: 'iOS' + version: '7.0' + +- regex: 'CFNetwork/609\.1' + name: 'iOS' + version: '6.1' + +- regex: 'CFNetwork/60[29]' + name: 'iOS' + version: '6.0' + +- regex: 'CFNetwork/548\.1' + name: 'iOS' + version: '5.1' + +- regex: 'CFNetwork/548\.0' + name: 'iOS' + version: '5.0' + +- regex: 'CFNetwork/485\.13' + name: 'iOS' + version: '4.3' + +- regex: 'CFNetwork/485\.12' + name: 'iOS' + version: '4.2' + +- regex: 'CFNetwork/485\.10' + name: 'iOS' + version: '4.1' + +- regex: 'CFNetwork/485\.2' + name: 'iOS' + version: '4.0' + +- regex: 'CFNetwork/467\.12' + name: 'iOS' + version: '3.2' + +- regex: 'CFNetwork/459' + name: 'iOS' + version: '3.1' + +- regex: '(?:CPU OS|iPh(?:one)?[ _]OS|iOS)[ _/](\d+(?:[_\.]\d+)*)' + name: 'iOS' + version: '$1' + +- regex: '(?:Apple-)?(?:iPhone|iPad|iPod)(?:.*Mac OS X.*Version/(\d+\.\d+)|; Opera)?' + name: 'iOS' + version: '$1' + +- regex: 'Podcasts/(?:[\d\.]+)|Instacast(?:HD)?/(?:\d\.[\d\.abc]+)|Pocket Casts, iOS|Overcast|Castro|Podcat|i[cC]atcher' + name: 'iOS' + version: '' + +- regex: 'iTunes-(iPod|iPad|iPhone)/(?:[\d\.]+)' + name: 'iOS' + version: '' + + +########## +# Mac +########## + +- regex: 'CFNetwork/807' + name: 'Mac' + version: '10.12' + +- regex: 'CFNetwork/760' + name: 'Mac' + version: '10.11' + +- regex: 'CFNetwork/720' + name: 'Mac' + version: '10.10' + +- regex: 'CFNetwork/673' + name: 'Mac' + version: '10.9' + +- regex: 'CFNetwork/596' + name: 'Mac' + version: '10.8' + +- regex: 'CFNetwork/520' + name: 'Mac' + version: '10.7' + +- regex: 'CFNetwork/454' + name: 'Mac' + version: '10.6' + +- regex: 'CFNetwork/(?:438|422|339|330|221|220|217)' + name: 'Mac' + version: '10.5' + +- regex: 'CFNetwork/12[89]' + name: 'Mac' + version: '10.4' + +- regex: 'CFNetwork/1\.2' + name: 'Mac' + version: '10.3' + +- regex: 'CFNetwork/1\.1' + name: 'Mac' + version: '10.2' + +- regex: 'Mac[ +]OS[ +]X(?:[ /](?:Version )?(\d+(?:[_\.]\d+)+))?' + name: 'Mac' + version: '$1' + +- regex: 'Mac (\d+(?:[_\.]\d+)+)' + name: 'Mac' + version: '$1' + +- regex: 'Darwin|Macintosh|Mac_PowerPC|PPC|Mac PowerPC|iMac|MacBook' + name: 'Mac' + version: '' + + + +########## +# ChromeOS +########## +- regex: 'CrOS [a-z0-9_]+ .* Chrome/(\d+[\.\d]+)' + name: 'Chrome OS' + version: '$1' + + + +########## +# BlackBerry +########## +- regex: '(?:BB10;.+Version|Black[Bb]erry[0-9a-z]+|Black[Bb]erry.+Version)/(\d+[\.\d]+)' + name: 'BlackBerry OS' + version: '$1' + + +- regex: 'RIM Tablet OS (\d+[\.\d]+)' + name: 'BlackBerry Tablet OS' + version: '$1' + + +- regex: 'RIM Tablet OS|QNX|Play[Bb]ook' + name: 'BlackBerry Tablet OS' + version: '' + + +- regex: 'BlackBerry' + name: 'BlackBerry OS' + version: '' + +- regex: 'bPod' + name: 'BlackBerry OS' + version: '' + + +########## +# BeOS +########## +- regex: 'BeOS' + name: 'BeOS' + version: '' + + + + +########## +# Symbian +########## +- regex: 'Symbian/3.+NokiaBrowser/7\.3' + name: 'Symbian^3' + version: 'Anna' + + +- regex: 'Symbian/3.+NokiaBrowser/7\.4' + name: 'Symbian^3' + version: 'Belle' + + +- regex: 'Symbian/3' + name: 'Symbian^3' + version: '' + + +- regex: '(?:Series ?60|SymbOS|S60)(?:[ /]?(\d+[\.\d]+|V\d+))?' + name: 'Symbian OS Series 60' + version: '$1' + + +- regex: 'Series40' + name: 'Symbian OS Series 40' + version: '' + + +- regex: 'SymbianOS/(\d+[\.\d]+)' + name: 'Symbian OS' + version: '$1' + + +- regex: 'MeeGo|WeTab' + name: 'MeeGo' + version: '' + + +- regex: 'Symbian(?: OS)?|SymbOS' + name: 'Symbian OS' + version: '' + + +- regex: 'Nokia' + name: 'Symbian' + version: '' + + + +########## +# Firefox OS +########## +- regex: '(?:Mobile|Tablet);.+Firefox/\d+\.\d+' + name: 'Firefox OS' + version: '' + + +########## +# RISC OS +########## +- regex: 'RISC OS(?:-NC)?(?:[ /](\d+[\.\d]+))?' + name: 'RISC OS' + version: '$1' + + +########## +# Inferno +########## +- regex: 'Inferno(?:[ /](\d+[\.\d]+))?' + name: 'Inferno' + version: '$1' + + +########## +# Bada +########## +- regex: 'bada(?:[ /](\d+[\.\d]+))' + name: 'Bada' + version: '$1' + + +- regex: 'bada' + name: 'Bada' + version: '' + + +########## +# Brew +########## +- regex: '(?:Brew MP|BREW|BMP)(?:[ /](\d+[\.\d]+))' + name: 'Brew' + version: '$1' + + +- regex: 'Brew MP|BREW|BMP' + name: 'Brew' + version: '' + + +########## +# Web TV +########## +- regex: 'GoogleTV(?:[ /](\d+[\.\d]+))?' + name: 'Google TV' + version: '$1' + + +- regex: 'AppleTV(?:/?(\d+[\.\d]+))?' + name: 'Apple TV' + version: '$1' + + +- regex: 'WebTV/(\d+[\.\d]+)' + name: 'WebTV' + version: '$1' + + +########## +# Remix OS +########## +- regex: 'RemixOS 5.1.1' + name: 'Remix OS' + version: '1' + +- regex: 'RemixOS 6.0' + name: 'Remix OS' + version: '2' + +- regex: 'RemixOS' + name: 'Remix OS' + version: '' + + +########## +# Unix +########## +- regex: '(?:SunOS|Solaris)(?:[/ ](\d+[\.\d]+))?' + name: 'Solaris' + version: '$1' + + +- regex: 'AIX(?:[/ ]?(\d+[\.\d]+))?' + name: 'AIX' + version: '$1' + + +- regex: 'HP-UX(?:[/ ]?(\d+[\.\d]+))?' + name: 'HP-UX' + version: '$1' + + +- regex: 'FreeBSD(?:[/ ]?(\d+[\.\d]+))?' + name: 'FreeBSD' + version: '$1' + + +- regex: 'NetBSD(?:[/ ]?(\d+[\.\d]+))?' + name: 'NetBSD' + version: '$1' + + +- regex: 'OpenBSD(?:[/ ]?(\d+[\.\d]+))?' + name: 'OpenBSD' + version: '$1' + + +- regex: 'DragonFly(?:[/ ]?(\d+[\.\d]+))?' + name: 'DragonFly' + version: '$1' + + +- regex: 'Syllable(?:[/ ]?(\d+[\.\d]+))?' + name: 'Syllable' + version: '$1' + + +- regex: 'IRIX(?:;64)?(?:[/ ]?(\d+[\.\d]+))' + name: 'IRIX' + version: '$1' + + +- regex: 'OSF1(?:[/ ]?v?(\d+[\.\d]+))?' + name: 'OSF1' + version: '$1' + + + +########## +# Gaming Console +########## +- regex: 'Nintendo Wii' + name: 'Nintendo' + version: 'Wii' + + +- regex: 'PlayStation ?([3|4])' + name: 'PlayStation' + version: '$1' + + +- regex: 'Xbox|KIN\.(?:One|Two)' + name: 'Xbox' + version: '360' + + + +########## +# Mobile Gaming Console +########## +- regex: 'Nitro|Nintendo ([3]?DS[i]?)' + name: 'Nintendo Mobile' + version: '$1' + + +- regex: 'PlayStation ((?:Portable|Vita))' + name: 'PlayStation Portable' + version: '$1' + + + +########## +# IBM +########## +- regex: 'OS/2' + name: 'OS/2' + version: '' + + + +########### +# Linux (Generic) +########### +- regex: 'Linux(?:OS)?[^a-z]' + name: 'GNU/Linux' + version: '' + + diff --git a/priv/ua_inspector/short_codes.client_browsers.yml b/priv/ua_inspector/short_codes.client_browsers.yml new file mode 100644 index 000000000..0d45154d1 --- /dev/null +++ b/priv/ua_inspector/short_codes.client_browsers.yml @@ -0,0 +1,162 @@ +- "36": "360 Phone Browser" +- "3B": "360 Browser" +- "AA": "Avant Browser" +- "AB": "ABrowse" +- "AF": "ANT Fresco" +- "AG": "ANTGalio" +- "AL": "Aloha Browser" +- "AM": "Amaya" +- "AO": "Amigo" +- "AN": "Android Browser" +- "AR": "Arora" +- "AV": "Amiga Voyager" +- "AW": "Amiga Aweb" +- "AT": "Atomic Web Browser" +- "AS": "Avast Secure Browser" +- "BB": "BlackBerry Browser" +- "BD": "Baidu Browser" +- "BS": "Baidu Spark" +- "BE": "Beonex" +- "BJ": "Bunjalloo" +- "BL": "B-Line" +- "BR": "Brave" +- "BK": "BriskBard" +- "BX": "BrowseX" +- "CA": "Camino" +- "CC": "Coc Coc" +- "CD": "Comodo Dragon" +- "C1": "Coast" +- "CX": "Charon" +- "CF": "Chrome Frame" +- "HC": "Headless Chrome" +- "CH": "Chrome" +- "CI": "Chrome Mobile iOS" +- "CK": "Conkeror" +- "CM": "Chrome Mobile" +- "CN": "CoolNovo" +- "CO": "CometBird" +- "CP": "ChromePlus" +- "CR": "Chromium" +- "CY": "Cyberfox" +- "CS": "Cheshire" +- "CU": "Cunaguaro" +- "DB": "dbrowser" +- "DE": "Deepnet Explorer" +- "DF": "Dolphin" +- "DO": "Dorado" +- "DL": "Dooble" +- "DI": "Dillo" +- "EI": "Epic" +- "EL": "Elinks" +- "EB": "Element Browser" +- "EP": "GNOME Web" +- "ES": "Espial TV Browser" +- "FB": "Firebird" +- "FD": "Fluid" +- "FE": "Fennec" +- "FF": "Firefox" +- "FK": "Firefox Focus" +- "FL": "Flock" +- "FM": "Firefox Mobile" +- "FW": "Fireweb" +- "FN": "Fireweb Navigator" +- "GA": "Galeon" +- "GE": "Google Earth" +- "HJ": "HotJava" +- "IA": "Iceape" +- "IB": "IBrowse" +- "IC": "iCab" +- "I2": "iCab Mobile" +- "I1": "Iridium" +- "ID": "IceDragon" +- "IV": "Isivioo" +- "IW": "Iceweasel" +- "IE": "Internet Explorer" +- "IM": "IE Mobile" +- "IR": "Iron" +- "JS": "Jasmine" +- "JI": "Jig Browser" +- "KI": "Kindle Browser" +- "KM": "K-meleon" +- "KO": "Konqueror" +- "KP": "Kapiko" +- "KY": "Kylo" +- "KZ": "Kazehakase" +- "LB": "Liebao" +- "LG": "LG Browser" +- "LI": "Links" +- "LU": "LuaKit" +- "LS": "Lunascape" +- "LX": "Lynx" +- "MB": "MicroB" +- "MC": "NCSA Mosaic" +- "ME": "Mercury" +- "MF": "Mobile Safari" +- "MI": "Midori" +- "MU": "MIUI Browser" +- "MS": "Mobile Silk" +- "MX": "Maxthon" +- "NB": "Nokia Browser" +- "NO": "Nokia OSS Browser" +- "NV": "Nokia Ovi Browser" +- "NE": "NetSurf" +- "NF": "NetFront" +- "NL": "NetFront Life" +- "NP": "NetPositive" +- "NS": "Netscape" +- "NT": "NTENT Browser" +- "OB": "Obigo" +- "OD": "Odyssey Web Browser" +- "OF": "Off By One" +- "OE": "ONE Browser" +- "OI": "Opera Mini" +- "OM": "Opera Mobile" +- "OP": "Opera" +- "ON": "Opera Next" +- "OO": "Opera Touch" +- "OR": "Oregano" +- "OV": "Openwave Mobile Browser" +- "OW": "OmniWeb" +- "OT": "Otter Browser" +- "PL": "Palm Blazer" +- "PM": "Pale Moon" +- "PP": "Oppo Browser" +- "PR": "Palm Pre" +- "PU": "Puffin" +- "PW": "Palm WebPro" +- "PA": "Palmscape" +- "PX": "Phoenix" +- "PO": "Polaris" +- "PT": "Polarity" +- "PS": "Microsoft Edge" +- "QQ": "QQ Browser" +- "QT": "Qutebrowser" +- "QZ": "QupZilla" +- "RK": "Rekonq" +- "RM": "RockMelt" +- "SB": "Samsung Browser" +- "SA": "Sailfish Browser" +- "SC": "SEMC-Browser" +- "SE": "Sogou Explorer" +- "SF": "Safari" +- "SH": "Shiira" +- "SK": "Skyfire" +- "SS": "Seraphic Sraf" +- "SL": "Sleipnir" +- "SM": "SeaMonkey" +- "SN": "Snowshoe" +- "SR": "Sunrise" +- "SP": "SuperBird" +- "ST": "Streamy" +- "SX": "Swiftfox" +- "TZ": "Tizen Browser" +- "TS": "TweakStyle" +- "UC": "UC Browser" +- "VI": "Vivaldi" +- "VB": "Vision Mobile Browser" +- "WE": "WebPositive" +- "WF": "Waterfox" +- "WO": "wOSBrowser" +- "WT": "WeTab Browser" +- "YA": "Yandex Browser" +- "XI": "Xiino" diff --git a/priv/ua_inspector/short_codes.desktop_families.yml b/priv/ua_inspector/short_codes.desktop_families.yml new file mode 100644 index 000000000..3b996ef94 --- /dev/null +++ b/priv/ua_inspector/short_codes.desktop_families.yml @@ -0,0 +1,8 @@ +- "AmigaOS" +- "IBM" +- "GNU/Linux" +- "Mac" +- "Unix" +- "Windows" +- "BeOS" +- "Chrome OS" diff --git a/priv/ua_inspector/short_codes.device_brands.yml b/priv/ua_inspector/short_codes.device_brands.yml new file mode 100644 index 000000000..7ab3944ed --- /dev/null +++ b/priv/ua_inspector/short_codes.device_brands.yml @@ -0,0 +1,386 @@ +- "3Q": "3Q" +- "4G": "4Good" +- "AC": "Acer" +- "AD": "Advance" +- "AZ": "Ainol" +- "AI": "Airness" +- "AW": "Aiwa" +- "AK": "Akai" +- "AL": "Alcatel" +- "A2": "Allview" +- "A1": "Altech UEC" +- "AN": "Arnova" +- "KN": "Amazon" +- "AO": "Amoi" +- "AP": "Apple" +- "AR": "Archos" +- "AS": "ARRIS" +- "AT": "Airties" +- "AU": "Asus" +- "AV": "Avvio" +- "AX": "Audiovox" +- "AY": "Axxion" +- "AM": "Azumi Mobile" +- "BB": "BBK" +- "BE": "Becker" +- "BI": "Bird" +- "BT": "Bitel" +- "BG": "BGH" +- "BL": "Beetel" +- "BP": "Blaupunkt" +- "B3": "Bluboo" +- "BM": "Bmobile" +- "BN": "Barnes & Noble" +- "BO": "BangOlufsen" +- "BQ": "BenQ" +- "BS": "BenQ-Siemens" +- "BU": "Blu" +- "B2": "Blackview" +- "BW": "Boway" +- "BX": "bq" +- "BV": "Bravis" +- "BR": "Brondi" +- "B1": "Bush" +- "CB": "CUBOT" +- "CF": "Carrefour" +- "CP": "Captiva" +- "CS": "Casio" +- "CA": "Cat" +- "CE": "Celkon" +- "CC": "ConCorde" +- "C2": "Changhong" +- "CH": "Cherry Mobile" +- "CK": "Cricket" +- "C1": "Crosscall" +- "CL": "Compal" +- "CN": "CnM" +- "CM": "Crius Mea" +- "C3": "China Mobile" +- "CR": "CreNova" +- "CT": "Capitel" +- "CQ": "Compaq" +- "CO": "Coolpad" +- "C5": "Condor" +- "CW": "Cowon" +- "CU": "Cube" +- "CY": "Coby Kyros" +- "C6": "Comio" +- "C7": "ComTrade Tesla" +- "C4": "Cyrus" +- "DA": "Danew" +- "DT": "Datang" +- "DE": "Denver" +- "DX": "DEXP" +- "DS": "Desay" +- "DB": "Dbtel" +- "DC": "DoCoMo" +- "DG": "Dialog" +- "DI": "Dicam" +- "D2": "Digma" +- "DL": "Dell" +- "DN": "DNS" +- "DM": "DMM" +- "DO": "Doogee" +- "DV": "Doov" +- "DP": "Dopod" +- "DR": "Doro" +- "DU": "Dune HD" +- "EB": "E-Boda" +- "EA": "EBEST" +- "EC": "Ericsson" +- "ES": "ECS" +- "EI": "Ezio" +- "EL": "Elephone" +- "EP": "Easypix" +- "EK": "EKO" +- "E1": "Energy Sistem" +- "ER": "Ericy" +- "EE": "Essential" +- "EN": "Eton" +- "E2": "Essentielb" +- "ET": "eTouch" +- "EV": "Evertek" +- "EO": "Evolveo" +- "EX": "Explay" +- "EZ": "Ezze" +- "FA": "Fairphone" +- "FL": "Fly" +- "FT": "Freetel" +- "FO": "Foxconn" +- "FN": "FNB" +- "FU": "Fujitsu" +- "GM": "Garmin-Asus" +- "GA": "Gateway" +- "GD": "Gemini" +- "GI": "Gionee" +- "GG": "Gigabyte" +- "GS": "Gigaset" +- "GC": "GOCLEVER" +- "GL": "Goly" +- "GO": "Google" +- "G1": "GoMobile" +- "GR": "Gradiente" +- "GP": "Grape" +- "GU": "Grundig" +- "HA": "Haier" +- "HS": "Hasee" +- "HE": "HannSpree" +- "HI": "Hisense" +- "HL": "Hi-Level" +- "HM": "Homtom" +- "HO": "Hosin" +- "HP": "HP" +- "HT": "HTC" +- "HU": "Huawei" +- "HX": "Humax" +- "HY": "Hyrican" +- "HN": "Hyundai" +- "IA": "Ikea" +- "IB": "iBall" +- "IJ": "i-Joy" +- "IY": "iBerry" +- "IK": "iKoMo" +- "IM": "i-mate" +- "I1": "iOcean" +- "I2": "IconBIT" +- "IW": "iNew" +- "IF": "Infinix" +- "IN": "Innostream" +- "II": "Inkti" +- "IX": "Intex" +- "IO": "i-mobile" +- "IQ": "INQ" +- "IT": "Intek" +- "IV": "Inverto" +- "I3": "Impression" +- "IZ": "iTel" +- "JA": "JAY-Tech" +- "JI": "Jiayu" +- "JO": "Jolla" +- "KA": "Karbonn" +- "KD": "KDDI" +- "K1": "Kiano" +- "KI": "Kingsun" +- "KG": "Kogan" +- "KO": "Konka" +- "KM": "Komu" +- "KB": "Koobee" +- "KT": "K-Touch" +- "KH": "KT-Tech" +- "KP": "KOPO" +- "KW": "Konrow" +- "KR": "Koridy" +- "KS": "Kempler & Strauss" +- "KU": "Kumai" +- "KY": "Kyocera" +- "KZ": "Kazam" +- "L2": "Landvo" +- "LV": "Lava" +- "LA": "Lanix" +- "LC": "LCT" +- "L1": "LeEco" +- "L4": "Lemhoov" +- "LE": "Lenovo" +- "LN": "Lenco" +- "LP": "Le Pan" +- "LG": "LG" +- "LI": "Lingwin" +- "LO": "Loewe" +- "LM": "Logicom" +- "L3": "Lexand" +- "LX": "Lexibook" +- "LY": "LYF" +- "MJ": "Majestic" +- "MA": "Manta Multimedia" +- "MB": "Mobistel" +- "M3": "Mecer" +- "MD": "Medion" +- "M2": "MEEG" +- "M1": "Meizu" +- "ME": "Metz" +- "MX": "MEU" +- "MI": "MicroMax" +- "M5": "MIXC" +- "M6": "Mobiistar" +- "MC": "Mediacom" +- "MK": "MediaTek" +- "MO": "Mio" +- "M7": "Miray" +- "MM": "Mpman" +- "M4": "Modecom" +- "MF": "Mofut" +- "MR": "Motorola" +- "MS": "Microsoft" +- "M9": "MTC" +- "MZ": "MSI" +- "MU": "Memup" +- "MT": "Mitsubishi" +- "ML": "MLLED" +- "MQ": "M.T.T." +- "MY": "MyPhone" +- "M8": "Myria" +- "NE": "NEC" +- "NF": "Neffos" +- "NA": "Netgear" +- "NG": "NGM" +- "NO": "Nous" +- "NI": "Nintendo" +- "N1": "Noain" +- "N2": "Nextbit" +- "NK": "Nokia" +- "NV": "Nvidia" +- "NB": "Noblex" +- "NM": "Nomi" +- "NN": "Nikon" +- "NW": "Newgen" +- "NX": "Nexian" +- "NT": "NextBook" +- "OB": "Obi" +- "O1": "Odys" +- "OD": "Onda" +- "ON": "OnePlus" +- "OP": "OPPO" +- "OR": "Orange" +- "OT": "O2" +- "OK": "Ouki" +- "OU": "OUYA" +- "OO": "Opsson" +- "OV": "Overmax" +- "OY": "Oysters" +- "PA": "Panasonic" +- "PE": "PEAQ" +- "PG": "Pentagram" +- "PH": "Philips" +- "PI": "Pioneer" +- "PL": "Polaroid" +- "P9": "Primepad" +- "PM": "Palm" +- "PO": "phoneOne" +- "PT": "Pantech" +- "PY": "Ployer" +- "PV": "Point of View" +- "PP": "PolyPad" +- "P2": "Pomp" +- "P3": "PPTV" +- "PS": "Positivo" +- "PR": "Prestigio" +- "P1": "ProScan" +- "PU": "PULID" +- "QI": "Qilive" +- "QT": "Qtek" +- "QM": "QMobile" +- "QU": "Quechua" +- "RA": "Ramos" +- "RC": "RCA Tablets" +- "RB": "Readboy" +- "RI": "Rikomagic" +- "RM": "RIM" +- "RK": "Roku" +- "RO": "Rover" +- "SA": "Samsung" +- "S0": "Sanei" +- "SD": "Sega" +- "SE": "Sony Ericsson" +- "S1": "Sencor" +- "SF": "Softbank" +- "SX": "SFR" +- "SG": "Sagem" +- "SH": "Sharp" +- "SI": "Siemens" +- "SN": "Sendo" +- "S6": "Senseit" +- "SK": "Skyworth" +- "SC": "Smartfren" +- "SO": "Sony" +- "SP": "Spice" +- "SU": "SuperSonic" +- "S5": "Supra" +- "SV": "Selevision" +- "SY": "Sanyo" +- "SM": "Symphony" +- "SR": "Smart" +- "S7": "Smartisan" +- "S4": "Star" +- "S8": "STK" +- "S9": "Savio" +- "ST": "Storex" +- "S2": "Stonex" +- "S3": "SunVan" +- "SZ": "Sumvision" +- "TA": "Tesla" +- "T5": "TB Touch" +- "TC": "TCL" +- "T7": "Teclast" +- "TE": "Telit" +- "T4": "ThL" +- "TH": "TiPhone" +- "TB": "Tecno Mobile" +- "TP": "TechPad" +- "TD": "Tesco" +- "TI": "TIANYU" +- "TL": "Telefunken" +- "T2": "Telenor" +- "TM": "T-Mobile" +- "TN": "Thomson" +- "T1": "Tolino" +- "T9": "Top House" +- "TO": "Toplux" +- "T8": "Touchmate" +- "TS": "Toshiba" +- "TT": "TechnoTrend" +- "T6": "TrekStor" +- "T3": "Trevi" +- "TU": "Tunisie Telecom" +- "TR": "Turbo-X" +- "TV": "TVC" +- "TX": "TechniSat" +- "TZ": "teXet" +- "UH": "Uhappy" +- "UL": "Ulefone" +- "UO": "Unnecto" +- "UN": "Unowhy" +- "US": "Uniscope" +- "UM": "UMIDIGI" +- "UU": "Unonu" +- "UT": "UTStarcom" +- "VA": "Vastking" +- "VD": "Videocon" +- "VE": "Vertu" +- "VI": "Vitelcom" +- "VK": "VK Mobile" +- "VS": "ViewSonic" +- "VT": "Vestel" +- "VR": "Vernee" +- "VL": "Verykool" +- "VV": "Vivo" +- "V3": "Vinsoc" +- "V2": "Vonino" +- "V1": "Voto" +- "VO": "Voxtel" +- "VF": "Vodafone" +- "VZ": "Vizio" +- "VW": "Videoweb" +- "WA": "Walton" +- "WF": "Wileyfox" +- "WE": "WellcoM" +- "WY": "Wexler" +- "WI": "Wiko" +- "WL": "Wolder" +- "WG": "Wolfgang" +- "WO": "Wonu" +- "WX": "Woxter" +- "XI": "Xiaomi" +- "XO": "Xolo" +- "YA": "Yarvik" +- "YU": "Yuandao" +- "YS": "Yusun" +- "YT": "Ytone" +- "ZE": "Zeemi" +- "ZO": "Zonda" +- "ZP": "Zopo" +- "ZT": "ZTE" +- "ZU": "Zuum" +- "ZN": "Zen" +- "ZY": "Zync" +- "WB": "Web TV" +- "XX": "Unknown" diff --git a/priv/ua_inspector/short_codes.mobile_browsers.yml b/priv/ua_inspector/short_codes.mobile_browsers.yml new file mode 100644 index 000000000..9ce9a1b8e --- /dev/null +++ b/priv/ua_inspector/short_codes.mobile_browsers.yml @@ -0,0 +1,14 @@ +- "36" +- "PU" +- "SK" +- "MF" +- "OI" +- "OM" +- "DB" +- "ST" +- "BL" +- "IV" +- "FM" +- "C1" +- "AL" +- "SA" diff --git a/priv/ua_inspector/short_codes.os_families.yml b/priv/ua_inspector/short_codes.os_families.yml new file mode 100644 index 000000000..f6eca7b49 --- /dev/null +++ b/priv/ua_inspector/short_codes.os_families.yml @@ -0,0 +1,103 @@ +- "Android": + - "AND" + - "CYN" + - "FIR" + - "REM" + - "RZD" + - "MLD" + - "MCD" + - "YNS" +- "AmigaOS": + - "AMG" + - "MOR" +- "Apple TV": + - "ATV" +- "BlackBerry": + - "BLB" + - "QNX" +- "Brew": + - "BMP" +- "BeOS": + - "BEO" + - "HAI" +- "Chrome OS": + - "COS" +- "Firefox OS": + - "FOS" + - "KOS" +- "Gaming Console": + - "WII" + - "PS3" +- "Google TV": + - "GTV" +- "IBM": + - "OS2" +- "iOS": + - "IOS" +- "RISC OS": + - "ROS" +- "GNU/Linux": + - "LIN" + - "ARL" + - "DEB" + - "KNO" + - "MIN" + - "UBT" + - "KBT" + - "XBT" + - "LBT" + - "FED" + - "RHT" + - "VLN" + - "MDR" + - "GNT" + - "SAB" + - "SLW" + - "SSE" + - "CES" + - "BTR" + - "SAF" +- "Mac": + - "MAC" +- "Mobile Gaming Console": + - "PSP" + - "NDS" + - "XBX" +- "Real-time OS": + - "MTK" + - "TDX" +- "Other Mobile": + - "WOS" + - "POS" + - "SBA" + - "TIZ" + - "SMG" + - "MAE" +- "Symbian": + - "SYM" + - "SYS" + - "SY3" + - "S60" + - "S40" +- "Unix": + - "SOS" + - "AIX" + - "HPX" + - "BSD" + - "NBS" + - "OBS" + - "DFB" + - "SYL" + - "IRI" + - "T64" + - "INF" +- "WebTV": + - "WTV" +- "Windows": + - "WIN" +- "Windows Mobile": + - "WPH" + - "WMO" + - "WCE" + - "WRT" + - "WIO" diff --git a/priv/ua_inspector/short_codes.oss.yml b/priv/ua_inspector/short_codes.oss.yml new file mode 100644 index 000000000..8d42b1706 --- /dev/null +++ b/priv/ua_inspector/short_codes.oss.yml @@ -0,0 +1,80 @@ +- "AIX": "AIX" +- "AND": "Android" +- "AMG": "AmigaOS" +- "ATV": "Apple TV" +- "ARL": "Arch Linux" +- "BTR": "BackTrack" +- "SBA": "Bada" +- "BEO": "BeOS" +- "BLB": "BlackBerry OS" +- "QNX": "BlackBerry Tablet OS" +- "BMP": "Brew" +- "CES": "CentOS" +- "COS": "Chrome OS" +- "CYN": "CyanogenMod" +- "DEB": "Debian" +- "DFB": "DragonFly" +- "FED": "Fedora" +- "FOS": "Firefox OS" +- "FIR": "Fire OS" +- "BSD": "FreeBSD" +- "GNT": "Gentoo" +- "GTV": "Google TV" +- "HPX": "HP-UX" +- "HAI": "Haiku OS" +- "IRI": "IRIX" +- "INF": "Inferno" +- "KOS": "KaiOS" +- "KNO": "Knoppix" +- "KBT": "Kubuntu" +- "LIN": "GNU/Linux" +- "LBT": "Lubuntu" +- "VLN": "VectorLinux" +- "MAC": "Mac" +- "MAE": "Maemo" +- "MDR": "Mandriva" +- "SMG": "MeeGo" +- "MCD": "MocorDroid" +- "MIN": "Mint" +- "MLD": "MildWild" +- "MOR": "MorphOS" +- "NBS": "NetBSD" +- "MTK": "MTK / Nucleus" +- "WII": "Nintendo" +- "NDS": "Nintendo Mobile" +- "OS2": "OS/2" +- "T64": "OSF1" +- "OBS": "OpenBSD" +- "PSP": "PlayStation Portable" +- "PS3": "PlayStation" +- "RHT": "Red Hat" +- "ROS": "RISC OS" +- "REM": "Remix OS" +- "RZD": "RazoDroiD" +- "SAB": "Sabayon" +- "SSE": "SUSE" +- "SAF": "Sailfish OS" +- "SLW": "Slackware" +- "SOS": "Solaris" +- "SYL": "Syllable" +- "SYM": "Symbian" +- "SYS": "Symbian OS" +- "S40": "Symbian OS Series 40" +- "S60": "Symbian OS Series 60" +- "SY3": "Symbian^3" +- "TDX": "ThreadX" +- "TIZ": "Tizen" +- "UBT": "Ubuntu" +- "WTV": "WebTV" +- "WIN": "Windows" +- "WCE": "Windows CE" +- "WIO": "Windows IoT" +- "WMO": "Windows Mobile" +- "WPH": "Windows Phone" +- "WRT": "Windows RT" +- "XBX": "Xbox" +- "XBT": "Xubuntu" +- "YNS": "YunOs" +- "IOS": "iOS" +- "POS": "palmOS" +- "WOS": "webOS" diff --git a/priv/ua_inspector/ua_inspector.readme.md b/priv/ua_inspector/ua_inspector.readme.md new file mode 100644 index 000000000..b29b93c4f --- /dev/null +++ b/priv/ua_inspector/ua_inspector.readme.md @@ -0,0 +1,5 @@ +# UAInspector Parser Databases + +The files in this directory are taken from the +[matomo-org/device-detector](https://github.com/matomo-org/device-detector) +project. See there for detailed license information about the data contained. diff --git a/priv/ua_inspector/vendor_fragment.vendorfragments.yml b/priv/ua_inspector/vendor_fragment.vendorfragments.yml new file mode 100644 index 000000000..5a026115a --- /dev/null +++ b/priv/ua_inspector/vendor_fragment.vendorfragments.yml @@ -0,0 +1,71 @@ +############### +# Device Detector - The Universal Device Detection library for parsing User Agents +# +# @link http://piwik.org +# @license http://www.gnu.org/licenses/lgpl.html LGPL v3 or later +############### + +Dell: + - 'MDDR(JS)?' + - 'MDDC(JS)?' + - 'MDDS(JS)?' + +Acer: + - 'MAAR(JS)?' + +Sony: + - 'MASE(JS)?' + - 'MASP(JS)?' + - 'MASA(JS)?' + +Asus: + - 'MAAU' + - 'NP0[6789]' + - 'ASJB' + - 'ASU2(JS)?' + +Samsung: + - 'MASM(JS)?' + - 'SMJB' + +Lenovo: + - 'MALC(JS)?' + - 'MALE(JS)?' + - 'MALN(JS)?' + - 'LCJB' + - 'LEN2' + +Toshiba: + - 'MATM(JS)?' + - 'MATB(JS)?' + - 'MATP(JS)?' + - 'TNJB' + - 'TAJB' + +Medion: + - 'MAMD' + +MSI: + - 'MAMI(JS)?' + - 'MAM3' + +Gateway: + - 'MAGW(JS)?' + +Fujitsu: + - 'MAFS(JS)?' + - 'FSJB' + +Compaq: + - 'CPDTDF' + - 'CPNTDF(JS?)' + - 'CMNTDF(JS)?' + - 'CMDTDF(JS)?' + +HP: + - 'HPCMHP' + - 'HPNTDF(JS)?' + - 'HPDTDF(JS)?' + +Hyrican: + - 'MANM(JS)?' diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 000000000..1e4e8360a Binary files /dev/null and b/screenshot.png differ diff --git a/test/mix/tasks/send_feedback_emails_test.exs b/test/mix/tasks/send_feedback_emails_test.exs new file mode 100644 index 000000000..10d3cd015 --- /dev/null +++ b/test/mix/tasks/send_feedback_emails_test.exs @@ -0,0 +1,65 @@ +defmodule Mix.Tasks.SendFeedbackEmailsTest do + use Plausible.DataCase + use Bamboo.Test + alias Mix.Tasks.SendFeedbackEmails + + describe "when user does not have an active site" do + test "does not send an email ever" do + insert(:user, inserted_at: days_ago(15)) + + SendFeedbackEmails.execute() + + assert_no_emails_delivered() + end + end + + describe "when user has an active site" do + test "sends an email if the user is more than 30 days old and logged on in the last week" do + user = insert(:user, inserted_at: days_ago(31), last_seen: days_ago(1)) + site = insert(:site, members: [user]) + insert(:pageview, hostname: site.domain) + + SendFeedbackEmails.execute() + + assert_email_delivered_with(subject: "Plausible feedback") + end + + test "sends the email only once" do + user = insert(:user, inserted_at: days_ago(31), last_seen: days_ago(1)) + site = insert(:site, members: [user]) + insert(:pageview, hostname: site.domain) + + SendFeedbackEmails.execute() + assert_email_delivered_with(subject: "Plausible feedback") + + SendFeedbackEmails.execute() + assert_no_emails_delivered() + end + + test "does not send if user has not logged in recently" do + user = insert(:user, inserted_at: days_ago(31), last_seen: days_ago(15)) + site = insert(:site, members: [user]) + insert(:pageview, hostname: site.domain) + + SendFeedbackEmails.execute() + + assert_no_emails_delivered() + end + + test "does not send if user is less than a month old" do + user = insert(:user, inserted_at: days_ago(15), last_seen: days_ago(1)) + site = insert(:site, members: [user]) + insert(:pageview, hostname: site.domain) + + SendFeedbackEmails.execute() + + assert_no_emails_delivered() + end + end + + defp days_ago(days) do + NaiveDateTime.utc_now() + |> NaiveDateTime.truncate(:second) + |> Timex.shift(days: -days) + end +end diff --git a/test/mix/tasks/send_intro_emails_test.exs b/test/mix/tasks/send_intro_emails_test.exs new file mode 100644 index 000000000..757e1028e --- /dev/null +++ b/test/mix/tasks/send_intro_emails_test.exs @@ -0,0 +1,122 @@ +defmodule Mix.Tasks.SendIntroEmailsTest do + use Plausible.DataCase + use Bamboo.Test + + describe "when user has not managed to set up a site" do + test "does not send an email 5 hours after signup" do + _user = insert(:user, inserted_at: hours_ago(5)) + + Mix.Tasks.SendIntroEmails.execute() + + assert_no_emails_delivered() + end + + test "sends a setup help email 6 hours after signup" do + user = insert(:user, inserted_at: hours_ago(6)) + + Mix.Tasks.SendIntroEmails.execute() + + assert_email_delivered_with( + to: [{user.name, user.email}], + subject: "Your Plausible setup" + ) + end + + test "sends a setup help email 23 hours after signup" do + user = insert(:user, inserted_at: hours_ago(23)) + + Mix.Tasks.SendIntroEmails.execute() + + assert_email_delivered_with( + to: [{user.name, user.email}], + subject: "Your Plausible setup" + ) + end + + test "does not send an email 24 hours after signup" do + _user = insert(:user, inserted_at: hours_ago(24)) + + Mix.Tasks.SendIntroEmails.execute() + + assert_no_emails_delivered() + end + end + + describe "when user has managed to set up their first site" do + test "does not send an email 5 hours after signup" do + _user = insert(:user, inserted_at: hours_ago(5)) + + Mix.Tasks.SendIntroEmails.execute() + + assert_no_emails_delivered() + end + + test "sends a setup help email 6 hours after signup if the user has created a site but has not received a pageview yet" do + user = insert(:user, inserted_at: hours_ago(6)) + insert(:site, members: [user]) + + Mix.Tasks.SendIntroEmails.execute() + + assert_email_delivered_with( + to: [{user.name, user.email}], + subject: "Your Plausible setup" + ) + end + + test "sends a welcome email 6 hours after signup if the user has created a site and has received a pageview" do + user = insert(:user, inserted_at: hours_ago(6)) + site = insert(:site, members: [user]) + insert(:pageview, hostname: site.domain) + + Mix.Tasks.SendIntroEmails.execute() + + assert_email_delivered_with( + to: [{user.name, user.email}], + subject: "Welcome to Plausible :) Plus, a quick question..." + ) + end + + test "sends a welcome email 23 hours after signup" do + user = insert(:user, inserted_at: hours_ago(23)) + site = insert(:site, members: [user]) + insert(:pageview, hostname: site.domain) + + Mix.Tasks.SendIntroEmails.execute() + + assert_email_delivered_with( + to: [{user.name, user.email}], + subject: "Welcome to Plausible :) Plus, a quick question..." + ) + end + + test "does not send a welcome email 24 hours after signup" do + user = insert(:user, inserted_at: hours_ago(24)) + site = insert(:site, members: [user]) + insert(:pageview, hostname: site.domain) + + Mix.Tasks.SendIntroEmails.execute() + + assert_no_emails_delivered() + end + end + + test "does not send two intro emails to the same person" do + user = insert(:user, inserted_at: hours_ago(12)) + + Mix.Tasks.SendIntroEmails.execute() + + site = insert(:site, members: [user]) + insert(:pageview, hostname: site.domain) + + Mix.Tasks.SendIntroEmails.execute() + + assert_delivered_email(PlausibleWeb.Email.help_email(user)) + refute_delivered_email(PlausibleWeb.Email.welcome_email(user)) + end + + defp hours_ago(hours) do + NaiveDateTime.utc_now() + |> NaiveDateTime.truncate(:second) + |> Timex.shift(hours: -hours) + end +end diff --git a/test/mix/tasks/send_trial_notifications_test.exs b/test/mix/tasks/send_trial_notifications_test.exs new file mode 100644 index 000000000..74f91534b --- /dev/null +++ b/test/mix/tasks/send_trial_notifications_test.exs @@ -0,0 +1,73 @@ +defmodule Mix.Tasks.SendTrialNotificationsTest do + use Plausible.DataCase + use Bamboo.Test + + test "does not send a notification if user didn't set up their site" do + insert(:user, inserted_at: Timex.now |> Timex.shift(days: -14)) + insert(:user, inserted_at: Timex.now |> Timex.shift(days: -29)) + insert(:user, inserted_at: Timex.now |> Timex.shift(days: -30)) + insert(:user, inserted_at: Timex.now |> Timex.shift(days: -31)) + + Mix.Tasks.SendTrialNotifications.execute() + + assert_no_emails_delivered() + end + + describe "with site and pageviews" do + test "sends a reminder 14 days before trial ends (16 days after user signed up)" do + user = insert(:user, inserted_at: Timex.now |> Timex.shift(days: -16)) + site = insert(:site, members: [user]) + insert(:pageview, hostname: site.domain) + + Mix.Tasks.SendTrialNotifications.execute() + + assert_delivered_email(PlausibleWeb.Email.trial_two_week_reminder(user)) + end + + test "sends an upgrade email the day before the trial ends" do + user = insert(:user, inserted_at: Timex.now |> Timex.shift(days: -29)) + site = insert(:site, members: [user]) + insert(:pageview, hostname: site.domain) + + Mix.Tasks.SendTrialNotifications.execute() + + assert_delivered_email(PlausibleWeb.Email.trial_upgrade_email(user, "tomorrow", 1)) + end + + # test "sends an upgrade email the day the trial ends" do + # user = insert(:user, inserted_at: Timex.now |> Timex.shift(days: -30)) + # site = insert(:site, members: [user]) + # insert(:pageview, hostname: site.domain) + # + # Mix.Tasks.SendTrialNotifications.execute() + # + # assert_delivered_email(PlausibleWeb.Email.trial_upgrade_email(user, "today", 1)) + # end + # + #test "sends a trial over email on the day after the trial ends" do + # user = insert(:user, inserted_at: Timex.now |> Timex.shift(days: -31)) + # site = insert(:site, members: [user]) + # insert(:pageview, hostname: site.domain) + + # Mix.Tasks.SendTrialNotifications.execute() + + # assert_delivered_email(PlausibleWeb.Email.trial_over_email(user)) + #end + + test "does not send a notification if user has a subscription" do + user1 = insert(:user, inserted_at: Timex.now |> Timex.shift(days: -14)) + site1 = insert(:site, members: [user1]) + insert(:pageview, hostname: site1.domain) + user2 = insert(:user, inserted_at: Timex.now |> Timex.shift(days: -29)) + site2 = insert(:site, members: [user2]) + insert(:pageview, hostname: site2.domain) + + insert(:subscription, user: user1) + insert(:subscription, user: user2) + + Mix.Tasks.SendTrialNotifications.execute() + + assert_no_emails_delivered() + end + end +end diff --git a/test/plausible/billing/billing_test.exs b/test/plausible/billing/billing_test.exs new file mode 100644 index 000000000..4a429627e --- /dev/null +++ b/test/plausible/billing/billing_test.exs @@ -0,0 +1,120 @@ +defmodule Plausible.BillingTest do + use Plausible.DataCase + alias Plausible.Billing + + describe "trial_days_left" do + test "is 30 days for new signup" do + user = insert(:user) + + assert Billing.trial_days_left(user) == 30 + end + + test "is 29 days for day old user" do + user = insert(:user, inserted_at: Timex.shift(Timex.now(), days: -1)) + + assert Billing.trial_days_left(user) == 29 + end + end + + @subscription_id "subscription-123" + @plan_id "plan-123" + + + describe "subscription_created" do + test "creates a subscription" do + user = insert(:user) + Billing.subscription_created(%{ + "alert_name" => "subscription_created", + "subscription_id" => @subscription_id, + "subscription_plan_id" => @plan_id, + "update_url" => "update_url.com", + "cancel_url" => "cancel_url.com", + "passthrough" => user.id, + "status" => "active", + "next_bill_date" => "2019-06-01", + "unit_price" => "6.00" + }) + + subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id) + assert subscription.paddle_subscription_id == @subscription_id + assert subscription.next_bill_date == ~D[2019-06-01] + assert subscription.next_bill_amount == "6.00" + end + end + + describe "subscription_updated" do + test "updates an existing subscription" do + user = insert(:user) + subscription = insert(:subscription, user: user) + + Billing.subscription_updated(%{ + "alert_name" => "subscription_updated", + "subscription_id" => subscription.paddle_subscription_id, + "subscription_plan_id" => "new-plan-id", + "update_url" => "update_url.com", + "cancel_url" => "cancel_url.com", + "passthrough" => user.id, + "status" => "active", + "next_bill_date" => "2019-06-01", + "new_unit_price" => "12.00" + }) + + subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id) + assert subscription.paddle_plan_id == "new-plan-id" + assert subscription.next_bill_amount == "12.00" + end + end + + describe "subscription_cancelled" do + test "sets the status to deleted" do + user = insert(:user) + subscription = insert(:subscription, status: "active", user: user) + + Billing.subscription_cancelled(%{ + "alert_name" => "subscription_cancelled", + "subscription_id" => subscription.paddle_subscription_id, + "status" => "deleted" + }) + + subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id) + assert subscription.status == "deleted" + end + + test "ignores if the subscription cannot be found" do + res = Billing.subscription_cancelled(%{ + "alert_name" => "subscription_cancelled", + "subscription_id" => "some_nonexistent_id", + "status" => "deleted" + }) + + assert res == {:ok, nil} + end + end + + describe "subscription_payment_succeeded" do + test "sets the next bill amount and date" do + user = insert(:user) + subscription = insert(:subscription, user: user) + + Billing.subscription_payment_succeeded(%{ + "alert_name" => "subscription_payment_succeeded", + "subscription_id" => subscription.paddle_subscription_id + }) + + subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id) + assert subscription.next_bill_date == ~D[2019-07-10] + assert subscription.next_bill_amount == "6.00" + end + + test "ignores if the subscription cannot be found" do + res = Billing.subscription_payment_succeeded(%{ + "alert_name" => "subscription_payment_succeeded", + "subscription_id" => "nonexistent_subscription_id", + "next_bill_date" => Timex.shift(Timex.today(), days: 30), + "unit_price" => "12.00" + }) + + assert res == {:ok, nil} + end + end +end diff --git a/test/plausible/stats/query_test.exs b/test/plausible/stats/query_test.exs new file mode 100644 index 000000000..d365b6fff --- /dev/null +++ b/test/plausible/stats/query_test.exs @@ -0,0 +1,46 @@ +defmodule Plausible.Stats.QueryTest do + use ExUnit.Case, async: true + alias Plausible.Stats.Query + + @tz "UTC" + + test "parses day format" do + q = Query.from(@tz, %{"period" => "day", "date" => "2019-01-01"}) + + assert q.date_range.first == ~D[2019-01-01] + assert q.date_range.last == ~D[2019-01-01] + assert q.step_type == "hour" + end + + test "parses month format" do + q = Query.from(@tz, %{"period" => "month", "date" => "2019-01-01"}) + + assert q.date_range.first == ~D[2019-01-01] + assert q.date_range.last == ~D[2019-01-31] + assert q.step_type == "date" + end + + test "parses 3 month format" do + q = Query.from(@tz, %{"period" => "3mo"}) + + assert q.date_range.first == Timex.shift(Timex.today(), months: -2) |> Timex.beginning_of_month() + assert q.date_range.last == Timex.today() + assert q.step_type == "month" + end + + test "defaults to 6 months format" do + assert Query.from(@tz, %{}) == Query.from(@tz, %{"period" => "6mo"}) + end + + test "parses custom format" do + q = Query.from(@tz, %{ + "period" => "custom", + "from" => "2019-01-01", + "to" => "2019-02-01" + }) + + assert q.date_range.first == ~D[2019-01-01] + assert q.date_range.last == ~D[2019-02-01] + assert q.step_type == "date" + end +end diff --git a/test/plausible/stats/stats_test.exs b/test/plausible/stats/stats_test.exs new file mode 100644 index 000000000..68f2e1a52 --- /dev/null +++ b/test/plausible/stats/stats_test.exs @@ -0,0 +1,149 @@ +defmodule Plausible.StatsTest do + use Plausible.DataCase + alias Plausible.Stats + + describe "calculate_plot" do + test "displays pageviews for a day" do + site = insert(:site) + insert(:pageview, hostname: site.domain, inserted_at: ~N[2019-01-01 00:00:00]) + insert(:pageview, hostname: site.domain, inserted_at: ~N[2019-01-01 23:59:00]) + + query = Stats.Query.from(site.timezone, %{"period" => "day", "date" => "2019-01-01"}) + + {plot, _labels, _index} = Stats.calculate_plot(site, query) + + zeroes = Stream.repeatedly(fn -> 0 end) |> Stream.take(22) |> Enum.into([]) + + assert Enum.count(plot) == 24 + assert plot == [1] ++ zeroes ++ [1] + end + + test "displays pageviews for a month" do + site = insert(:site) + insert(:pageview, hostname: site.domain, inserted_at: ~N[2019-01-01 12:00:00]) + insert(:pageview, hostname: site.domain, inserted_at: ~N[2019-01-31 12:00:00]) + + query = Stats.Query.from(site.timezone, %{"period" => "month", "date" => "2019-01-01"}) + {plot, _labels, _index} = Stats.calculate_plot(site, query) + + assert Enum.count(plot) == 31 + assert List.first(plot) == 1 + assert List.last(plot) == 1 + end + + test "displays pageviews for a 3 months" do + site = insert(:site) + insert(:pageview, hostname: site.domain) + insert(:pageview, hostname: site.domain, inserted_at: months_ago(2)) + + query = Stats.Query.from(site.timezone, %{"period" => "3mo"}) + {plot, _labels, _index} = Stats.calculate_plot(site, query) + + assert Enum.count(plot) == 3 + assert plot == [1, 0, 1] + end + end + + describe "labels" do + test "shows last 30 days" do + site = insert(:site) + + query = %Stats.Query{ + date_range: Date.range(~D[2019-01-01], ~D[2019-01-31]), + step_type: "date" + } + + {_plot, labels, _index} = Stats.calculate_plot(site, query) + + assert List.first(labels) == "2019-01-01" + assert List.last(labels) == "2019-01-31" + end + + test "shows last 7 days" do + site = insert(:site) + query = %Stats.Query{ + date_range: Date.range(~D[2019-01-01], ~D[2019-01-08]), + step_type: "date" + } + + {_plot, labels, _index} = Stats.calculate_plot(site, query) + + assert List.first(labels) == "2019-01-01" + assert List.last(labels) == "2019-01-08" + end + + test "shows last 24 hours" do + site = insert(:site) + query = %Stats.Query{ + period: "day", + date_range: Date.range(~D[2019-01-31], ~D[2019-01-31]), + step_type: "hour" + } + + {_plot, labels, _index} = Stats.calculate_plot(site, query) + + assert List.first(labels) == "2019-01-31T00:00:00" + assert List.last(labels) == "2019-01-31T23:00:00" + end + end + + describe "referrer_drilldown" do + test "shows grouped counts of referrers" do + site = insert(:site) + + insert(:pageview, %{ + hostname: site.domain, + referrer: "10words.io/somepage", + referrer_source: "10words", + new_visitor: true + }) + + insert(:pageview, %{ + hostname: site.domain, + referrer: "10words.io/somepage", + referrer_source: "10words", + new_visitor: true + }) + + insert(:pageview, %{ + hostname: site.domain, + referrer: "10words.io/some_other_page", + referrer_source: "10words", + new_visitor: true + }) + + query = Stats.Query.from("UTC", %{"period" => "day"}) + drilldown = Stats.referrer_drilldown(site, query, "10words") + + assert {"10words.io/somepage", 2} in drilldown + assert {"10words.io/some_other_page", 1} in drilldown + end + + test "counts nil values as a group" do + site = insert(:site) + + insert(:pageview, %{ + hostname: site.domain, + referrer: nil, + referrer_source: "10words", + new_visitor: true + }) + + insert(:pageview, %{ + hostname: site.domain, + referrer: nil, + referrer_source: "10words", + new_visitor: true + }) + + query = Stats.Query.from("UTC", %{"period" => "day"}) + drilldown = Stats.referrer_drilldown(site, query, "10words") + + assert {nil, 2} in drilldown + end + end + + defp months_ago(months) do + Timex.now() |> Timex.shift(months: -months) + end +end diff --git a/test/plausible_web/controllers/api/external_controller_test.exs b/test/plausible_web/controllers/api/external_controller_test.exs new file mode 100644 index 000000000..6cb5457ea --- /dev/null +++ b/test/plausible_web/controllers/api/external_controller_test.exs @@ -0,0 +1,288 @@ +defmodule PlausibleWeb.Api.ExternalControllerTest do + use PlausibleWeb.ConnCase + use Plausible.Repo + + @user_agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36" + @country_code "EE" + + describe "POST /api/page" do + test "records the pageview", %{conn: conn} do + params = %{ + url: "http://gigride.live/", + referrer: "http://m.facebook.com/", + new_visitor: true, + screen_width: 1440, + uid: UUID.uuid4() + } + + conn = conn + |> put_req_header("content-type", "text/plain") + |> put_req_header("user-agent", @user_agent) + |> put_req_header("cf-ipcountry", @country_code) + |> post("/api/page", Jason.encode!(params)) + + pageview = Repo.one(Plausible.Pageview) + + assert response(conn, 202) == "" + assert pageview.hostname == "gigride.live" + assert pageview.pathname == "/" + assert pageview.new_visitor == true + assert pageview.user_agent == @user_agent + assert pageview.screen_width == params[:screen_width] + assert pageview.country_code == @country_code + end + + test "www. is stripped from hostname", %{conn: conn} do + params = %{ + url: "http://www.example.com/", + uid: UUID.uuid4(), + new_visitor: true + } + + conn + |> put_req_header("content-type", "text/plain") + |> post("/api/page", Jason.encode!(params)) + + pageview = Repo.one(Plausible.Pageview) + + assert pageview.hostname == "example.com" + end + + test "bots and crawlers are ignored", %{conn: conn} do + params = %{ + url: "http://www.example.com/", + new_visitor: true + } + + conn + |> put_req_header("content-type", "text/plain") + |> put_req_header("user-agent", "generic crawler") + |> post("/api/page", Jason.encode!(params)) + + pageviews = Repo.all(Plausible.Pageview) + + assert Enum.count(pageviews) == 0 + end + + test "parses user_agent", %{conn: conn} do + params = %{ + url: "http://gigride.live/", + new_visitor: false, + uid: UUID.uuid4() + } + + conn = conn + |> put_req_header("content-type", "text/plain") + |> put_req_header("user-agent", @user_agent) + |> post("/api/page", Jason.encode!(params)) + + pageview = Repo.one(Plausible.Pageview) + + assert response(conn, 202) == "" + assert pageview.operating_system == "Mac" + assert pageview.browser == "Chrome" + end + + test "parses referrer", %{conn: conn} do + params = %{ + url: "http://gigride.live/", + referrer: "https://facebook.com", + new_visitor: false, + uid: UUID.uuid4() + } + + conn = conn + |> put_req_header("content-type", "text/plain") + |> put_req_header("user-agent", @user_agent) + |> post("/api/page", Jason.encode!(params)) + + pageview = Repo.one(Plausible.Pageview) + + assert response(conn, 202) == "" + assert pageview.referrer_source == "Facebook" + end + + test "ignores when referrer is internal", %{conn: conn} do + params = %{ + url: "http://gigride.live/", + referrer: "https://gigride.live", + new_visitor: false, + uid: UUID.uuid4() + } + + conn = conn + |> put_req_header("content-type", "text/plain") + |> put_req_header("user-agent", @user_agent) + |> post("/api/page", Jason.encode!(params)) + + pageview = Repo.one(Plausible.Pageview) + + assert response(conn, 202) == "" + assert pageview.referrer_source == nil + end + + test "ignores localhost referrer", %{conn: conn} do + params = %{ + url: "http://gigride.live/", + referrer: "http://localhost:4000/", + new_visitor: true, + uid: UUID.uuid4() + } + + conn = conn + |> put_req_header("content-type", "text/plain") + |> put_req_header("user-agent", @user_agent) + |> post("/api/page", Jason.encode!(params)) + + pageview = Repo.one(Plausible.Pageview) + + assert response(conn, 202) == "" + assert pageview.referrer_source == nil + end + + test "parses subdomain referrer", %{conn: conn} do + params = %{ + url: "http://gigride.live/", + referrer: "https://blog.gigride.live", + new_visitor: false, + uid: UUID.uuid4() + } + + conn = conn + |> put_req_header("content-type", "text/plain") + |> put_req_header("user-agent", @user_agent) + |> post("/api/page", Jason.encode!(params)) + + pageview = Repo.one(Plausible.Pageview) + + assert response(conn, 202) == "" + assert pageview.referrer_source == "blog.gigride.live" + end + + test "referrer is cleaned", %{conn: conn} do + params = %{ + url: "http://www.example.com/", + referrer: "https://www.indiehackers.com/page?query=param#hash", + uid: UUID.uuid4(), + new_visitor: true + } + + conn + |> put_req_header("content-type", "text/plain") + |> post("/api/page", Jason.encode!(params)) + + pageview = Repo.one(Plausible.Pageview) + + assert pageview.referrer == "indiehackers.com/page" + assert pageview.raw_referrer == "https://www.indiehackers.com/page?query=param#hash" + end + + test "?ref= query param controls the referrer source", %{conn: conn} do + params = %{ + url: "http://www.example.com/?wat=wet&ref=traffic-source", + referrer: "https://www.indiehackers.com/page", + uid: UUID.uuid4(), + new_visitor: true + } + + conn + |> put_req_header("content-type", "text/plain") + |> post("/api/page", Jason.encode!(params)) + + pageview = Repo.one(Plausible.Pageview) + + assert pageview.referrer_source == "traffic-source" + end + + test "?utm_source= query param controls the referrer source", %{conn: conn} do + params = %{ + url: "http://www.example.com/?wat=wet&utm_source=traffic-source", + referrer: "https://www.indiehackers.com/page", + uid: UUID.uuid4(), + new_visitor: true + } + + conn + |> put_req_header("content-type", "text/plain") + |> post("/api/page", Jason.encode!(params)) + + pageview = Repo.one(Plausible.Pageview) + + assert pageview.referrer_source == "traffic-source" + end + + test "ignores pageviews from a user blacklist", %{conn: conn} do + params = %{ + url: "http://gigride.live/", + referrer: "https://blog.gigride.live", + new_visitor: false, + uid: "e8150466-7ddb-4771-bcf5-7c58f232e8a6" + } + + conn + |> put_req_header("content-type", "text/plain") + |> put_req_header("user-agent", @user_agent) + |> post("/api/page", Jason.encode!(params)) + + assert Repo.aggregate(Plausible.Pageview, :count, :id) == 0 + end + + test "if it's an :unknown referrer, just the domain is used", %{conn: conn} do + params = %{ + url: "http://gigride.live/", + referrer: "https://www.indiehackers.com/landing-page-feedback", + new_visitor: false, + uid: UUID.uuid4() + } + + conn = conn + |> put_req_header("content-type", "text/plain") + |> put_req_header("user-agent", @user_agent) + |> post("/api/page", Jason.encode!(params)) + + pageview = Repo.one(Plausible.Pageview) + + assert response(conn, 202) == "" + assert pageview.referrer_source == "indiehackers.com" + end + + test "if the referrer is not http or https, it is ignored", %{conn: conn} do + params = %{ + url: "http://gigride.live/", + referrer: "android-app://com.google.android.gm", + new_visitor: false, + uid: UUID.uuid4() + } + + conn = conn + |> put_req_header("content-type", "text/plain") + |> put_req_header("user-agent", @user_agent) + |> post("/api/page", Jason.encode!(params)) + + pageview = Repo.one(Plausible.Pageview) + + assert response(conn, 202) == "" + assert is_nil(pageview.referrer_source) + end + + end + + test "screen size is calculated from screen_width", %{conn: conn} do + params = %{ + url: "http://gigride.live/", + new_visitor: true, + screen_width: 480, + uid: UUID.uuid4() + } + + conn = conn + |> put_req_header("content-type", "text/plain") + |> put_req_header("user-agent", @user_agent) + |> post("/api/page", Jason.encode!(params)) + + pageview = Repo.one(Plausible.Pageview) + + assert response(conn, 202) == "" + assert pageview.screen_size == "Mobile" + end +end diff --git a/test/plausible_web/controllers/api/internal_controller_test.exs b/test/plausible_web/controllers/api/internal_controller_test.exs new file mode 100644 index 000000000..043a8973d --- /dev/null +++ b/test/plausible_web/controllers/api/internal_controller_test.exs @@ -0,0 +1,23 @@ +defmodule PlausibleWeb.Api.InternalControllerTest do + use PlausibleWeb.ConnCase + use Plausible.Repo + import Plausible.TestUtils + + describe "GET /:domain/status" do + setup [:create_user, :log_in, :create_site] + + test "is WAITING when site has no pageviews", %{conn: conn, site: site} do + conn = get(conn, "/api/#{site.domain}/status") + + assert json_response(conn, 200) == "WAITING" + end + + test "is READY when site has at least 1 pageview", %{conn: conn, site: site} do + insert(:pageview, hostname: site.domain) + + conn = get(conn, "/api/#{site.domain}/status") + + assert json_response(conn, 200) == "READY" + end + end +end diff --git a/test/plausible_web/controllers/api/paddle_controller_test.exs b/test/plausible_web/controllers/api/paddle_controller_test.exs new file mode 100644 index 000000000..ad4ca8db9 --- /dev/null +++ b/test/plausible_web/controllers/api/paddle_controller_test.exs @@ -0,0 +1,41 @@ +defmodule PlausibleWeb.Api.PaddleControllerTest do + use PlausibleWeb.ConnCase + use Plausible.Repo + + @body %{ + "alert_id" => "16173800", + "alert_name"=> "subscription_created", + "cancel_url"=> "https://checkout.paddle.com/subscription/cancel?user=1032746&subscription=1869424&hash=eyJpdiI6Ik5XSnhkT1k2RSticStZcEVYZ2FEeTVZTHVSaTNpcFM4XC9aMFJNSjh6TFNrPSIsInZhbHVlIjoiWTMzd3dENGQySTc0UjVPM0U3dzVDUWlBMEQ3dGVzM2lyODRPczBVcjdBRXlncmQzRUVmQjhnZVhpTzJIRjFtNW41MmZOVmJNVUJ4empmeXgyUno1Z3c9PSIsIm1hYyI6ImFjNDMyNDIxNmNmMWNiNmViMmFlNGVkMzQ3ZjYyYTQ5ZWI2YTEyMDQ4YjFhNTEyMjAxNzNlNjEwOWU4OTVhOWMifQ%3D%3D", + "checkout_id"=> "38111668-chre6449b9c3cc8-f473e48a86", + "currency"=> "USD", + "email"=> "josh@joshuae.com", + "event_time"=> "2019-08-20 21:44:54", + "marketing_consent"=> 0, + "next_bill_date"=> "2019-09-20", + "passthrough"=> "235", + "quantity"=> "1", + "status"=> "active", + "subscription_id"=> "1869424", + "subscription_plan_id"=> "558018", + "unit_price"=> "6.00", + "update_url"=> "https://checkout.paddle.com/subscription/update?user=1032746&subscription=1869424&hash=eyJpdiI6IkNUS2VZQlRxcFA5MVlEXC9Oa1ZwRDBNaGN4VVwvaU1RU2srWXc0bU1tQndyTT0iLCJ2YWx1ZSI6ImtTeU1ESkxWcEVrTDFmKzkyZ0FaSFo2Q0VNK3A2XC9NdEU4S2tGVFE2blJicGxBQzZ1XC9mMG1PcUo3MWV2OGY4YURjT1UxY2hpeHh5SFhlMmhXaFpoalE9PSIsIm1hYyI6ImZkNGU5MTg3YzQxZWYxNDJjNDkyMWFkYmZhZjIyMGQ3ZGI2YTVmMTcxZGViY2VkNzI0ZjNlMDRkZTgwNTEwMzUifQ%3D%3D", + "user_id"=> "1032746", + "p_signature"=> "qfqKA3dI9d60uie9IORcvkHYV+rd1UaCu/f5kh4miTkeIQNimgusQG8pS1OHobCvN/OktwKCjFcbIwoa4nakOOWGroHJ8FjLJHBK4g1uI37Bp6l73dNl8mB4dNGW1M+atkz7ag6pETRIdEKCmC5tV9afN5CvbcqRV1lsj/x2fAsjAe/sQkmAP1jbDXOMEuHqkWssSB7Q+NGHHLHuNQ67m7YFBnZSgYzLeLMEApkZClJn0j6MokUVjW37ISn5eA5FlUbT7s6Kph54roRzLIpYvC+ff/n6ae2Iu1OsORxRBg4Uv8dqqjqBKXlv84/OB80U89yMIbRw/pbHD6+zF4FxgNV7nk2bjgK2V6h55AOuhJHHUMb4XX9R8i8iG1FOlNJaTwbhkIkvQF3q7nEItKCqizn+l4tFQ9MUcrjw8jytDznbOnSlmNhtcDVlnvXNDaSPkEA7AyR6c+BiZV/Y6I3y8sr8h/F/cBM3OPTwfdKK34jyWW4LRn15nSxq2kjH3SyLPEpTJUMdcRGAgBZc06E4lENU2x22E/JKG5BRi1aDs5OFQtrjYi2hOTI0dyPF3OLNeZcCgBCKBmKq5XIf1T0RPFWAWtKkzXhl/QH+4feNATb9/i6k5xKeUJf0ltWzsI5x84kvsC/m05hn/AuBDmcZGkVnDLXrqttR+zDXY6P1euE=" + } + + describe "webhook verification" do + test "is verified when signature is correct", %{conn: conn} do + insert(:user, id: 235) + conn = post(conn, "/api/paddle/webhook", @body) + + assert conn.status == 200 + end + + test "not verified when signature is corrupted", %{conn: conn} do + corrupted = Map.put(@body, "p_signature", Base.encode64("123 fake signature")) + conn = post(conn, "/api/paddle/webhook", corrupted) + assert conn.status == 400 + end + end + +end diff --git a/test/plausible_web/controllers/auth_controller_test.exs b/test/plausible_web/controllers/auth_controller_test.exs new file mode 100644 index 000000000..67098aab8 --- /dev/null +++ b/test/plausible_web/controllers/auth_controller_test.exs @@ -0,0 +1,170 @@ +defmodule PlausibleWeb.AuthControllerTest do + use PlausibleWeb.ConnCase + use Bamboo.Test + import Plausible.TestUtils + + describe "GET /register" do + test "shows the register form", %{conn: conn} do + conn = get(conn, "/register") + + assert html_response(conn, 200) =~ "Enter your details to get started" + end + + test "registering sends an activation link", %{conn: conn} do + post(conn, "/register", user: %{ + name: "Jane Doe", + email: "user@example.com" + }) + + assert_email_delivered_with(subject: "Plausible activation link") + end + + test "user sees success page after registering", %{conn: conn} do + conn = post(conn, "/register", user: %{ + name: "Jane Doe", + email: "user@example.com" + }) + + assert html_response(conn, 200) =~ "Success!" + end + end + + describe "GET /claim-activation" do + test "creates the user", %{conn: conn} do + token = Plausible.Auth.Token.sign_activation("Jane Doe", "user@example.com") + get(conn, "/claim-activation?token=#{token}") + + assert Plausible.Auth.find_user_by(email: "user@example.com") + end + + test "redirects new user to create a password", %{conn: conn} do + token = Plausible.Auth.Token.sign_activation("Jane Doe", "user@example.com") + conn = get(conn, "/claim-activation?token=#{token}") + + assert redirected_to(conn) == "/password" + end + + test "shows error when user with that email already exists", %{conn: conn} do + token = Plausible.Auth.Token.sign_activation("Jane Doe", "user@example.com") + + conn = get(conn, "/claim-activation?token=#{token}") + conn = get(conn, "/claim-activation?token=#{token}") + + assert conn.status == 400 + end + end + + describe "GET /login_form" do + test "shows the login form", %{conn: conn} do + conn = get(conn, "/login") + assert html_response(conn, 200) =~ "Enter your email and password" + end + end + + describe "POST /login" do + test "valid email and password - logs the user in", %{conn: conn} do + user = insert(:user, password: "password") + + conn = post(conn, "/login", email: user.email, password: "password") + + assert get_session(conn, :current_user_id) == user.id + assert redirected_to(conn) == "/" + end + + test "email does not exist - renders login form again", %{conn: conn} do + + conn = post(conn, "/login", email: "user@example.com", password: "password") + + assert get_session(conn, :current_user_id) == nil + assert html_response(conn, 200) =~ "Enter your email and password" + end + + test "bad password - renders login form again", %{conn: conn} do + user = insert(:user, password: "password") + conn = post(conn, "/login", email: user.email, password: "wrong") + + assert get_session(conn, :current_user_id) == nil + assert html_response(conn, 200) =~ "Enter your email and password" + end + end + + describe "GET /password/request-reset" do + test "renders the form", %{conn: conn} do + conn = get(conn, "/password/request-reset") + assert html_response(conn, 200) =~ "Enter your email so we can send a password reset link" + end + end + + describe "POST /password/request-reset" do + test "email is empty - renders form with error", %{conn: conn} do + conn = post(conn, "/password/request-reset", %{email: ""}) + + assert html_response(conn, 200) =~ "Enter your email so we can send a password reset link" + end + + test "email is present and exists - sends password reset email", %{conn: conn} do + user = insert(:user) + conn = post(conn, "/password/request-reset", %{email: user.email}) + + assert html_response(conn, 200) =~ "Success!" + assert_email_delivered_with(subject: "Plausible password reset") + end + end + + describe "GET /password/reset" do + test "with valid token - shows form", %{conn: conn} do + token = Plausible.Auth.Token.sign_password_reset("email@example.com") + conn = get(conn, "/password/reset", %{token: token}) + + assert html_response(conn, 200) =~ "Reset your password" + end + + test "with invalid token - shows error page", %{conn: conn} do + conn = get(conn, "/password/reset", %{token: "blabla"}) + + assert html_response(conn, 401) =~ "Your token is invalid" + end + end + + describe "POST /password/reset" do + alias Plausible.Auth.{User, Token, Password} + + test "with valid token - resets the password", %{conn: conn} do + user = insert(:user) + token = Token.sign_password_reset(user.email) + post(conn, "/password/reset", %{token: token, password: "new-password"}) + + user = Plausible.Repo.get(User, user.id) + assert Password.match?("new-password", user.password_hash) + end + end + + describe "GET /settings" do + setup [:create_user, :log_in] + + test "shows the form", %{conn: conn} do + conn = get(conn, "/settings") + assert html_response(conn, 200) =~ "Account settings" + end + end + + describe "DELETE /me" do + setup [:create_user, :log_in, :create_site] + use Plausible.Repo + + test "deletes the user", %{conn: conn, user: user} do + Repo.insert_all("intro_emails", [%{ + user_id: user.id, + timestamp: NaiveDateTime.utc_now() + }]) + + Repo.insert_all("feedback_emails", [%{ + user_id: user.id, + timestamp: NaiveDateTime.utc_now() + }]) + + conn = delete(conn, "/me") + assert redirected_to(conn) == "/" + end + end +end diff --git a/test/plausible_web/controllers/billing_controller_test.exs b/test/plausible_web/controllers/billing_controller_test.exs new file mode 100644 index 000000000..875d094e9 --- /dev/null +++ b/test/plausible_web/controllers/billing_controller_test.exs @@ -0,0 +1,22 @@ +defmodule PlausibleWeb.BillingControllerTest do + use PlausibleWeb.ConnCase + import Plausible.TestUtils + + describe "GET /change-plan" do + setup [:create_user, :log_in] + + test "redirects to /upgrade if user does not have a subscription", %{conn: conn} do + conn = get(conn, "/billing/change-plan") + + assert redirected_to(conn) == "/billing/upgrade" + end + end + + describe "POST /change-plan" do + setup [:create_user, :log_in] + + test "calls Paddle API to update subscription" do + + end + end +end diff --git a/test/plausible_web/controllers/page_controller_test.exs b/test/plausible_web/controllers/page_controller_test.exs new file mode 100644 index 000000000..9018d3003 --- /dev/null +++ b/test/plausible_web/controllers/page_controller_test.exs @@ -0,0 +1,10 @@ +defmodule PlausibleWeb.PageControllerTest do + use PlausibleWeb.ConnCase + + describe "GET /" do + test "shows the landing page", %{conn: conn} do + conn = get(conn, "/") + assert html_response(conn, 200) =~ "Plausible" + end + end +end diff --git a/test/plausible_web/controllers/site_controller_test.exs b/test/plausible_web/controllers/site_controller_test.exs new file mode 100644 index 000000000..8762ab82d --- /dev/null +++ b/test/plausible_web/controllers/site_controller_test.exs @@ -0,0 +1,126 @@ +defmodule PlausibleWeb.SiteControllerTest do + use PlausibleWeb.ConnCase + use Plausible.Repo + import Plausible.TestUtils + + describe "GET /sites/new" do + setup [:create_user, :log_in] + + test "shows the site form", %{conn: conn} do + conn = get(conn, "/sites/new") + assert html_response(conn, 200) =~ "Your website details" + end + end + + describe "POST /sites" do + setup [:create_user, :log_in] + + test "creates the site with valid params", %{conn: conn} do + conn = post(conn, "/sites", %{ + "site" => %{ + "domain" => "example.com", + "timezone" => "Europe/London" + } + }) + + assert redirected_to(conn) == "/example.com/snippet" + assert Repo.exists?(Plausible.Site, domain: "example.com") + end + + test "cleans up the url", %{conn: conn} do + conn = post(conn, "/sites", %{ + "site" => %{ + "domain" => "https://www.Example.com/", + "timezone" => "Europe/London" + } + }) + + assert redirected_to(conn) == "/example.com/snippet" + assert Repo.exists?(Plausible.Site, domain: "example.com") + end + + test "renders form again when domain is missing", %{conn: conn} do + conn = post(conn, "/sites", %{ + "site" => %{ + "timezone" => "Europe/London" + } + }) + + assert html_response(conn, 200) =~ "can't be blank" + end + + test "renders form again when it is a duplicate domain", %{conn: conn} do + insert(:site, domain: "example.com") + + conn = post(conn, "/sites", %{ + "site" => %{ + "domain" => "example.com", + "timezone" => "Europe/London" + } + }) + + assert html_response(conn, 200) =~ "has already been taken" + end + end + + describe "GET /:website/settings" do + setup [:create_user, :log_in, :create_site] + + test "shows settings form", %{conn: conn, site: site} do + conn = get(conn, "/#{site.domain}/settings") + + assert html_response(conn, 200) =~ "Settings" + end + end + + describe "PUT /:website/settings" do + setup [:create_user, :log_in, :create_site] + + test "updates the timezone", %{conn: conn, site: site} do + put(conn, "/#{site.domain}/settings", %{ + "site" => %{ + "timezone" => "Europe/London" + } + }) + + updated = Repo.get(Plausible.Site, site.id) + assert updated.timezone == "Europe/London" + end + end + + describe "POST /sites/:website/make-public" do + setup [:create_user, :log_in, :create_site] + + test "makes the site public", %{conn: conn, site: site} do + post(conn, "/sites/#{site.domain}/make-public") + + updated = Repo.get(Plausible.Site, site.id) + assert updated.public + end + end + + describe "POST /sites/:website/make-private" do + setup [:create_user, :log_in, :create_site] + + test "makes the site private", %{conn: conn, site: site} do + post(conn, "/sites/#{site.domain}/make-private") + + updated = Repo.get(Plausible.Site, site.id) + refute updated.public + end + end + + describe "DELETE /:website" do + setup [:create_user, :log_in, :create_site] + + test "deletes the site and all pageviews", %{conn: conn, user: user, site: site} do + pageview = insert(:pageview, hostname: site.domain) + insert(:google_auth, user: user, site: site) + + delete(conn, "/#{site.domain}") + + refute Repo.exists?(from s in Plausible.Site, where: s.id == ^site.id) + refute Repo.exists?(from p in Plausible.Pageview, where: p.id == ^pageview.id) + end + end +end diff --git a/test/plausible_web/controllers/stats_controller_test.exs b/test/plausible_web/controllers/stats_controller_test.exs new file mode 100644 index 000000000..e874f9dee --- /dev/null +++ b/test/plausible_web/controllers/stats_controller_test.exs @@ -0,0 +1,40 @@ +defmodule PlausibleWeb.StatsControllerTest do + use PlausibleWeb.ConnCase + use Plausible.Repo + import Plausible.TestUtils + + describe "as an anonymous visitor" do + test "public site - shows site stats", %{conn: conn} do + insert(:site, domain: "public-site.io", public: true) + insert(:pageview, hostname: "public-site.io") + + conn = get(conn, "/public-site.io") + assert html_response(conn, 200) =~ "Analytics for" + end + + test "can not view stats of a private website", %{conn: conn} do + insert(:pageview, hostname: "some-other-site.com") + + conn = get(conn, "/some-other-site.com") + assert html_response(conn, 404) =~ "There's nothing here" + end + end + + describe "as a logged in user" do + setup [:create_user, :log_in, :create_site] + + test "can view stats of a website I've created", %{conn: conn, site: site} do + insert(:pageview, hostname: site.domain) + + conn = get(conn, "/" <> site.domain) + assert html_response(conn, 200) =~ "Analytics for" + end + + test "can not view stats of someone else's website", %{conn: conn} do + insert(:pageview, hostname: "some-other-site.com") + + conn = get(conn, "/some-other-site.com") + assert html_response(conn, 404) =~ "There's nothing here" + end + end +end diff --git a/test/plausible_web/views/email_view_test.exs b/test/plausible_web/views/email_view_test.exs new file mode 100644 index 000000000..37521055d --- /dev/null +++ b/test/plausible_web/views/email_view_test.exs @@ -0,0 +1,16 @@ +defmodule PlausibleWeb.EmailViewTest do + use PlausibleWeb.ConnCase, async: true + alias PlausibleWeb.EmailView + + describe "user salutation" do + test "picks first name if full name has two parts" do + user1 = %Plausible.Auth.User{name: "Jane"} + user2 = %Plausible.Auth.User{name: "Jane Doe"} + user3 = %Plausible.Auth.User{name: "Jane Alice Doe"} + + assert EmailView.user_salutation(user1) == "Jane" + assert EmailView.user_salutation(user2) == "Jane" + assert EmailView.user_salutation(user3) == "Jane" + end + end +end diff --git a/test/plausible_web/views/stats_view_test.exs b/test/plausible_web/views/stats_view_test.exs new file mode 100644 index 000000000..07922bb6c --- /dev/null +++ b/test/plausible_web/views/stats_view_test.exs @@ -0,0 +1,46 @@ +defmodule PlausibleWeb.StatsView.Test do + use PlausibleWeb.ConnCase, async: true + alias PlausibleWeb.StatsView + + describe "large_number_format" do + test "numbers under 1000 stay the same" do + assert StatsView.large_number_format(100) == "100" + end + + test "1000 becomes 1k" do + assert StatsView.large_number_format(1000) == "1k" + end + + test "1111 becomes 1.1k" do + assert StatsView.large_number_format(1111) == "1.1k" + end + + test "10_000 becomes 10k" do + assert StatsView.large_number_format(10_000) == "10k" + end + + test "15_993 becomes 15.9k" do + assert StatsView.large_number_format(15_923) == "15.9k" + end + + test "wat" do + assert StatsView.large_number_format(49012) == "49k" + end + + test "999_999 becomes 999k" do + assert StatsView.large_number_format(999_999) == "999k" + end + + test "1_000_000 becomes 1m" do + assert StatsView.large_number_format(1_000_000) == "1m" + end + + test "2_590_000 becomes 2.5m" do + assert StatsView.large_number_format(2_590_000) == "2.5m" + end + + test "99_999_999 becomes 99.9m" do + assert StatsView.large_number_format(99_999_999) == "99.9m" + end + end +end diff --git a/test/support/channel_case.ex b/test/support/channel_case.ex new file mode 100644 index 000000000..288090319 --- /dev/null +++ b/test/support/channel_case.ex @@ -0,0 +1,37 @@ +defmodule PlausibleWeb.ChannelCase do + @moduledoc """ + This module defines the test case to be used by + channel tests. + + Such tests rely on `Phoenix.ChannelTest` and also + import other functionality to make it easier + to build common data structures and query the data layer. + + Finally, if the test case interacts with the database, + it cannot be async. For this reason, every test runs + inside a transaction which is reset at the beginning + of the test unless the test case is marked as async. + """ + + use ExUnit.CaseTemplate + + using do + quote do + # Import conveniences for testing with channels + use Phoenix.ChannelTest + + # The default endpoint for testing + @endpoint PlausibleWeb.Endpoint + end + end + + setup tags do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Plausible.Repo) + + unless tags[:async] do + Ecto.Adapters.SQL.Sandbox.mode(Plausible.Repo, {:shared, self()}) + end + + :ok + end +end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex new file mode 100644 index 000000000..1af947660 --- /dev/null +++ b/test/support/conn_case.ex @@ -0,0 +1,39 @@ +defmodule PlausibleWeb.ConnCase do + @moduledoc """ + This module defines the test case to be used by + tests that require setting up a connection. + + Such tests rely on `Phoenix.ConnTest` and also + import other functionality to make it easier + to build common data structures and query the data layer. + + Finally, if the test case interacts with the database, + it cannot be async. For this reason, every test runs + inside a transaction which is reset at the beginning + of the test unless the test case is marked as async. + """ + + use ExUnit.CaseTemplate + + using do + quote do + # Import conveniences for testing with connections + use Phoenix.ConnTest + alias PlausibleWeb.Router.Helpers, as: Routes + import Plausible.Factory + + # The default endpoint for testing + @endpoint PlausibleWeb.Endpoint + end + end + + setup tags do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Plausible.Repo) + + unless tags[:async] do + Ecto.Adapters.SQL.Sandbox.mode(Plausible.Repo, {:shared, self()}) + end + + {:ok, conn: Phoenix.ConnTest.build_conn()} + end +end diff --git a/test/support/data_case.ex b/test/support/data_case.ex new file mode 100644 index 000000000..73b06dc4d --- /dev/null +++ b/test/support/data_case.ex @@ -0,0 +1,52 @@ +defmodule Plausible.DataCase do + @moduledoc """ + This module defines the setup for tests requiring + access to the application's data layer. + + You may define functions here to be used as helpers in + your tests. + + Finally, if the test case interacts with the database, + it cannot be async. For this reason, every test runs + inside a transaction which is reset at the beginning + of the test unless the test case is marked as async. + """ + + use ExUnit.CaseTemplate + + using do + quote do + use Plausible.Repo + + import Ecto.Changeset + import Plausible.DataCase + import Plausible.Factory + end + end + + setup tags do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Plausible.Repo) + + unless tags[:async] do + Ecto.Adapters.SQL.Sandbox.mode(Plausible.Repo, {:shared, self()}) + end + + :ok + end + + @doc """ + A helper that transforms changeset errors into a map of messages. + + assert {:error, changeset} = Accounts.create_user(%{password: "short"}) + assert "password is too short" in errors_on(changeset).password + assert %{password: ["password is too short"]} = errors_on(changeset) + + """ + def errors_on(changeset) do + Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> + Enum.reduce(opts, message, fn {key, value}, acc -> + String.replace(acc, "%{#{key}}", to_string(value)) + end) + end) + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex new file mode 100644 index 000000000..a51ef8e5f --- /dev/null +++ b/test/support/factory.ex @@ -0,0 +1,56 @@ +defmodule Plausible.Factory do + use ExMachina.Ecto, repo: Plausible.Repo + + def user_factory(attrs) do + pw = Map.get(attrs, :password, "password") + + user = %Plausible.Auth.User{ + name: "Jane Smith", + email: sequence(:email, &"email-#{&1}@example.com"), + password_hash: Plausible.Auth.Password.hash(pw) + } + + merge_attributes(user, attrs) + end + + def site_factory do + domain = sequence(:domain, &"example-#{&1}.com") + + %Plausible.Site{ + domain: domain, + timezone: "UTC", + } + end + + def pageview_factory do + hostname = sequence(:domain, &"example-#{&1}.com") + + %Plausible.Pageview{ + hostname: hostname, + pathname: "/", + new_visitor: true, + user_id: UUID.uuid4(), + } + end + + def subscription_factory do + %Plausible.Billing.Subscription{ + paddle_subscription_id: sequence(:paddle_subscription_id, &"subscription-#{&1}"), + paddle_plan_id: sequence(:paddle_plan_id, &"plan-#{&1}"), + cancel_url: "cancel.com", + update_url: "cancel.com", + status: "active", + next_bill_amount: "6.00", + next_bill_date: Timex.today() + } + end + + def google_auth_factory do + %Plausible.Site.GoogleAuth{ + email: sequence(:google_auth_email, &"email-#{&1}@email.com"), + refresh_token: "123", + access_token: "123", + expires: Timex.now() |> Timex.shift(days: 1) + } + end +end diff --git a/test/support/paddle_api_mock.ex b/test/support/paddle_api_mock.ex new file mode 100644 index 000000000..3aa0189b8 --- /dev/null +++ b/test/support/paddle_api_mock.ex @@ -0,0 +1,10 @@ +defmodule Plausible.PaddleApi.Mock do + def get_subscription(_) do + {:ok, %{ + "next_payment" => %{ + "date" => "2019-07-10", + "amount" => 6 + } + }} + end +end diff --git a/test/support/test_utils.ex b/test/support/test_utils.ex new file mode 100644 index 000000000..bd3399398 --- /dev/null +++ b/test/support/test_utils.ex @@ -0,0 +1,34 @@ +defmodule Plausible.TestUtils do + use Plausible.Repo + alias Plausible.Factory + + def create_user(_) do + {:ok, user: Factory.insert(:user)} + end + + def create_site(%{user: user}) do + site = Factory.insert(:site) + Plausible.Site.Membership.changeset(%Plausible.Site.Membership{}, %{site_id: site.id, user_id: user.id}) |> Repo.insert! + {:ok, site: site} + end + + def log_in(%{user: user, conn: conn}) do + opts = + Plug.Session.init( + store: :cookie, + key: "foobar", + encryption_salt: "encrypted cookie salt", + signing_salt: "signing salt", + log: false, + encrypt: false + ) + + conn = + conn + |> Plug.Session.call(opts) + |> Plug.Conn.fetch_session() + |> Plug.Conn.put_session(:current_user_id, user.id) + + {:ok, conn: conn} + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 000000000..f836109c6 --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1,3 @@ +{:ok, _} = Application.ensure_all_started(:ex_machina) +ExUnit.start() +Ecto.Adapters.SQL.Sandbox.mode(Plausible.Repo, :manual)