Bunch of website stuff

This commit is contained in:
Michael Jackson 2016-05-20 11:58:58 -07:00
parent 7e29e21483
commit 87524d4947
25 changed files with 784 additions and 162 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,13 @@
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
}

View File

@ -0,0 +1,15 @@
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)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,15 @@
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)
export const formatPercent = (n, fixed = 1) =>
String((n.toPrecision(2) * 100).toFixed(fixed))

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,9 @@
import React from 'react'
import contentHTML from './About.md'
class About extends React.Component {
render = () =>
<div className="wrapper" dangerouslySetInnerHTML={{ __html: contentHTML }}/>
}
export default About

View File

@ -0,0 +1,46 @@
npmcdn is an [open source](https://github.com/mjackson/npmcdn) 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://reactjs-training.com).
<div class="sponsor-logos">
<div class="sponsor-logo">
<a href="https://reactjs-training.com"><img src="../ReactTrainingLogo.png"></a>
</div>
</div>
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@reactjs-training.com) if interested.
### 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.
URLs that do not specify a package version number redirect to one that does. This is the `latest` version when no version is specified, or the `maxSatisfying` version when a [semver version](https://github.com/npm/node-semver) is given. Redirects are cached for 5 minutes.
Browsers are instructed (via the `Cache-Control` header) to cache assets for 4 hours.
### Sponsors
The fast, global infrastructure that powers npmcdn is graciously provided by [CloudFlare](https://www.cloudflare.com) and [Heroku](https://www.heroku.com).
<div class="sponsor-logos">
<div class="sponsor-logo">
<a href="https://www.cloudflare.com"><img src="../CloudFlareLogo.png"></a>
</div>
<div class="sponsor-logo">
<a href="https://www.heroku.com"><img src="../HerokuLogo.png"></a>
</div>
</div>
These sponsors provide some of the most robust, reliable infrastructure available today and I'm happy to be able to partner with them on npmcdn.
### Support
npmcdn is a free, best-effort service and cannot provide any uptime or support guarantees.
I do my best to keep it running, but sometimes things go wrong. Sometimes there are network or provider issues outside my control. Sometimes abusive traffic temporarily affects response times. Sometimes I break things by doing something dumb, but I try not to.
The goal of npmcdn is to provide a hassle-free CDN for npm package authors. It's also a great resource for people creating demos and instructional material. However, if you rely on it to serve files that are crucial to your business, you should probably pay for a host with well-supported infrastructure and uptime guarantees.
npmcdn is not affiliated with or supported by npm, Inc. in any way. Please do not contact npm for help with npmcdn.
### Feedback
If you think this is useful, I'd love to hear from you. Please reach out to [@mjackson](https://twitter.com/mjackson) with any questions/concerns.

View File

@ -1,36 +0,0 @@
body {
font: 14px Helvetica, sans-serif;
line-height: 1.5;
padding: 5px 20px;
}
@media all and (min-width: 660px) {
body {
padding: 50px 20px;
}
}
header, #wrapper {
max-width: 600px;
margin: 0 auto;
}
header {
text-align: center;
}
code {
background: #eee;
}
table {
border-color: black;
border-style: solid;
border-width: 0 0 1px 1px;
}
table th, table td {
text-align: left;
vertical-align: top;
padding: 5px 7px;
border-color: black;
border-style: solid;
border-width: 1px 1px 0 0;
}

View File

@ -1,86 +1,9 @@
import React from 'react'
import './Home.css'
import contentHTML from './Home.md'
const Home = React.createClass({
render() {
return (
<div>
<header>
<h1>npmcdn</h1>
</header>
<section id="wrapper">
<p>npmcdn is a CDN for packages that are published via <a href="https://www.npmjs.com/">npm</a>. Use it to quickly and easily load files using a simple URL like <code>https://npmcdn.com/package@version/path/to/file</code>.</p>
<p>A few examples:</p>
<ul>
<li><a href="/react@15.0.1/dist/react.min.js">https://npmcdn.com/react@15.0.1/dist/react.min.js</a></li>
<li><a href="/react-dom@15.0.1/dist/react-dom.min.js">https://npmcdn.com/react-dom@15.0.1/dist/react-dom.min.js</a></li>
<li><a href="/history@1.12.5/umd/History.min.js">https://npmcdn.com/history@1.12.5/umd/History.min.js</a></li>
<li><a href="/react-router@1.0.0/umd/ReactRouter.min.js">https://npmcdn.com/react-router@1.0.0/umd/ReactRouter.min.js</a></li>
</ul>
<p>You may also use a <a href="https://docs.npmjs.com/cli/dist-tag">tag</a> or <a href="https://docs.npmjs.com/misc/semver">version range</a> instead of a fixed version number, or omit the version/tag entirely to use the <code>latest</code> tag.</p>
<ul>
<li><a href="/react@^0.14/dist/react.min.js">https://npmcdn.com/react@^0.14/dist/react.min.js</a></li>
<li><a href="/react/dist/react.min.js">https://npmcdn.com/react/dist/react.min.js</a></li>
</ul>
<p>If you omit the file path, the <a href="https://docs.npmjs.com/files/package.json#main">main module</a> will be served. This is especially useful for loading libaries that publish a UMD build as their main module.</p>
<ul>
<li><a href="/three">https://npmcdn.com/three</a></li>
<li><a href="/jquery">https://npmcdn.com/jquery</a></li>
<li><a href="/angular-formly">https://npmcdn.com/angular-formly</a></li>
</ul>
<p>Append a <code>/</code> at the end of a URL to view a listing of all the files in a package.</p>
<ul>
<li><a href="/lodash/">https://npmcdn.com/lodash/</a></li>
<li><a href="/modernizr/">https://npmcdn.com/modernizr/</a></li>
<li><a href="/react/">https://npmcdn.com/react/</a></li>
</ul>
<p>You may use the special <code>/bower.zip</code> file path in packages that contain a <code>bower.json</code> file to dynamically generate a zip file that Bower can use to install the package.</p>
<ul>
<li><a href="/react-swap/bower.zip">https://npmcdn.com/react-swap/bower.zip</a></li>
<li><a href="/react-collapse@1.6.3/bower.zip">https://npmcdn.com/react-collapse@1.6.3/bower.zip</a></li>
</ul>
<p><strong>Please note: <em>We do NOT recommend JavaScript libraries use Bower.</em></strong> Bower places additional burdens on JavaScript package authors for little to no gain. npmcdn is intended to make it easier to publish code, not harder, so Bower support will be removed in January 2017. Please move to npm for installing packages and stop using Bower before that time. See <a href="https://github.com/mjackson/npm-http-server#bower-support">here</a> for our rationale.</p>
<h3>Query Parameters</h3>
<table cellPadding="0" cellSpacing="0">
<thead>
<tr>
<th width="80px">Name</th>
<th width="120px">Default Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>main</code></td>
<td><code>main</code></td>
<td>The name of the field in <a href="https://docs.npmjs.com/files/package.json">package.json</a> to use as the main entry point when there is no file path in the URL (e.g. <code>?main=browser</code>).</td>
</tr>
</tbody>
</table>
<h3>Suggested Workflow</h3>
<p>For npm package authors, npmcdn relieves the burden of publishing your code to a CDN in addition to the npm registry. All you need to do is include your <a href="https://github.com/umdjs/umd">UMD</a> build in your npm package (not your repo, that's different!).</p>
<p>You can do this easily using the following setup:</p>
<ul>
<li>Add the <code>umd</code> (or <code>dist</code>) directory to your <code>.gitignore</code> file</li>
<li>Add the <code>umd</code> directory to your <a href="https://docs.npmjs.com/files/package.json#files">files array</a> in <code>package.json</code></li>
<li>Use a build script to generate your UMD build in the <code>umd</code> directory when you publish</li>
</ul>
<p>That's it! Now when you <code>npm publish</code> you'll have a version available on npmcdn as well.</p>
<h3>Feedback</h3>
<p>If you think this is useful, I'd love to hear from you. Please reach out to <a href="https://twitter.com/mjackson">@mjackson</a> with any questions/concerns.</p>
<p>Also, please feel free to examine the source on <a href="https://github.com/mjackson/npmcdn.com">GitHub</a>.</p>
</section>
</div>
)
}
})
class Home extends React.Component {
render = () =>
<div className="wrapper" dangerouslySetInnerHTML={{ __html: contentHTML }}/>
}
export default Home

View File

@ -0,0 +1,64 @@
npmcdn is a fast, global [content-delivery network](https://en.wikipedia.org/wiki/Content_delivery_network) for stuff that is published to [npm](https://www.npmjs.com/). Use it to quickly and easily load files using a simple URL like `https://npmcdn.com/package@version/file`.
A few examples:
* [https://npmcdn.com/react@15.0.1/dist/react.min.js](/react@15.0.1/dist/react.min.js)
* [https://npmcdn.com/react-dom@15.0.1/dist/react-dom.min.js](/react-dom@15.0.1/dist/react-dom.min.js)
* [https://npmcdn.com/history@1.12.5/umd/History.min.js](/history@1.12.5/umd/History.min.js)
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.
* [https://npmcdn.com/react@^0.14/dist/react.min.js](/react@^0.14/dist/react.min.js)
* [https://npmcdn.com/react/dist/react.min.js](/react/dist/react.min.js)
If you omit the file path, the [main module](https://docs.npmjs.com/files/package.json#main) will be served. This is especially useful for loading libaries that publish a UMD build as their main module.
* [https://npmcdn.com/jquery](/jquery)
* [https://npmcdn.com/angular-formly](/angular-formly)
* [https://npmcdn.com/three](/three)
Append a `/` at the end of a URL to view a listing of all the files in a package.
* [https://npmcdn.com/lodash/](/lodash/)
* [https://npmcdn.com/modernizr/](/modernizr/)
* [https://npmcdn.com/react/](/react/)
You may use the special `/bower.zip` file path in packages that contain a `bower.json` file to dynamically generate a zip file that Bower can use to install the package.
* [https://npmcdn.com/react-swap/bower.zip](/react-swap/bower.zip)
* [https://npmcdn.com/react-collapse@1.6.3/bower.zip](/react-collapse@1.6.3/bower.zip)
**_We do NOT recommend JavaScript libraries use Bower._** Bower places additional burdens on JavaScript package authors for little to no gain. npmcdn is intended to make it easier to publish code, not harder, so Bower support will be removed in January 2017\. Please move to npm for installing packages and stop using Bower before that time. See [here](https://github.com/mjackson/npm-http-server#bower-support) for our rationale.
### Query Parameters
<table cellpadding="0" cellspacing="0">
<thead>
<tr>
<th width="80px">Name</th>
<th width="120px">Default Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>`main`</td>
<td>`main`</td>
<td>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 (e.g. `?main=browser`).</td>
</tr>
</tbody>
</table>
**All other query parameters are invalid** and will result in a `403 Forbidden` response. This helps preserve the integrity of the cache for URLs with valid query strings.
### Suggested Workflow
For npm package authors, npmcdn 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!).
You can do this easily using the following setup:
* Add the `umd` (or `dist`) directory to your `.gitignore` file
* Add the `umd` directory to your [files array](https://docs.npmjs.com/files/package.json#files) in `package.json`
* Use a build script to generate your UMD build in the `umd` directory when you publish
That's it! Now when you `npm publish` you'll have a version available on npmcdn as well.

View File

@ -0,0 +1,84 @@
import React, { PropTypes } from 'react'
import { Motion, spring } from 'react-motion'
import { findDOMNode } from 'react-dom'
import Window from './Window'
class Layout extends React.Component {
state = {
underlineLeft: 0,
underlineWidth: 0,
useSpring: false
}
adjustUnderline = (useSpring = false) => {
let itemIndex
switch (this.props.location.pathname) {
case '/about':
itemIndex = 2
break
case '/stats':
itemIndex = 1
break
case '/':
default:
itemIndex = 0
}
const itemNodes = findDOMNode(this).querySelectorAll('.underlist > li')
const currentNode = itemNodes[itemIndex]
this.setState({
underlineLeft: currentNode.offsetLeft,
underlineWidth: currentNode.offsetWidth,
useSpring
})
}
componentDidMount = () =>
this.adjustUnderline()
componentDidUpdate = (prevProps) => {
if (prevProps.location.pathname !== this.props.location.pathname)
this.adjustUnderline(true)
}
render = () => {
const { underlineLeft, underlineWidth, useSpring } = this.state
const style = {
left: useSpring ? spring(underlineLeft, { stiffness: 220 }) : underlineLeft,
width: useSpring ? spring(underlineWidth) : underlineWidth
}
return (
<div>
<Window onResize={this.adjustUnderline}/>
<header>
<h1>npmcdn</h1>
<nav>
<ol className="underlist">
<li><a href="#/">Home</a></li>
<li><a href="#/stats">Stats</a></li>
<li><a href="#/about">About</a></li>
</ol>
<Motion defaultStyle={{ left: underlineLeft, width: underlineWidth }} style={style}>
{s => (
<div
className="underlist-underline"
style={{
WebkitTransform: `translate3d(${s.left}px,0,0)`,
transform: `translate3d(${s.left}px,0,0)`,
width: s.width
}}
/>
)}
</Motion>
</nav>
</header>
{this.props.children}
</div>
)
}
}
export default Layout

View File

@ -0,0 +1,39 @@
import React, { PropTypes } from 'react'
import { parseNumber, formatNumber } from '../NumberUtils'
class NumberTextInput extends React.Component {
static propTypes = {
value: PropTypes.number,
parseNumber: PropTypes.func,
formatNumber: PropTypes.func
}
static defaultProps = {
value: 0,
parseNumber,
formatNumber
}
componentWillMount = () =>
this.setState({ value: this.props.value })
handleChange = (event) => {
const value = this.props.parseNumber(event.target.value)
this.setState({ value }, () => {
if (this.props.onChange)
this.props.onChange(value)
})
}
render = () => {
const { value } = this.state
const displayValue = this.props.formatNumber(value)
return (
<input {...this.props} type="text" value={displayValue} onChange={this.handleChange}/>
)
}
}
export default NumberTextInput

View File

@ -0,0 +1,51 @@
import React from 'react'
import history from '../history'
import Layout from './Layout'
import About from './About'
import Stats from './Stats'
import Home from './Home'
const findMatchingComponents = (location) => {
let components
switch (location.pathname) {
case '/about':
components = [ Layout, About ]
break
case '/stats':
components = [ Layout, Stats ]
break
case '/':
default:
components = [ Layout, Home ]
}
return components
}
const renderNestedComponents = (components, props) =>
components.reduceRight(
(children, component) => React.createElement(component, { ...props, children }),
undefined
)
class Router extends React.Component {
state = {
location: history.getCurrentLocation()
}
componentDidMount = () =>
this.unlisten = history.listen(location => {
this.setState({ location })
})
componentWillUnmount = () =>
this.unlisten()
render = () => {
const { location } = this.state
const components = findMatchingComponents(location)
return renderNestedComponents(components, { location })
}
}
export default Router

View File

@ -0,0 +1,142 @@
import React, { PropTypes } from 'react'
import { findDOMNode } from 'react-dom'
import formatBytes from 'byte-size'
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'
const getSum = (data, countries) =>
countries.reduce((n, country) => n + (data[country] || 0), 0)
const addValues = (a, b) => {
for (const p in b) {
if (p in a) {
a[p] += b[p]
} else {
a[p] = b[p]
}
}
}
class Stats extends React.Component {
static defaultProps = {
stats: window.NPMCDN_STATS
}
state = {
minRequests: 1000000
}
updateMinRequests = (value) =>
this.setState({ minRequests: value })
render = () => {
const { minRequests } = this.state
const { stats } = this.props
const { timeseries, totals } = stats
// Summary data
const sinceDate = parseDate(totals.since)
const untilDate = parseDate(totals.until)
const uniqueVisitors = totals.uniques.all
const totalRequests = totals.requests.all
const cachedRequests = totals.requests.cached
const totalBandwidth = totals.bandwidth.all
const httpStatus = totals.requests.http_status
let errorRequests = 0
for (const status in httpStatus) {
if (httpStatus.hasOwnProperty(status) && status >= 500)
errorRequests += httpStatus[status]
}
// By Region
const regionRows = []
const requestsByCountry = {}
const bandwidthByCountry = {}
timeseries.forEach(ts => {
addValues(requestsByCountry, ts.requests.country)
addValues(bandwidthByCountry, ts.bandwidth.country)
})
const byRequestsDescending = (a, b) =>
requestsByCountry[b] - requestsByCountry[a]
const continentData = Object.keys(ContinentsIndex).reduce((memo, continent) => {
const countries = getCountriesByContinent(continent)
memo[continent] = {
countries,
requests: getSum(requestsByCountry, countries),
bandwidth: getSum(bandwidthByCountry, countries)
}
return memo
}, {})
const topContinents = Object.keys(continentData).sort((a, b) => {
return continentData[b].requests - continentData[a].requests
})
topContinents.forEach(continent => {
const name = ContinentsIndex[continent]
const { countries, requests, bandwidth } = continentData[continent]
if (bandwidth !== 0) {
regionRows.push(
<tr key={continent} className="continent-row">
<td>{ContinentsIndex[continent]}</td>
<td>{formatNumber(requests)} ({formatPercent(requests / totalRequests)}%)</td>
<td>{formatBytes(bandwidth)} ({formatPercent(bandwidth / totalBandwidth)}%)</td>
</tr>
)
const topCountries = countries.sort(byRequestsDescending)
topCountries.forEach(country => {
const countryRequests = requestsByCountry[country]
const countryBandwidth = bandwidthByCountry[country]
if (countryRequests > minRequests) {
regionRows.push(
<tr key={continent + country} className="country-row">
<td className="country-name">{CountriesIndex[country].name}</td>
<td>{formatNumber(countryRequests)} ({formatPercent(countryRequests / totalRequests)}%)</td>
<td>{formatBytes(countryBandwidth)} ({formatPercent(countryBandwidth / totalBandwidth)}%)</td>
</tr>
)
}
})
}
})
return (
<div className="wrapper">
<p>From <strong>{formatDate(sinceDate, 'MMM D')}</strong> to <strong>{formatDate(untilDate, 'MMM D')}</strong>, npmcdn served <strong>{formatNumber(totalRequests)}</strong> requests to <strong>{formatNumber(uniqueVisitors)}</strong> unique visitors, <strong>{formatPercent(cachedRequests / totalRequests, 0)}%</strong> of which came from the cache (CDN). Over the same period, <strong>{formatPercent(errorRequests / totalRequests, 4)}%</strong> of requests resulted in server error (returned an HTTP status &ge; 500).</p>
<h3>By Region</h3>
<label className="table-filter">Include countries that made at least <NumberTextInput value={minRequests} onChange={this.updateMinRequests}/> requests.</label>
<table cellSpacing="0" cellPadding="0" style={{ width: '100%' }}>
<thead>
<tr>
<th>Name</th>
<th>Requests (% of total)</th>
<th>Bandwidth (% of total)</th>
</tr>
</thead>
<tbody>
{regionRows}
</tbody>
</table>
</div>
)
}
}
export default Stats

View File

@ -0,0 +1,26 @@
import React, { PropTypes } from 'react'
import { addEvent, removeEvent } from '../DOMUtils'
const ResizeEvent = 'resize'
class Window extends React.Component {
static propTypes = {
onResize: PropTypes.func
}
handleWindowResize = () => {
if (this.props.onResize)
this.props.onResize()
}
componentDidMount = () =>
addEvent(window, ResizeEvent, this.handleWindowResize)
componentWillUnmount = () =>
removeEvent(window, ResizeEvent, this.handleWindowResize)
render = () =>
null
}
export default Window

View File

@ -0,0 +1,5 @@
import createHistory from 'history/lib/createHashHistory'
const history = createHistory()
export default history

View File

@ -1,8 +1,10 @@
import React from 'react'
import { render } from 'react-dom'
import Home from './components/Home'
import Router from './components/Router'
import './styles.css'
render(
<Home/>,
<Router/>,
document.getElementById('app')
)

131
modules/client/styles.css Normal file
View File

@ -0,0 +1,131 @@
body {
font: 16px Helvetica, sans-serif;
line-height: 1.5;
padding: 5px 20px;
}
@media all and (min-width: 660px) {
body {
padding: 50px 20px;
}
}
a:link {
color: blue;
}
a:visited {
color: rebeccapurple;
}
code {
background: #eee;
}
table {
border-color: black;
border-style: solid;
border-width: 0 0 1px 1px;
}
table th, table td {
text-align: left;
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;
}
header, .wrapper {
max-width: 600px;
margin: 0 auto;
}
h1, h2, h3, header nav {
font-family: Futura, Helvetica, sans-serif;
text-transform: uppercase;
}
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;
margin: 0;
display: flex;
justify-content: center;
}
.underlist li {
margin: 0 5px;
padding: 0 5px;
flex-basis: auto;
}
.underlist a:link {
text-decoration: none;
}
.underlist-underline {
height: 4px;
background-color: black;
position: absolute;
left: 0;
}
.sponsor-logos {
margin: 2em 0;
display: flex;
justify-content: center;
}
.sponsor-logo {
text-align: center;
flex: 1;
max-width: 80%;
}
.sponsor-logo img {
max-width: 60%;
}
.bar-graph {
height: 200px;
}
.bar-graph svg {
background: #eee;
}
.y-axis line,
.x-axis line {
stroke: black;
stroke-width: 1px;
}

View File

@ -2,30 +2,32 @@ import fs from 'fs'
import invariant from 'invariant'
import webpack from 'webpack'
const createAssets = (webpackStats) => {
const createBundle = (webpackStats) => {
const { publicPath, assetsByChunkName } = webpackStats
const createURL = (asset) =>
webpackStats.publicPath + asset
publicPath + asset
const getAssets = (chunkName = 'main') => {
const assets = webpackStats.assetsByChunkName[chunkName] || []
return Array.isArray(assets) ? assets : [ assets ]
}
const getAssets = (chunks = [ 'main' ]) =>
(Array.isArray(chunks) ? chunks : [ chunks ]).reduce((memo, chunk) => (
memo.concat(assetsByChunkName[chunk] || [])
), [])
const getScriptURLs = (chunkName = 'main') =>
getAssets(chunkName)
const getScriptAssets = (...args) =>
getAssets(...args)
.filter(asset => (/\.js$/).test(asset))
.map(createURL)
const getStyleURLs = (chunkName = 'main') =>
getAssets(chunkName)
const getStyleAssets = (...args) =>
getAssets(...args)
.filter(asset => (/\.css$/).test(asset))
.map(createURL)
return {
createURL,
getAssets,
getScriptURLs,
getStyleURLs
getScriptAssets,
getStyleAssets
}
}
@ -46,7 +48,7 @@ export const staticAssets = (webpackStatsFile) => {
)
}
const assets = createAssets(stats)
const assets = createBundle(stats)
return (req, res, next) => {
req.assets = assets
@ -62,7 +64,7 @@ export const staticAssets = (webpackStatsFile) => {
export const assetsCompiler = (webpackCompiler) => {
let assets
webpackCompiler.plugin('done', (stats) => {
assets = createAssets(stats.toJson())
assets = createBundle(stats.toJson())
})
return (req, res, next) => {

View File

@ -0,0 +1,50 @@
import 'isomorphic-fetch'
import { createStack, createFetch, header, base, query, parseJSON, onResponse } from 'http-client'
import invariant from 'invariant'
const CloudFlareKey = process.env.CLOUDFLARE_KEY
const CloudFlareEmail = process.env.CLOUDFLARE_EMAIL
invariant(
CloudFlareKey,
'Missing $CLOUDFLARE_KEY environment variable'
)
invariant(
CloudFlareEmail,
'Missing $CLOUDFLARE_EMAIL environment variable'
)
const createRangeQuery = (since, until) =>
query({
since: since.toISOString(),
until: until.toISOString()
})
const createNameQuery = (name) =>
query({ name })
const getResult = () =>
createStack(
parseJSON(),
onResponse(response => response.jsonData.result)
)
const commonStack = createStack(
header('X-Auth-Key', CloudFlareKey),
header('X-Auth-Email', CloudFlareEmail),
base('https://api.cloudflare.com/client/v4'),
getResult()
)
export const getZones = (domainName) =>
createFetch(
commonStack,
createNameQuery(domainName)
)('/zones')
export const getZoneAnalyticsDashboard = (zone, since, until) =>
createFetch(
commonStack,
createRangeQuery(since, until)
)(`/zones/${zone.id}/analytics/dashboard`)

View File

@ -1,16 +1,43 @@
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { getZones, getZoneAnalyticsDashboard } from './CloudFlare'
import HomePage from './components/HomePage'
const OneMinute = 1000 * 60
const ThirtyDays = OneMinute * 60 * 24 * 30
const DOCTYPE = '<!DOCTYPE html>'
export const sendHomePage = (req, res) => {
const fetchStats = (callback) => {
if (process.env.NODE_ENV === 'development') {
callback(null, require('./stats.json'))
} else {
getZones('npmcdn.com')
.then(zones => {
const zone = zones[0]
const since = new Date(Date.now() - ThirtyDays)
const until = new Date(Date.now() - OneMinute)
return getZoneAnalyticsDashboard(zone, since, until).then(result => {
callback(null, result)
})
})
.catch(callback)
}
}
export const sendHomePage = (req, res, next) => {
const props = {
styles: req.assets.getStyleURLs('home'),
scripts: req.assets.getScriptURLs('home')
styles: req.assets.getStyleAssets([ 'vendor', 'home' ]),
scripts: req.assets.getScriptAssets([ 'vendor', 'home' ])
}
res.send(
DOCTYPE + renderToStaticMarkup(<HomePage {...props}/>)
)
fetchStats((error, stats) => {
if (error) {
next(error)
} else {
res.send(
DOCTYPE + renderToStaticMarkup(<HomePage {...props} stats={stats}/>)
)
}
})
}

View File

@ -1,22 +1,20 @@
import React, { PropTypes } from 'react'
const assetType = PropTypes.string
class HomePage extends React.Component {
static propTypes = {
styles: PropTypes.arrayOf(PropTypes.string),
scripts: PropTypes.arrayOf(PropTypes.string),
stats: PropTypes.object
}
const HomePage = React.createClass({
propTypes: {
styles: PropTypes.arrayOf(assetType),
scripts: PropTypes.arrayOf(assetType)
},
static defaultProps = {
styles: [],
scripts: [],
stats: {}
}
getDefaultProps() {
return {
styles: [],
scripts: []
}
},
render() {
const { styles, scripts } = this.props
render = () => {
const { styles, scripts, stats } = this.props
return (
<html>
@ -26,6 +24,7 @@ const HomePage = React.createClass({
<meta name="timestamp" content={(new Date).toISOString()}/>
<title>npmcdn</title>
{styles.map(style => <link key={style} rel="stylesheet" href={style}/>)}
<script dangerouslySetInnerHTML={{ __html: `window.NPMCDN_STATS=${JSON.stringify(stats)}` }}/>
</head>
<body>
<div id="app"/>
@ -34,6 +33,6 @@ const HomePage = React.createClass({
</html>
)
}
})
}
export default HomePage

View File

@ -5,9 +5,8 @@
"start": "heroku local -f Procfile.local",
"build": "npm run build-assets && npm run build-lib",
"build-assets": "NODE_ENV=production webpack -p --json > stats.json",
"build-lib": "rimraf lib && babel ./modules -d lib",
"build-lib": "rimraf lib && babel ./modules -d lib --copy-files",
"heroku-postbuild": "npm run build"
},
"dependencies": {
"autoprefixer": "^6.3.6",
@ -16,18 +15,29 @@
"babel-preset-es2015": "^6.6.0",
"babel-preset-react": "^6.5.0",
"babel-preset-stage-1": "^6.5.0",
"byte-size": "^2.0.0",
"cors": "^2.7.1",
"countries-list": "^1.1.0",
"css-loader": "^0.23.1",
"date-fns": "^1.0.0",
"errorhandler": "^1.4.3",
"express": "^4.13.4",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.5",
"history": "^3.0.0-2",
"html-loader": "^0.4.3",
"http-client": "^4.0.1",
"invariant": "^2.2.1",
"isomorphic-fetch": "^2.2.1",
"json-loader": "^0.5.4",
"markdown-loader": "^0.1.7",
"morgan": "^1.7.0",
"npm-http-server": "^2.11.0",
"on-finished": "^2.3.0",
"postcss-loader": "^0.9.1",
"react": "^15.0.2",
"react-dom": "^15.0.2",
"react-motion": "^0.4.3",
"redis": "^2.6.0-1",
"rimraf": "^2.5.2",
"style-loader": "^0.13.1",

View File

@ -3,15 +3,16 @@ const webpack = require('webpack')
const autoprefixer = require('autoprefixer')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const AssetURLPrefix = '[hash:8]/'
const outputPrefix = '[chunkhash:8]-'
module.exports = {
entry: {
vendor: [ 'react' ],
home: path.resolve(__dirname, 'modules/client/home.js')
},
output: {
filename: `${AssetURLPrefix}[name].js`,
filename: `${outputPrefix}[name].js`,
path: path.resolve(__dirname, 'public/__assets__'),
publicPath: '/__assets__/'
},
@ -19,13 +20,17 @@ module.exports = {
module: {
loaders: [
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel' },
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style', 'css!postcss') }
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style', 'css!postcss') },
{ test: /\.json$/, loader: 'json' },
{ test: /\.png$/, loader: 'file' },
{ test: /\.md$/, loader: 'html!markdown' }
]
},
plugins: [
new webpack.optimize.OccurrenceOrderPlugin(),
new ExtractTextPlugin(`${AssetURLPrefix}styles.css`),
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor' }),
new ExtractTextPlugin(`${outputPrefix}styles.css`),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
})