diff --git a/.travis.yml b/.travis.yml index 3819858..4beb7a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,12 @@ language: node_js node_js: -- stable + - stable cache: yarn branches: only: - - master + - master +services: + - redis-server before_deploy: yarn build deploy: provider: heroku diff --git a/Procfile b/Procfile index 4e16c15..35b7fff 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,2 @@ web: node server.js ingest_logs: node server/IngestLogsWorker.js -ingest_stats: node server/IngestStatsWorker.js diff --git a/client/About.js b/client/About.js index 5c0d99c..9b8e883 100644 --- a/client/About.js +++ b/client/About.js @@ -1,8 +1,7 @@ import React from 'react' import contentHTML from './About.md' -function About() { - return
-} +const About = () => +
export default About diff --git a/client/About.md b/client/About.md index e54b017..39360a2 100644 --- a/client/About.md +++ b/client/About.md @@ -1,12 +1,4 @@ -unpkg is an [open source](https://github.com/mjackson/unpkg) project built by me, [Michael Jackson](https://twitter.com/mjackson). I built it because, as an npm package author, it felt tedious for me to use existing, git-based CDNs to make my open source work available via CDN. Development was sponsored by my company, [React Training](https://reacttraining.com). - -
- -
- -We'd love to talk to you more about training your team on [React](https://facebook.github.io/react/). Please [get in touch](mailto:hello@reacttraining.com) if interested. +unpkg is an [open source](https://github.com/unpkg) project built and maintained by [Michael Jackson](https://twitter.com/mjackson). ### Sponsors @@ -21,8 +13,6 @@ The fast, global infrastructure that powers unpkg is generously donated by [Clou
-These sponsors provide some of the most robust, reliable infrastructure available today and I'm happy to be able to partner with them on unpkg. - ### Cache Behavior The CDN caches all files based on their permanent URL, which includes the npm package version. This works because npm does not allow package authors to overwrite a package that has already been published with a different one at the same version number. diff --git a/client/App.js b/client/App.js index 8a92172..5b283af 100644 --- a/client/App.js +++ b/client/App.js @@ -1,20 +1,10 @@ import React from 'react' -import { HashRouter as Router, Switch, Route } from 'react-router-dom' +import { HashRouter } from 'react-router-dom' import Layout from './Layout' -import About from './About' -import Stats from './Stats' -import Home from './Home' -const App = () => ( - - - - - - - - - -) +const App = () => + + + export default App diff --git a/client/CountryUtils.js b/client/CountryUtils.js deleted file mode 100644 index a811a87..0000000 --- a/client/CountryUtils.js +++ /dev/null @@ -1,13 +0,0 @@ -import { - continents as ContinentsIndex, - countries as CountriesIndex -} from 'countries-list' - -const getCountriesByContinent = (continent) => - Object.keys(CountriesIndex).filter(country => CountriesIndex[country].continent === continent) - -export { - ContinentsIndex, - CountriesIndex, - getCountriesByContinent -} diff --git a/client/DOMUtils.js b/client/DOMUtils.js deleted file mode 100644 index 9c96fea..0000000 --- a/client/DOMUtils.js +++ /dev/null @@ -1,15 +0,0 @@ -export const addEvent = (node, type, handler) => { - if (node.addEventListener) { - node.addEventListener(type, handler, false) - } else if (node.attachEvent) { - node.attachEvent('on' + type, handler) - } -} - -export const removeEvent = (node, type, handler) => { - if (node.removeEventListener) { - node.removeEventListener(type, handler, false) - } else if (node.detachEvent) { - node.detachEvent('on' + type, handler) - } -} diff --git a/client/Home.js b/client/Home.js index b18879a..f99e466 100644 --- a/client/Home.js +++ b/client/Home.js @@ -1,8 +1,7 @@ import React from 'react' import contentHTML from './Home.md' -function Home() { - return
-} +const Home = () => +
export default Home diff --git a/client/Home.md b/client/Home.md index a34f27a..f0e5db5 100644 --- a/client/Home.md +++ b/client/Home.md @@ -1,60 +1,43 @@ -unpkg is a fast, global [content delivery network](https://en.wikipedia.org/wiki/Content_delivery_network) for everything on [npm](https://www.npmjs.com/). Use it to quickly and easily load files using a simple URL like: +unpkg is a fast, global [content delivery network](https://en.wikipedia.org/wiki/Content_delivery_network) for everything on [npm](https://www.npmjs.com/). Use it to quickly and easily load any file from any package using a URL like: -
`https://unpkg.com/package@version/file`
+
unpkg.com/:package@:version/:file
-A few examples: +### Examples - * [https://unpkg.com/react@15.3.1/dist/react.min.js](/react@15.3.1/dist/react.min.js) - * [https://unpkg.com/react-dom@15.3.1/dist/react-dom.min.js](/react-dom@15.3.1/dist/react-dom.min.js) - * [https://unpkg.com/history@4.2.0/umd/history.min.js](/history@4.2.0/umd/history.min.js) +Using a fixed version: -You may also use a [tag](https://docs.npmjs.com/cli/dist-tag) or [version range](https://docs.npmjs.com/misc/semver) instead of a fixed version number, or omit the version/tag entirely to use the `latest` tag. + * [unpkg.com/react@15.3.1/dist/react.min.js](/react@15.3.1/dist/react.min.js) + * [unpkg.com/react-dom@15.3.1/dist/react-dom.min.js](/react-dom@15.3.1/dist/react-dom.min.js) - * [https://unpkg.com/react@^0.14/dist/react.min.js](/react@^0.14/dist/react.min.js) - * [https://unpkg.com/react/dist/react.min.js](/react/dist/react.min.js) +You may also use a [semver range](https://docs.npmjs.com/misc/semver) or a [tag](https://docs.npmjs.com/cli/dist-tag) instead of a fixed version number, or omit the version/tag entirely to use the `latest` tag. -If you omit the file path, unpkg will try to serve [the `browser` bundle](https://github.com/defunctzombie/package-browser-field-spec) if present, the [`main` module](https://docs.npmjs.com/files/package.json#main) otherwise. + * [unpkg.com/react@^15/dist/react.min.js](/react@^15/dist/react.min.js) + * [unpkg.com/react/dist/react.min.js](/react/dist/react.min.js) - * [https://unpkg.com/jquery](/jquery) - * [https://unpkg.com/angular-formly](/angular-formly) - * [https://unpkg.com/three](/three) +If you omit the file path, unpkg will serve the package's "main" file. + + * [unpkg.com/jquery](/jquery) + * [unpkg.com/three](/three) Append a `/` at the end of a URL to view a listing of all the files in a package. - * [https://unpkg.com/lodash/](/lodash/) - * [https://unpkg.com/modernizr/](/modernizr/) - * [https://unpkg.com/react/](/react/) + * [unpkg.com/react/](/react/) + * [unpkg.com/lodash/](/lodash/) ### Query Parameters - - - - - - - - - - - - - - - - - - - - - - - - - -
NameDefault ValueDescription
`main``unpkg`, `browser`, `main`The name of the field in [package.json](https://docs.npmjs.com/files/package.json) to use as the main entry point when there is no file path in the URL
`meta`Return metadata about any file in a package as JSON (e.g. `/any/file?meta`)
`expand`Expands all ["bare" `import` specifiers](https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier) in JavaScript modules to unpkg URLs. This feature is *very experimental*
+
+
`?main=:mainField`
+
The name of the field in [package.json](https://docs.npmjs.com/files/package.json) to use as the main entry point when there is no file path in the URL. Defaults to using `unpkg`, [`browser`](https://github.com/defunctzombie/package-browser-field-spec), and then [`main`](https://docs.npmjs.com/files/package.json#main).
-### Suggested Workflow +
`?meta`
+
Return metadata about any file in a package as JSON (e.g. `/any/file?meta`)
+ +
`?module`
+
Expands all ["bare" `import` specifiers](https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier) in JavaScript modules to unpkg URLs. This feature is *very experimental*
+
+ +### Workflow For npm package authors, unpkg relieves the burden of publishing your code to a CDN in addition to the npm registry. All you need to do is include your [UMD](https://github.com/umdjs/umd) build in your npm package (not your repo, that's different!). diff --git a/client/Layout.js b/client/Layout.js index 8861d8d..e72c06f 100644 --- a/client/Layout.js +++ b/client/Layout.js @@ -1,9 +1,11 @@ import React from 'react' -import ReactDOM from 'react-dom' import PropTypes from 'prop-types' import { Motion, spring } from 'react-motion' -import { withRouter, Link } from 'react-router-dom' +import { Switch, Route, Link, withRouter } from 'react-router-dom' import WindowSize from './WindowSize' +import About from './About' +import Stats from './Stats' +import Home from './Home' class Layout extends React.Component { static propTypes = { @@ -14,24 +16,25 @@ class Layout extends React.Component { state = { underlineLeft: 0, underlineWidth: 0, - useSpring: false + useSpring: false, + stats: null } adjustUnderline = (useSpring = false) => { let itemIndex switch (this.props.location.pathname) { - case '/about': - itemIndex = 2 - break case '/stats': itemIndex = 1 break + case '/about': + itemIndex = 2 + break case '/': default: itemIndex = 0 } - const itemNodes = ReactDOM.findDOMNode(this).querySelectorAll('.underlist > li') + const itemNodes = this.listNode.querySelectorAll('li') const currentNode = itemNodes[itemIndex] this.setState({ @@ -43,6 +46,10 @@ class Layout extends React.Component { componentDidMount() { this.adjustUnderline() + + fetch('/_stats/last-month') + .then(res => res.json()) + .then(stats => this.setState({ stats })) } componentDidUpdate(prevProps) { @@ -63,34 +70,36 @@ class Layout extends React.Component {
-

unpkg

-
- {this.props.children} -
-
-

© 2016-{(new Date()).getFullYear()} Michael Jackson

-
-
+ + + }/> + + +
) } diff --git a/client/NumberUtils.js b/client/NumberUtils.js deleted file mode 100644 index 6fb0514..0000000 --- a/client/NumberUtils.js +++ /dev/null @@ -1,15 +0,0 @@ -export const formatNumber = (n) => { - const digits = String(n).split('') - const groups = [] - - while (digits.length) - groups.unshift(digits.splice(-3).join('')) - - return groups.join(',') -} - -export const parseNumber = (s) => - parseInt(s.replace(/,/g, ''), 10) || 0 - -export const formatPercent = (n, fixed = 1) => - String((n.toPrecision(2) * 100).toFixed(fixed)) diff --git a/client/ReactTrainingLogo.png b/client/ReactTrainingLogo.png deleted file mode 100644 index dc180e4..0000000 Binary files a/client/ReactTrainingLogo.png and /dev/null differ diff --git a/client/Stats.js b/client/Stats.js index 9f30cf0..347131f 100644 --- a/client/Stats.js +++ b/client/Stats.js @@ -3,115 +3,125 @@ import PropTypes from 'prop-types' import formatBytes from 'pretty-bytes' import formatDate from 'date-fns/format' import parseDate from 'date-fns/parse' -import { formatNumber, formatPercent } from './NumberUtils' -import { ContinentsIndex, CountriesIndex, getCountriesByContinent } from './CountryUtils' -import NumberTextInput from './NumberTextInput' +import formatNumber from './utils/formatNumber' +import formatPercent from './utils/formatPercent' -const getSum = (data, countries) => - countries.reduce((n, country) => n + (data[country] || 0), 0) +import { continents, countries } from 'countries-list' -const addValues = (a, b) => { - for (const p in b) { - if (p in a) { - a[p] += b[p] - } else { - a[p] = b[p] - } - } -} +const getCountriesByContinent = (continent) => + Object.keys(countries).filter(country => countries[country].continent === continent) + +const sumKeyValues = (hash, keys) => + keys.reduce((n, key) => n + (hash[key] || 0), 0) + +const sumValues = (hash) => + Object.keys(hash).reduce((memo, key) => memo + hash[key], 0) class Stats extends React.Component { static propTypes = { - serverData: PropTypes.object - } - - static defaultProps = { - serverData: window.serverData + data: PropTypes.object } state = { - minRequests: 5000000 - } - - updateMinRequests = (value) => { - this.setState({ minRequests: value }) + minPackageRequests: 100000, + minCountryRequests: 1000000 } render() { - const { minRequests } = this.state - const stats = this.props.serverData.cloudflareStats - const { timeseries, totals } = stats + const { data } = this.props + + if (data == null) + return null + + const totals = data.totals // Summary data - const sinceDate = parseDate(totals.since) - const untilDate = parseDate(totals.until) - const uniqueVisitors = totals.uniques.all + const since = parseDate(totals.since) + const until = parseDate(totals.until) - const totalRequests = totals.requests.all - const cachedRequests = totals.requests.cached - const totalBandwidth = totals.bandwidth.all - const httpStatus = totals.requests.http_status + // Packages + const packageRows = [] - let errorRequests = 0 - for (const status in httpStatus) { - if (httpStatus.hasOwnProperty(status) && status >= 500) - errorRequests += httpStatus[status] - } + Object.keys(totals.requests.package).sort((a, b) => { + return totals.requests.package[b] - totals.requests.package[a] + }).forEach(packageName => { + const requests = totals.requests.package[packageName] + const bandwidth = totals.bandwidth.package[packageName] - // By Region - const regionRows = [] - const requestsByCountry = {} - const bandwidthByCountry = {} - - timeseries.forEach(ts => { - addValues(requestsByCountry, ts.requests.country) - addValues(bandwidthByCountry, ts.bandwidth.country) + if (requests >= this.state.minPackageRequests) { + packageRows.push( + + {packageName} + {formatNumber(requests)} ({formatPercent(requests / totals.requests.all)}%) + {bandwidth + ? {formatBytes(bandwidth)} ({formatPercent(bandwidth / totals.bandwidth.all)}%) + : - + } + + ) + } }) - const byRequestsDescending = (a, b) => - requestsByCountry[b] - requestsByCountry[a] + // Protocols + const protocolRows = Object.keys(totals.requests.protocol).sort((a, b) => { + return totals.requests.protocol[b] - totals.requests.protocol[a] + }).map(protocol => { + const requests = totals.requests.protocol[protocol] - const continentData = Object.keys(ContinentsIndex).reduce((memo, continent) => { - const countries = getCountriesByContinent(continent) + return ( + + {protocol} + {formatNumber(requests)} ({formatPercent(requests / sumValues(totals.requests.protocol))}%) + + ) + }) + + // Regions + const regionRows = [] + + const continentsData = Object.keys(continents).reduce((memo, continent) => { + const localCountries = getCountriesByContinent(continent) memo[continent] = { - countries, - requests: getSum(requestsByCountry, countries), - bandwidth: getSum(bandwidthByCountry, countries) + countries: localCountries, + requests: sumKeyValues(totals.requests.country, localCountries), + bandwidth: sumKeyValues(totals.bandwidth.country, localCountries) } return memo }, {}) - const topContinents = Object.keys(continentData).sort((a, b) => { - return continentData[b].requests - continentData[a].requests + const topContinents = Object.keys(continentsData).sort((a, b) => { + return continentsData[b].requests - continentsData[a].requests }) topContinents.forEach(continent => { - const continentName = ContinentsIndex[continent] - const { countries, requests, bandwidth } = continentData[continent] + const continentName = continents[continent] + const continentData = continentsData[continent] - if (bandwidth !== 0) { + if (continentData.requests > this.state.minCountryRequests && continentData.bandwidth !== 0) { regionRows.push( {continentName} - {formatNumber(requests)} ({formatPercent(requests / totalRequests)}%) - {formatBytes(bandwidth)} ({formatPercent(bandwidth / totalBandwidth)}%) + {formatNumber(continentData.requests)} ({formatPercent(continentData.requests / totals.requests.all)}%) + {formatBytes(continentData.bandwidth)} ({formatPercent(continentData.bandwidth / totals.bandwidth.all)}%) ) - const topCountries = countries.sort(byRequestsDescending) + const topCountries = continentData.countries.sort((a, b) => { + return totals.requests.country[b] - totals.requests.country[a] + }) topCountries.forEach(country => { - const countryRequests = requestsByCountry[country] - const countryBandwidth = bandwidthByCountry[country] + const countryRequests = totals.requests.country[country] + const countryBandwidth = totals.bandwidth.country[country] - if (countryRequests > minRequests) { + if (countryRequests > this.state.minCountryRequests) { regionRows.push( - {CountriesIndex[country].name} - {formatNumber(countryRequests)} ({formatPercent(countryRequests / totalRequests)}%) - {formatBytes(countryBandwidth)} ({formatPercent(countryBandwidth / totalBandwidth)}%) + {countries[country].name} + {formatNumber(countryRequests)} ({formatPercent(countryRequests / totals.requests.all)}%) + {formatBytes(countryBandwidth)} ({formatPercent(countryBandwidth / totals.bandwidth.all)}%) ) } @@ -121,16 +131,67 @@ class Stats extends React.Component { return (
-

From {formatDate(sinceDate, 'MMM D')} to {formatDate(untilDate, 'MMM D')}, unpkg served {formatNumber(totalRequests)} requests to {formatNumber(uniqueVisitors)} unique visitors, {formatPercent(cachedRequests / totalRequests, 0)}% of which came from the cache (CDN). Over the same period, {formatPercent(errorRequests / totalRequests, 4)}% of requests resulted in server error (returned an HTTP status ≥ 500).

+

From {formatDate(since, 'MMM D')} to {formatDate(until, 'MMM D')} unpkg served {formatNumber(totals.requests.all)} requests and a total of {formatBytes(totals.bandwidth.all)} of data to {formatNumber(totals.uniques.all)} unique visitors, {formatPercent(totals.requests.cached / totals.requests.all, 0)}% of which were served from the cache.

-

By Region

+

Packages

- +

Include only packages that received at least requests. +

- + + + + + + + {packageRows} + +
NamePackageRequests (% of total)Bandwidth (% of total)
+ +

Protocols

+ + + + + + + + + + {protocolRows} + +
ProtocolRequests (% of total)
+ +

Regions

+ +

Include only countries that made at least requests. +

+ + + + + diff --git a/client/WindowSize.js b/client/WindowSize.js index 7279396..9d5db5c 100644 --- a/client/WindowSize.js +++ b/client/WindowSize.js @@ -1,6 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' -import { addEvent, removeEvent } from './DOMUtils' +import addEvent from './utils/addEvent' +import removeEvent from './utils/removeEvent' const ResizeEvent = 'resize' diff --git a/client/index.css b/client/index.css index c8bf606..c8c6b9d 100644 --- a/client/index.css +++ b/client/index.css @@ -11,9 +11,9 @@ body { padding: 5px 20px; } -@media all and (min-width: 660px) { +@media (min-width: 800px) { body { - padding: 50px 20px; + padding: 40px 20px 120px; } } @@ -24,94 +24,103 @@ a:visited { color: rebeccapurple; } -code { - background: #eee; +h1 { + font-size: 2em; +} +h2 { + font-size: 1.8em; +} +h3 { + font-size: 1.6em; +} + +ul { + padding-left: 25px; +} + +dd { + margin-left: 25px; } table { - border-color: black; - border-style: solid; - border-width: 0 0 1px 1px; + border: 1px solid black; + border: 0; } -table th, table td { +th { text-align: left; + background-color: #eee; +} +th, td { + padding: 5px; +} +th { + vertical-align: bottom; +} +td { vertical-align: top; - padding: 5px 7px; - border-color: black; - border-style: solid; - border-width: 1px 1px 0 0; -} - -.continent-row { - font-weight: bold; -} -.country-name { - padding-left: 2em; -} - -.table-filter { - display: block; - text-align: right; - color: #666; - margin-bottom: 1em; -} -.table-filter input { - font-size: 1em; - text-align: right; - max-width: 100px; - color: #999; } .wrapper { - max-width: 600px; + max-width: 700px; margin: 0 auto; } -h1, h2, h3, header nav { - font-family: Futura, Helvetica, sans-serif; - text-transform: uppercase; -} -h3 { - margin-top: 2em; -} -header h1 { - font-size: 4em; - line-height: 1; - text-align: center; - letter-spacing: 0.1em; -} -header nav { - margin-bottom: 4em; -} -header nav a:link, -header nav a:visited { - color: black; -} - -.underlist { - list-style-type: none; - padding: 0; +.layout-title { margin: 0; + text-transform: uppercase; + text-align: center; + font-size: 5em; +} +.layout-nav { + margin: 0 0 3em; +} +.layout-nav-list { + margin: 0; + padding: 0; display: flex; justify-content: center; } -.underlist li { - margin: 0 5px; - padding: 0 5px; - +.layout-nav-list li { flex-basis: auto; + list-style-type: none; + display: inline-block; + font-size: 1.1em; + margin: 0 10px; } -.underlist a:link { +.layout-nav-list li a:link { text-decoration: none; } -.underlist-underline { +.layout-nav-list li a:link, +.layout-nav-list li a:visited { + color: black; +} +.layout-nav-underline { height: 4px; background-color: black; position: absolute; left: 0; } +.home-example { + text-align: center; + background-color: #eee; + margin: 2em 0; + padding: 5px 0; +} + +.table-filter { + font-size: 0.8em; + text-align: right; +} + +.regions-table .continent-row { + font-weight: bold; +} +.regions-table .country-row td.country-name { + padding-left: 20px; +} + .about-logos { margin: 2em 0; display: flex; @@ -125,10 +134,3 @@ header nav a:visited { .about-logo img { max-width: 60%; } - -footer { - margin-top: 40px; - border-top: 1px solid black; - font-size: 0.8em; - text-align: right; -} diff --git a/client/utils/addEvent.js b/client/utils/addEvent.js new file mode 100644 index 0000000..775b6d8 --- /dev/null +++ b/client/utils/addEvent.js @@ -0,0 +1,9 @@ +const addEvent = (node, type, handler) => { + if (node.addEventListener) { + node.addEventListener(type, handler, false) + } else if (node.attachEvent) { + node.attachEvent('on' + type, handler) + } +} + +export default addEvent diff --git a/client/utils/formatNumber.js b/client/utils/formatNumber.js new file mode 100644 index 0000000..6c69ad3 --- /dev/null +++ b/client/utils/formatNumber.js @@ -0,0 +1,11 @@ +const formatNumber = (n) => { + const digits = String(n).split('') + const groups = [] + + while (digits.length) + groups.unshift(digits.splice(-3).join('')) + + return groups.join(',') +} + +export default formatNumber diff --git a/client/utils/formatPercent.js b/client/utils/formatPercent.js new file mode 100644 index 0000000..2e6f4f0 --- /dev/null +++ b/client/utils/formatPercent.js @@ -0,0 +1,4 @@ +const formatPercent = (n, fixed = 1) => + String((n.toPrecision(2) * 100).toFixed(fixed)) + +export default formatPercent diff --git a/client/utils/parseNumber.js b/client/utils/parseNumber.js new file mode 100644 index 0000000..089bda7 --- /dev/null +++ b/client/utils/parseNumber.js @@ -0,0 +1,4 @@ +const parseNumber = (s) => + parseInt(s.replace(/,/g, ''), 10) || 0 + +export default parseNumber diff --git a/client/utils/removeEvent.js b/client/utils/removeEvent.js new file mode 100644 index 0000000..c2c176f --- /dev/null +++ b/client/utils/removeEvent.js @@ -0,0 +1,9 @@ +const removeEvent = (node, type, handler) => { + if (node.removeEventListener) { + node.removeEventListener(type, handler, false) + } else if (node.detachEvent) { + node.detachEvent('on' + type, handler) + } +} + +export default removeEvent diff --git a/package.json b/package.json index 6f0fd46..1506e15 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "date-fns": "^1.28.1", "express": "^4.15.2", "gunzip-maybe": "^1.4.0", - "http-client": "^4.3.1", "invariant": "^2.2.2", "isomorphic-fetch": "^2.2.1", "mime": "^1.3.6", @@ -35,7 +34,8 @@ "sri-toolbox": "^0.2.0", "tar-fs": "^1.15.2", "throng": "^4.0.0", - "validate-npm-package-name": "^3.0.0" + "validate-npm-package-name": "^3.0.0", + "warning": "^3.0.0" }, "devDependencies": { "autoprefixer": "6.7.2", diff --git a/public/index.html b/public/index.html index 1b86c00..dc463cd 100644 --- a/public/index.html +++ b/public/index.html @@ -3,10 +3,9 @@ - - + + -
Region Requests (% of total) Bandwidth (% of total)