Rewrite frontend using create-react-app
This commit is contained in:
@ -1,10 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[ "es2015", { "loose": true } ],
|
||||
"stage-1",
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-assign"
|
||||
]
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
module.exports = [
|
||||
'goodjsproject',
|
||||
'thisoneisevil'
|
||||
]
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 9.1 KiB |
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
@ -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))
|
Binary file not shown.
Before Width: | Height: | Size: 27 KiB |
@ -1,9 +0,0 @@
|
||||
import React from 'react'
|
||||
import contentHTML from './About.md'
|
||||
|
||||
class About extends React.Component {
|
||||
render = () =>
|
||||
<div className="wrapper" dangerouslySetInnerHTML={{ __html: contentHTML }}/>
|
||||
}
|
||||
|
||||
export default About
|
@ -1,50 +0,0 @@
|
||||
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).
|
||||
|
||||
<div class="about-logos">
|
||||
<div class="about-logo">
|
||||
<a href="https://reacttraining.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@reacttraining.com) if interested.
|
||||
|
||||
### Sponsors
|
||||
|
||||
The fast, global infrastructure that powers unpkg is generously donated by [Cloudflare](https://www.cloudflare.com) and [Heroku](https://www.heroku.com).
|
||||
|
||||
<div class="about-logos">
|
||||
<div class="about-logo">
|
||||
<a href="https://www.cloudflare.com"><img src="../CloudflareLogo.png"></a>
|
||||
</div>
|
||||
<div class="about-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 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.
|
||||
|
||||
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.
|
||||
|
||||
### Support
|
||||
|
||||
unpkg 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 unpkg 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.
|
||||
|
||||
unpkg is not affiliated with or supported by npm, Inc. in any way. Please do not contact npm for help with unpkg.
|
||||
|
||||
### Abuse
|
||||
|
||||
unpkg blacklists some packages to prevent abuse. If you find a malicious package on npm, please take a moment to add it to [our blacklist](https://github.com/mjackson/unpkg/blob/master/modules/PackageBlacklist.js)!
|
||||
|
||||
### 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.
|
@ -1,12 +0,0 @@
|
||||
import React from 'react'
|
||||
import contentHTML from './Home.md'
|
||||
|
||||
class Home extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="wrapper" dangerouslySetInnerHTML={{ __html: contentHTML }}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Home
|
@ -1,62 +0,0 @@
|
||||
unpkg 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:
|
||||
|
||||
<div style="text-align:center">`https://unpkg.com/package@version/file`</div>
|
||||
|
||||
A few 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)
|
||||
|
||||
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://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)
|
||||
|
||||
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.
|
||||
|
||||
* [https://unpkg.com/jquery](/jquery)
|
||||
* [https://unpkg.com/angular-formly](/angular-formly)
|
||||
* [https://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/)
|
||||
|
||||
### 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>`unpkg`, `browser`, `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.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`json`</td>
|
||||
<td>`undefined`</td>
|
||||
<td>Return a recursive list of metadata about all the files in a directory as JSON (e.g. `/any/path/?json`). Note: this only works for directories.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Suggested 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!).
|
||||
|
||||
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 unpkg as well.
|
@ -1,89 +0,0 @@
|
||||
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 {
|
||||
static propTypes = {
|
||||
location: PropTypes.object,
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
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>unpkg</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
|
@ -1,41 +0,0 @@
|
||||
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,
|
||||
onChange: 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 { parseNumber, formatNumber, ...props } = this.props // eslint-disable-line no-unused-vars
|
||||
const displayValue = formatNumber(value)
|
||||
|
||||
return (
|
||||
<input {...props} type="text" value={displayValue} onChange={this.handleChange}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default NumberTextInput
|
@ -1,51 +0,0 @@
|
||||
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
|
@ -1,145 +0,0 @@
|
||||
import React, { PropTypes } from 'react'
|
||||
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 propTypes = {
|
||||
stats: PropTypes.object
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
stats: window.cloudFlareStats
|
||||
}
|
||||
|
||||
state = {
|
||||
minRequests: 5000000
|
||||
}
|
||||
|
||||
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 continentName = ContinentsIndex[continent]
|
||||
const { countries, requests, bandwidth } = continentData[continent]
|
||||
|
||||
if (bandwidth !== 0) {
|
||||
regionRows.push(
|
||||
<tr key={continent} className="continent-row">
|
||||
<td>{continentName}</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>, unpkg 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 ≥ 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
|
@ -1,26 +0,0 @@
|
||||
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
|
@ -1,5 +0,0 @@
|
||||
import createHistory from 'history/lib/createHashHistory'
|
||||
|
||||
const history = createHistory()
|
||||
|
||||
export default history
|
@ -1,10 +0,0 @@
|
||||
import React from 'react'
|
||||
import { render } from 'react-dom'
|
||||
import Router from './components/Router'
|
||||
|
||||
import './styles.css'
|
||||
|
||||
render(
|
||||
<Router/>,
|
||||
document.getElementById('app')
|
||||
)
|
@ -1,127 +0,0 @@
|
||||
body {
|
||||
font-size: 16px;
|
||||
font-family: -apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
Helvetica,
|
||||
Arial,
|
||||
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;
|
||||
}
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
.about-logos {
|
||||
margin: 2em 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.about-logo {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
max-width: 80%;
|
||||
}
|
||||
.about-logo img {
|
||||
max-width: 60%;
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
require('babel-register')({
|
||||
only: __dirname
|
||||
})
|
@ -1,147 +0,0 @@
|
||||
const fs = require('fs')
|
||||
const invariant = require('invariant')
|
||||
const webpack = require('webpack')
|
||||
|
||||
const createBundle = (webpackStats) => {
|
||||
const { publicPath, assetsByChunkName } = webpackStats
|
||||
|
||||
const createURL = (asset) =>
|
||||
publicPath + asset
|
||||
|
||||
const getAssets = (chunks = [ 'main' ]) =>
|
||||
(Array.isArray(chunks) ? chunks : [ chunks ]).reduce((memo, chunk) => (
|
||||
memo.concat(assetsByChunkName[chunk] || [])
|
||||
), [])
|
||||
|
||||
const getScriptAssets = (...args) =>
|
||||
getAssets(...args)
|
||||
.filter(asset => (/\.js$/).test(asset))
|
||||
.map(createURL)
|
||||
|
||||
const getStyleAssets = (...args) =>
|
||||
getAssets(...args)
|
||||
.filter(asset => (/\.css$/).test(asset))
|
||||
.map(createURL)
|
||||
|
||||
return {
|
||||
createURL,
|
||||
getAssets,
|
||||
getScriptAssets,
|
||||
getStyleAssets
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An express middleware that sets req.manifest from the build manifest
|
||||
* in the given file. Should be used in production to get consistent hashes.
|
||||
*/
|
||||
const assetsManifest = (webpackManifestFile) => {
|
||||
let manifest
|
||||
try {
|
||||
manifest = JSON.parse(fs.readFileSync(webpackManifestFile, 'utf8'))
|
||||
} catch (error) {
|
||||
invariant(
|
||||
false,
|
||||
'assetsManifest middleware cannot read the manifest file "%s"; ' +
|
||||
'do `npm run build` before starting the server',
|
||||
webpackManifestFile
|
||||
)
|
||||
}
|
||||
|
||||
return (req, res, next) => {
|
||||
req.manifest = manifest
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An express middleware that sets req.bundle from the build
|
||||
* info in the given stats file. Should be used in production.
|
||||
*/
|
||||
const staticAssets = (webpackStatsFile) => {
|
||||
let stats
|
||||
try {
|
||||
stats = JSON.parse(fs.readFileSync(webpackStatsFile, 'utf8'))
|
||||
} catch (error) {
|
||||
invariant(
|
||||
false,
|
||||
'staticAssets middleware cannot read the build stats in %s; ' +
|
||||
'do `npm run build` before starting the server',
|
||||
webpackStatsFile
|
||||
)
|
||||
}
|
||||
|
||||
const bundle = createBundle(stats)
|
||||
|
||||
return (req, res, next) => {
|
||||
req.bundle = bundle
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An express middleware that sets req.bundle from the
|
||||
* latest result from a running webpack compiler (i.e. using
|
||||
* webpack-dev-middleware). Should only be used in dev.
|
||||
*/
|
||||
const devAssets = (webpackCompiler) => {
|
||||
let bundle
|
||||
webpackCompiler.plugin('done', (stats) => {
|
||||
bundle = createBundle(stats.toJson())
|
||||
})
|
||||
|
||||
return (req, res, next) => {
|
||||
invariant(
|
||||
bundle != null,
|
||||
'devAssets middleware needs a running compiler; ' +
|
||||
'use webpack-dev-middleware in front of devAssets'
|
||||
)
|
||||
|
||||
req.bundle = bundle
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a webpack compiler that automatically inlines the
|
||||
* webpack dev runtime in all entry points.
|
||||
*/
|
||||
const createDevCompiler = (webpackConfig, webpackRuntimeModuleID) =>
|
||||
webpack({
|
||||
...webpackConfig,
|
||||
entry: prependModuleID(
|
||||
webpackConfig.entry,
|
||||
webpackRuntimeModuleID
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Returns a modified copy of the given webpackEntry object with
|
||||
* the moduleID in front of all other assets.
|
||||
*/
|
||||
const prependModuleID = (webpackEntry, moduleID) => {
|
||||
if (typeof webpackEntry === 'string')
|
||||
return [ moduleID, webpackEntry ]
|
||||
|
||||
if (Array.isArray(webpackEntry))
|
||||
return [ moduleID, ...webpackEntry ]
|
||||
|
||||
if (webpackEntry && typeof webpackEntry === 'object') {
|
||||
const entry = { ...webpackEntry }
|
||||
|
||||
for (const chunkName in entry)
|
||||
if (entry.hasOwnProperty(chunkName))
|
||||
entry[chunkName] = prependModuleID(entry[chunkName], moduleID)
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
throw new Error('Invalid webpack entry object')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
assetsManifest,
|
||||
staticAssets,
|
||||
devAssets,
|
||||
createDevCompiler
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
require('isomorphic-fetch')
|
||||
const { createStack, createFetch, header, base, query, parseJSON, onResponse } = require('http-client')
|
||||
const invariant = require('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()
|
||||
)
|
||||
|
||||
const getZones = (domainName) =>
|
||||
createFetch(
|
||||
commonStack,
|
||||
createNameQuery(domainName)
|
||||
)('/zones')
|
||||
|
||||
const getZoneAnalyticsDashboard = (zone, since, until) =>
|
||||
createFetch(
|
||||
commonStack,
|
||||
createRangeQuery(since, until)
|
||||
)(`/zones/${zone.id}/analytics/dashboard`)
|
||||
|
||||
const getAnalyticsDashboards = (domainNames, since, until) =>
|
||||
Promise.all(
|
||||
domainNames.map(domainName => getZones(domainName))
|
||||
).then(
|
||||
domainZones => domainZones.reduce((memo, zones) => memo.concat(zones))
|
||||
).then(
|
||||
zones => Promise.all(zones.map(zone => getZoneAnalyticsDashboard(zone, since, until)))
|
||||
).then(
|
||||
results => results.reduce(reduceResults)
|
||||
)
|
||||
|
||||
const reduceResults = (target, results) => {
|
||||
Object.keys(results).forEach(key => {
|
||||
const value = results[key]
|
||||
|
||||
if (typeof value === 'object' && value) {
|
||||
target[key] = reduceResults(target[key] || {}, value)
|
||||
} else if (typeof value === 'number') {
|
||||
target[key] = (target[key] || 0) + results[key]
|
||||
}
|
||||
})
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getZones,
|
||||
getZoneAnalyticsDashboard,
|
||||
getAnalyticsDashboards
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,47 +0,0 @@
|
||||
const React = require('react')
|
||||
const { renderToStaticMarkup } = require('react-dom/server')
|
||||
const { getAnalyticsDashboards } = require('./Cloudflare')
|
||||
const HomePage = require('./components/HomePage')
|
||||
|
||||
const OneMinute = 1000 * 60
|
||||
const ThirtyDays = OneMinute * 60 * 24 * 30
|
||||
const DOCTYPE = '<!DOCTYPE html>'
|
||||
|
||||
const fetchStats = (callback) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
callback(null, require('./CloudflareStats.json'))
|
||||
} else {
|
||||
const since = new Date(Date.now() - ThirtyDays)
|
||||
const until = new Date(Date.now() - OneMinute)
|
||||
|
||||
getAnalyticsDashboards([ 'npmcdn.com', 'unpkg.com' ], since, until)
|
||||
.then(result => callback(null, result), callback)
|
||||
}
|
||||
}
|
||||
|
||||
const sendHomePage = (req, res, next) => {
|
||||
const chunks = [ 'vendor', 'home' ]
|
||||
const props = {
|
||||
styles: req.bundle.getStyleAssets(chunks),
|
||||
scripts: req.bundle.getScriptAssets(chunks)
|
||||
}
|
||||
|
||||
if (req.manifest)
|
||||
props.webpackManifest = req.manifest
|
||||
|
||||
fetchStats((error, stats) => {
|
||||
if (error) {
|
||||
next(error)
|
||||
} else {
|
||||
res.set('Cache-Control', 'public, max-age=60')
|
||||
|
||||
res.send(
|
||||
DOCTYPE + renderToStaticMarkup(<HomePage {...props} stats={stats}/>)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendHomePage
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
const path = require('path')
|
||||
|
||||
exports.id = 1
|
||||
exports.port = parseInt(process.env.PORT, 10) || 5000
|
||||
exports.webpackConfig = require('../../webpack.config')
|
||||
exports.statsFile = path.resolve(__dirname, '../../stats.json')
|
||||
exports.publicDir = path.resolve(__dirname, '../../public')
|
||||
exports.manifestFile = path.resolve(exports.publicDir, '__assets__/chunk-manifest.json')
|
||||
exports.timeout = parseInt(process.env.TIMEOUT, 10) || 20000
|
||||
exports.maxAge = process.env.MAX_AGE || '365d'
|
||||
|
||||
exports.registryURL = process.env.REGISTRY_URL || 'https://registry.npmjs.org'
|
||||
exports.bowerBundle = process.env.BOWER_BUNDLE || '/bower.zip'
|
||||
exports.redirectTTL = process.env.REDIRECT_TTL || 500
|
||||
exports.autoIndex = !process.env.DISABLE_INDEX
|
||||
exports.redisURL = process.env.REDIS_URL
|
||||
|
||||
exports.blacklist = require('../PackageBlacklist')
|
@ -1,31 +0,0 @@
|
||||
const redis = require('redis')
|
||||
const onFinished = require('on-finished')
|
||||
|
||||
const URLFormat = /^\/((?:@[^\/@]+\/)?[^\/@]+)(?:@([^\/]+))?(\/.*)?$/
|
||||
|
||||
const logStats = (redisURL) => {
|
||||
const redisClient = redis.createClient(redisURL)
|
||||
|
||||
return (req, res, next) => {
|
||||
onFinished(res, () => {
|
||||
const path = req.path
|
||||
|
||||
if (res.statusCode === 200 && path.charAt(path.length - 1) !== '/') {
|
||||
//redisClient.zincrby([ 'request-paths', 1, path ])
|
||||
|
||||
const match = URLFormat.exec(path)
|
||||
|
||||
if (match) {
|
||||
const packageName = match[1]
|
||||
redisClient.zincrby([ 'package-requests', 1, packageName ])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
logStats
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
const React = require('react')
|
||||
|
||||
const PropTypes = React.PropTypes
|
||||
|
||||
class HomePage extends React.Component {
|
||||
static propTypes = {
|
||||
webpackManifest: PropTypes.object,
|
||||
styles: PropTypes.arrayOf(PropTypes.string),
|
||||
scripts: PropTypes.arrayOf(PropTypes.string),
|
||||
stats: PropTypes.object
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
webpackManifest: {},
|
||||
styles: [],
|
||||
scripts: [],
|
||||
stats: {}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { webpackManifest, styles, scripts, stats } = this.props
|
||||
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<meta charSet="utf-8"/>
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
|
||||
<meta name="description" content="A fast, global content delivery network for stuff that is published to npm"/>
|
||||
<meta name="viewport" content="width=700,maximum-scale=1"/>
|
||||
<meta name="timestamp" content={(new Date).toISOString()}/>
|
||||
<link rel="icon" href="/favicon.ico?v3"/>
|
||||
<title>unpkg</title>
|
||||
<script dangerouslySetInnerHTML={{ __html: "window.Promise || document.write('\\x3Cscript src=\"/es6-promise.min.js\">\\x3C/script>\\x3Cscript>ES6Promise.polyfill()\\x3C/script>')" }}/>
|
||||
<script dangerouslySetInnerHTML={{ __html: "window.fetch || document.write('\\x3Cscript src=\"/fetch.min.js\">\\x3C/script>')" }}/>
|
||||
<script dangerouslySetInnerHTML={{ __html: "window.webpackManifest = " + JSON.stringify(webpackManifest) }}/>
|
||||
<script dangerouslySetInnerHTML={{ __html: "window.cloudFlareStats = " + JSON.stringify(stats) }}/>
|
||||
{styles.map(s => <link rel="stylesheet" key={s} href={s}/>)}
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"/>
|
||||
{scripts.map(s => <script key={s} src={s}/>)}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HomePage
|
@ -1,136 +0,0 @@
|
||||
/*eslint-disable no-console*/
|
||||
const http = require('http')
|
||||
const cors = require('cors')
|
||||
const throng = require('throng')
|
||||
const morgan = require('morgan')
|
||||
const express = require('express')
|
||||
const devErrorHandler = require('errorhandler')
|
||||
const WebpackDevServer = require('webpack-dev-server')
|
||||
const { createRequestHandler } = require('express-unpkg')
|
||||
const DefaultServerConfig = require('./ServerConfig')
|
||||
const { assetsManifest, staticAssets, devAssets, createDevCompiler } = require('./AssetsUtils')
|
||||
const { sendHomePage } = require('./MainController')
|
||||
const { logStats } = require('./StatsUtils')
|
||||
|
||||
const createRouter = (config = {}) => {
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/', sendHomePage)
|
||||
|
||||
if (config.redisURL)
|
||||
router.use(logStats(config.redisURL))
|
||||
|
||||
router.use(createRequestHandler(config))
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
const errorHandler = (err, req, res, next) => {
|
||||
res.status(500).send('<p>Internal Server Error</p>')
|
||||
console.error(err.stack)
|
||||
next(err)
|
||||
}
|
||||
|
||||
const createServer = (config) => {
|
||||
const app = express()
|
||||
|
||||
app.disable('x-powered-by')
|
||||
|
||||
app.use(errorHandler)
|
||||
app.use(cors())
|
||||
app.use(express.static(config.publicDir, { maxAge: config.maxAge }))
|
||||
app.use(assetsManifest(config.manifestFile))
|
||||
app.use(staticAssets(config.statsFile))
|
||||
app.use(createRouter(config))
|
||||
|
||||
const server = http.createServer(app)
|
||||
|
||||
// Heroku dynos automatically timeout after 30s. Set our
|
||||
// own timeout here to force sockets to close before that.
|
||||
// https://devcenter.heroku.com/articles/request-timeout
|
||||
if (config.timeout) {
|
||||
server.setTimeout(config.timeout, (socket) => {
|
||||
const message = `Timeout of ${config.timeout}ms exceeded`
|
||||
|
||||
socket.end([
|
||||
`HTTP/1.1 503 Service Unavailable`,
|
||||
`Date: ${(new Date).toGMTString()}`,
|
||||
`Content-Type: text/plain`,
|
||||
`Content-Length: ${message.length}`,
|
||||
`Connection: close`,
|
||||
``,
|
||||
message
|
||||
].join(`\r\n`))
|
||||
})
|
||||
}
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
const createDevServer = (config) => {
|
||||
const webpackConfig = config.webpackConfig
|
||||
const compiler = createDevCompiler(
|
||||
webpackConfig,
|
||||
`webpack-dev-server/client?http://localhost:${config.port}`
|
||||
)
|
||||
|
||||
const server = new WebpackDevServer(compiler, {
|
||||
// webpack-dev-middleware options.
|
||||
publicPath: webpackConfig.output.publicPath,
|
||||
quiet: false,
|
||||
noInfo: false,
|
||||
stats: {
|
||||
// https://webpack.github.io/docs/node.js-api.html#stats-tojson
|
||||
assets: true,
|
||||
colors: true,
|
||||
version: true,
|
||||
hash: true,
|
||||
timings: true,
|
||||
chunks: false
|
||||
},
|
||||
|
||||
// webpack-dev-server options.
|
||||
contentBase: false,
|
||||
setup(app) {
|
||||
// This runs before webpack-dev-middleware.
|
||||
app.disable('x-powered-by')
|
||||
app.use(morgan('dev'))
|
||||
}
|
||||
})
|
||||
|
||||
// This runs after webpack-dev-middleware.
|
||||
server.use(devErrorHandler())
|
||||
server.use(express.static(config.publicDir))
|
||||
server.use(devAssets(compiler))
|
||||
server.use(createRouter(config))
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
const startServer = (serverConfig) => {
|
||||
const config = {
|
||||
...DefaultServerConfig,
|
||||
...serverConfig
|
||||
}
|
||||
|
||||
const server = process.env.NODE_ENV === 'production'
|
||||
? createServer(config)
|
||||
: createDevServer(config)
|
||||
|
||||
server.listen(config.port, () => {
|
||||
console.log('Server #%s listening on port %s, Ctrl+C to stop', config.id, config.port)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createServer,
|
||||
createDevServer,
|
||||
startServer
|
||||
}
|
||||
|
||||
if (require.main === module)
|
||||
throng({
|
||||
start: (id) => startServer({ id }),
|
||||
workers: process.env.WEB_CONCURRENCY || 1,
|
||||
lifetime: Infinity
|
||||
})
|
Reference in New Issue
Block a user