Server render auto-index pages

Also, load the autoIndex bundle on the client so we can use React
instead of using an inline script.
This commit is contained in:
Michael Jackson
2018-07-31 10:07:27 -07:00
parent 168ccf5aac
commit 135da0fdc5
62 changed files with 761 additions and 589 deletions

8
server/client/.babelrc Normal file
View File

@ -0,0 +1,8 @@
{
"presets": ["env", "stage-2", "react"],
"env": {
"production": {
"plugins": ["transform-react-remove-prop-types"]
}
}
}

14
server/client/.eslintrc Normal file
View File

@ -0,0 +1,14 @@
{
"parser": "babel-eslint",
"env": {
"browser": true
},
"extends": [
"eslint:recommended",
"plugin:import/errors",
"plugin:react/recommended"
],
"rules": {
"react/no-children-prop": 0
}
}

56
server/client/MainPage.js Normal file
View File

@ -0,0 +1,56 @@
const React = require("react");
const PropTypes = require("prop-types");
const h = require("./utils/createHTML");
const x = require("./utils/execScript");
function MainPage({ title, description, scripts, styles, data, content }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="description" content={description} />
<meta httpEquiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1"
/>
<meta name="timestamp" content={new Date().toISOString()} />
<link rel="shortcut icon" href="/favicon.ico" />
{styles.map(s => <link key={s} rel="stylesheet" href={s} />)}
{x(
"window.Promise || document.write('\\x3Cscript src=\"/_polyfills/es6-promise.min.js\">\\x3C/script>\\x3Cscript>ES6Promise.polyfill()\\x3C/script>')"
)}
{x(
"window.fetch || document.write('\\x3Cscript src=\"/_polyfills/fetch.min.js\">\\x3C/script>')"
)}
{x(`window.__DATA__ = ${JSON.stringify(data)}`)}
<title>{title}</title>
</head>
<body>
<div id="root" dangerouslySetInnerHTML={h(content)} />
{scripts.map(s => <script key={s} src={s} />)}
</body>
</html>
);
}
MainPage.propTypes = {
title: PropTypes.string,
description: PropTypes.string,
scripts: PropTypes.arrayOf(PropTypes.string),
styles: PropTypes.arrayOf(PropTypes.string),
data: PropTypes.any,
content: PropTypes.string
};
MainPage.defaultProps = {
title: "UNPKG",
description: "The CDN for everything on npm",
scripts: [],
styles: [],
data: {},
content: ""
};
module.exports = MainPage;

View File

@ -0,0 +1,8 @@
body {
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif;
line-height: 1.7;
padding: 0px 10px 5px;
color: #000000;
}

View File

@ -0,0 +1,10 @@
require("./autoIndex.css");
const React = require("react");
const ReactDOM = require("react-dom");
const App = require("./autoIndex/App");
const props = window.__DATA__ || {};
ReactDOM.hydrate(<App {...props} />, document.getElementById("root"));

View File

@ -0,0 +1,23 @@
.app {
max-width: 900px;
margin: 0 auto;
}
.app-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.app-version-selector {
line-height: 2.25em;
float: right;
}
.app-version-selector select {
font-size: 1em;
}
.app-address {
text-align: right;
}

View File

@ -0,0 +1,77 @@
require("./App.css");
const React = require("react");
const DirectoryListing = require("./DirectoryListing");
class App extends React.Component {
static defaultProps = {
availableVersions: []
};
handleChange = event => {
window.location.href = window.location.href.replace(
"@" + this.props.packageVersion,
"@" + event.target.value
);
};
render() {
return (
<div className="app">
<header className="app-header">
<h1>
Index of /{this.props.packageName}@{this.props.packageVersion}
{this.props.filename}
</h1>
<div className="app-version-selector">
Version:{" "}
<select
id="version"
defaultValue={this.props.packageVersion}
onChange={this.handleChange}
>
{this.props.availableVersions.map(v => (
<option key={v} value={v}>
{v}
</option>
))}
</select>
</div>
</header>
<hr />
<DirectoryListing
filename={this.props.filename}
entry={this.props.entry}
entries={this.props.entries}
/>
<hr />
<address className="app-address">
{this.props.packageName}@{this.props.packageVersion}
</address>
</div>
);
}
}
if (process.env.NODE_ENV === "development") {
const PropTypes = require("prop-types");
const entryType = PropTypes.object;
App.propTypes = {
packageName: PropTypes.string.isRequired,
packageVersion: PropTypes.string.isRequired,
availableVersions: PropTypes.arrayOf(PropTypes.string),
filename: PropTypes.string.isRequired,
entry: entryType.isRequired,
entries: PropTypes.objectOf(entryType).isRequired
};
}
module.exports = App;

View File

@ -0,0 +1,17 @@
.directory-listing table {
width: 100%;
border-collapse: collapse;
font: 0.85em Monaco, monospace;
}
.directory-listing tr.even {
background-color: #eee;
}
.directory-listing th {
text-align: left;
}
.directory-listing th,
.directory-listing td {
padding: 0.5em 1em;
}

View File

@ -0,0 +1,126 @@
require("./DirectoryListing.css");
const React = require("react");
const formatBytes = require("pretty-bytes");
const sortBy = require("sort-by");
function getDirname(name) {
return (
name
.split("/")
.slice(0, -1)
.join("/") || "."
);
}
function getMatchingEntries(entry, entries) {
const dirname = entry.name || ".";
return Object.keys(entries)
.filter(name => entry.name !== name && getDirname(name) === dirname)
.map(name => entries[name]);
}
function getRelativeName(base, name) {
return base.length ? name.substr(base.length + 1) : name;
}
function DirectoryListing({ filename, entry, entries }) {
const rows = [];
if (filename !== "/") {
rows.push(
<tr key="..">
<td>
<a title="Parent directory" href="../">
..
</a>
</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
);
}
const matchingEntries = getMatchingEntries(entry, entries);
matchingEntries
.filter(({ type }) => type === "directory")
.sort(sortBy("name"))
.forEach(({ name }) => {
const relName = getRelativeName(entry.name, name);
const href = relName + "/";
rows.push(
<tr key={name}>
<td>
<a title={relName} href={href}>
{href}
</a>
</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
);
});
matchingEntries
.filter(({ type }) => type === "file")
.sort(sortBy("name"))
.forEach(({ name, size, contentType, lastModified }) => {
const relName = getRelativeName(entry.name, name);
rows.push(
<tr key={name}>
<td>
<a title={relName} href={relName}>
{relName}
</a>
</td>
<td>{contentType}</td>
<td>{formatBytes(size)}</td>
<td>{lastModified}</td>
</tr>
);
});
return (
<div className="directory-listing">
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Size</th>
<th>Last Modified</th>
</tr>
</thead>
<tbody>
{rows.map((row, index) =>
React.cloneElement(row, {
className: index % 2 ? "odd" : "even"
})
)}
</tbody>
</table>
</div>
);
}
if (process.env.NODE_ENV === "development") {
const PropTypes = require("prop-types");
const entryType = PropTypes.shape({
name: PropTypes.string.isRequired
});
DirectoryListing.propTypes = {
filename: PropTypes.string.isRequired,
entry: entryType.isRequired,
entries: PropTypes.objectOf(entryType).isRequired
};
}
module.exports = DirectoryListing;

63
server/client/main.css Normal file
View File

@ -0,0 +1,63 @@
body {
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif;
line-height: 1.7;
padding: 5px 20px;
color: #000000;
}
@media (min-width: 800px) {
body {
padding: 40px 20px 120px;
}
}
a:link {
color: blue;
}
a:visited {
color: rebeccapurple;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.8em;
}
h3 {
font-size: 1.6em;
}
ul {
padding-left: 25px;
}
dd {
margin-left: 25px;
}
table {
border: 1px solid black;
border: 0;
}
th {
text-align: left;
background-color: #eee;
}
th,
td {
padding: 5px;
}
th {
vertical-align: bottom;
}
td {
vertical-align: top;
}
.wrapper {
max-width: 700px;
margin: 0 auto;
}

8
server/client/main.js Normal file
View File

@ -0,0 +1,8 @@
require("./main.css");
const React = require("react");
const ReactDOM = require("react-dom");
const App = require("./main/App");
ReactDOM.render(<App />, document.getElementById("root"));

View File

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

View File

@ -0,0 +1,12 @@
require("./About.css");
const React = require("react");
const h = require("../utils/createHTML");
const markup = require("./About.md");
function About() {
return <div className="wrapper" dangerouslySetInnerHTML={h(markup)} />;
}
module.exports = About;

View File

@ -0,0 +1,40 @@
unpkg is an [open source](https://github.com/unpkg) project built and maintained by [Michael Jackson](https://twitter.com/mjackson).
### 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>
### Cache Behavior
The CDN caches 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 maintains a list of packages that are known to be malicious. If you find such a package on npm, please let us know!
### Feedback
If you think this is useful, we'd love to hear from you. Please reach out to [@unpkg](https://twitter.com/unpkg) with any questions or concerns.

14
server/client/main/App.js Normal file
View File

@ -0,0 +1,14 @@
const React = require("react");
const { HashRouter } = require("react-router-dom");
const Layout = require("./Layout");
function App() {
return (
<HashRouter>
<Layout />
</HashRouter>
);
}
module.exports = App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,6 @@
.home-example {
text-align: center;
background-color: #eee;
margin: 2em 0;
padding: 5px 0;
}

View File

@ -0,0 +1,12 @@
require("./Home.css");
const React = require("react");
const h = require("../utils/createHTML");
const markup = require("./Home.md");
function Home() {
return <div className="wrapper" dangerouslySetInnerHTML={h(markup)} />;
}
module.exports = Home;

View File

@ -0,0 +1,48 @@
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:
<div class="home-example">unpkg.com/:package@:version/:file</div>
### Examples
Using a fixed version:
* [unpkg.com/react@16.0.0/umd/react.production.min.js](/react@16.0.0/umd/react.production.min.js)
* [unpkg.com/react-dom@16.0.0/umd/react-dom.production.min.js](/react-dom@16.0.0/umd/react-dom.production.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.
* [unpkg.com/react@^16/umd/react.production.min.js](/react@^16/umd/react.production.min.js)
* [unpkg.com/react/umd/react.production.min.js](/react/umd/react.production.min.js)
If you omit the file path (i.e. use a "bare" URL), unpkg will serve the file specified by the `unpkg` field in `package.json`, or fall back to `main`.
* [unpkg.com/d3](/d3)
* [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.
* [unpkg.com/react/](/react/)
* [unpkg.com/lodash/](/lodash/)
### Query Parameters
<dl>
<dt>`?meta`</dt>
<dd>Return metadata about any file in a package as JSON (e.g. `/any/file?meta`)</dd>
<dt>`?module`</dt>
<dd>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*</dd>
</dl>
### 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.

View File

@ -0,0 +1,38 @@
.layout-title {
margin: 0;
text-transform: uppercase;
text-align: center;
font-size: 5em;
}
.layout-nav {
margin: 0 0 3em;
}
.layout-navList {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
}
.layout-navList li {
flex-basis: auto;
list-style-type: none;
display: inline-block;
font-size: 1.1em;
margin: 0 10px;
}
.layout-navList li a:link {
text-decoration: none;
}
.layout-navList li a:link,
.layout-navList li a:visited {
color: black;
}
.layout-navUnderline {
height: 4px;
background-color: black;
position: absolute;
left: 0;
}

View File

@ -0,0 +1,135 @@
require("./Layout.css");
const React = require("react");
const PropTypes = require("prop-types");
const { Switch, Route, Link, withRouter } = require("react-router-dom");
const { Motion, spring } = require("react-motion");
const WindowSize = require("./WindowSize");
const About = require("./About");
const Stats = require("./Stats");
const Home = require("./Home");
class Layout extends React.Component {
static propTypes = {
location: PropTypes.object,
children: PropTypes.node
};
state = {
underlineLeft: 0,
underlineWidth: 0,
useSpring: false,
stats: null
};
adjustUnderline = (useSpring = false) => {
let itemIndex;
switch (this.props.location.pathname) {
case "/stats":
itemIndex = 1;
break;
case "/about":
itemIndex = 2;
break;
case "/":
default:
itemIndex = 0;
}
const itemNodes = this.listNode.querySelectorAll("li");
const currentNode = itemNodes[itemIndex];
this.setState({
underlineLeft: currentNode.offsetLeft,
underlineWidth: currentNode.offsetWidth,
useSpring
});
};
componentDidMount() {
this.adjustUnderline();
fetch("/_stats?period=last-month")
.then(res => res.json())
.then(stats => this.setState({ stats }));
if (window.localStorage) {
const savedStats = window.localStorage.savedStats;
if (savedStats) this.setState({ stats: JSON.parse(savedStats) });
window.onbeforeunload = () => {
localStorage.savedStats = JSON.stringify(this.state.stats);
};
}
}
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 className="layout">
<WindowSize onChange={this.adjustUnderline} />
<div className="wrapper">
<header>
<h1 className="layout-title">unpkg</h1>
<nav className="layout-nav">
<ol
className="layout-navList"
ref={node => (this.listNode = node)}
>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/stats">Stats</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
</ol>
<Motion
defaultStyle={{ left: underlineLeft, width: underlineWidth }}
style={style}
children={style => (
<div
className="layout-navUnderline"
style={{
WebkitTransform: `translate3d(${style.left}px,0,0)`,
transform: `translate3d(${style.left}px,0,0)`,
width: style.width
}}
/>
)}
/>
</nav>
</header>
</div>
<Switch>
<Route
path="/stats"
render={() => <Stats data={this.state.stats} />}
/>
<Route path="/about" component={About} />
<Route path="/" component={Home} />
</Switch>
</div>
);
}
}
module.exports = withRouter(Layout);

View File

@ -0,0 +1,8 @@
.table-filter {
font-size: 0.8em;
text-align: right;
}
.regions-table .country-row td.country-name {
padding-left: 20px;
}

323
server/client/main/Stats.js Normal file
View File

@ -0,0 +1,323 @@
require("./Stats.css");
const React = require("react");
const PropTypes = require("prop-types");
const formatBytes = require("pretty-bytes");
const formatDate = require("date-fns/format");
const parseDate = require("date-fns/parse");
const { continents, countries } = require("countries-list");
const formatNumber = require("../utils/formatNumber");
const formatPercent = require("../utils/formatPercent");
function getCountriesByContinent(continent) {
return Object.keys(countries).filter(
country => countries[country].continent === continent
);
}
function sumKeyValues(hash, keys) {
return keys.reduce((n, key) => n + (hash[key] || 0), 0);
}
function sumValues(hash) {
return Object.keys(hash).reduce((memo, key) => memo + hash[key], 0);
}
class Stats extends React.Component {
static propTypes = {
data: PropTypes.object
};
state = {
minPackageRequests: 1000000,
minCountryRequests: 1000000
};
render() {
const { data } = this.props;
if (data == null) return null;
const totals = data.totals;
// Summary data
const since = parseDate(totals.since);
const until = parseDate(totals.until);
// Packages
const packageRows = [];
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];
if (requests >= this.state.minPackageRequests) {
packageRows.push(
<tr key={packageName}>
<td>
<a
href={`https://npmjs.org/package/${packageName}`}
title={`${packageName} on npm`}
>
{packageName}
</a>
</td>
<td>
{formatNumber(requests)} ({formatPercent(
requests / totals.requests.all
)}%)
</td>
{bandwidth ? (
<td>
{formatBytes(bandwidth)} ({formatPercent(
bandwidth / totals.bandwidth.all
)}%)
</td>
) : (
<td>-</td>
)}
</tr>
);
}
});
// Regions
const regionRows = [];
const continentsData = Object.keys(continents).reduce((memo, continent) => {
const localCountries = getCountriesByContinent(continent);
memo[continent] = {
countries: localCountries,
requests: sumKeyValues(totals.requests.country, localCountries),
bandwidth: sumKeyValues(totals.bandwidth.country, localCountries)
};
return memo;
}, {});
const topContinents = Object.keys(continentsData).sort((a, b) => {
return continentsData[b].requests - continentsData[a].requests;
});
topContinents.forEach(continent => {
const continentName = continents[continent];
const continentData = continentsData[continent];
if (
continentData.requests > this.state.minCountryRequests &&
continentData.bandwidth !== 0
) {
regionRows.push(
<tr key={continent} className="continent-row">
<td>
<strong>{continentName}</strong>
</td>
<td>
<strong>
{formatNumber(continentData.requests)} ({formatPercent(
continentData.requests / totals.requests.all
)}%)
</strong>
</td>
<td>
<strong>
{formatBytes(continentData.bandwidth)} ({formatPercent(
continentData.bandwidth / totals.bandwidth.all
)}%)
</strong>
</td>
</tr>
);
const topCountries = continentData.countries.sort((a, b) => {
return totals.requests.country[b] - totals.requests.country[a];
});
topCountries.forEach(country => {
const countryRequests = totals.requests.country[country];
const countryBandwidth = totals.bandwidth.country[country];
if (countryRequests > this.state.minCountryRequests) {
regionRows.push(
<tr key={continent + country} className="country-row">
<td className="country-name">{countries[country].name}</td>
<td>
{formatNumber(countryRequests)} ({formatPercent(
countryRequests / totals.requests.all
)}%)
</td>
<td>
{formatBytes(countryBandwidth)} ({formatPercent(
countryBandwidth / totals.bandwidth.all
)}%)
</td>
</tr>
);
}
});
}
});
// 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];
return (
<tr key={protocol}>
<td>{protocol}</td>
<td>
{formatNumber(requests)} ({formatPercent(
requests / sumValues(totals.requests.protocol)
)}%)
</td>
</tr>
);
});
return (
<div className="wrapper">
<p>
From <strong>{formatDate(since, "MMM D")}</strong> to{" "}
<strong>{formatDate(until, "MMM D")}</strong> unpkg served{" "}
<strong>{formatNumber(totals.requests.all)}</strong> requests and a
total of <strong>{formatBytes(totals.bandwidth.all)}</strong> of data
to <strong>{formatNumber(totals.uniques.all)}</strong> unique
visitors,{" "}
<strong>
{formatPercent(totals.requests.cached / totals.requests.all, 0)}%
</strong>{" "}
of which were served from the cache.
</p>
<h3>Packages</h3>
<p>
The table below shows the most popular packages served by unpkg from{" "}
<strong>{formatDate(since, "MMM D")}</strong> to{" "}
<strong>{formatDate(until, "MMM D")}</strong>. Only the top{" "}
{Object.keys(totals.requests.package).length} packages are shown.
</p>
<p className="table-filter">
Include only packages that received at least{" "}
<select
value={this.state.minPackageRequests}
onChange={event =>
this.setState({
minPackageRequests: parseInt(event.target.value, 10)
})
}
>
<option value="0">0</option>
<option value="1000">1,000</option>
<option value="10000">10,000</option>
<option value="100000">100,000</option>
<option value="1000000">1,000,000</option>
<option value="10000000">10,000,000</option>
</select>{" "}
requests.
</p>
<table cellSpacing="0" cellPadding="0" style={{ width: "100%" }}>
<thead>
<tr>
<th>
<strong>Package</strong>
</th>
<th>
<strong>Requests (% of total)</strong>
</th>
<th>
<strong>Bandwidth (% of total)</strong>
</th>
</tr>
</thead>
<tbody>{packageRows}</tbody>
</table>
<h3>Regions</h3>
<p>
The table below breaks down requests to unpkg from{" "}
<strong>{formatDate(since, "MMM D")}</strong> to{" "}
<strong>{formatDate(until, "MMM D")}</strong> by geographic region.
</p>
<p className="table-filter">
Include only countries that made at least{" "}
<select
value={this.state.minCountryRequests}
onChange={event =>
this.setState({
minCountryRequests: parseInt(event.target.value, 10)
})
}
>
<option value="0">0</option>
<option value="100000">100,000</option>
<option value="1000000">1,000,000</option>
<option value="10000000">10,000,000</option>
<option value="100000000">100,000,000</option>
</select>{" "}
requests.
</p>
<table
cellSpacing="0"
cellPadding="0"
style={{ width: "100%" }}
className="regions-table"
>
<thead>
<tr>
<th>
<strong>Region</strong>
</th>
<th>
<strong>Requests (% of total)</strong>
</th>
<th>
<strong>Bandwidth (% of total)</strong>
</th>
</tr>
</thead>
<tbody>{regionRows}</tbody>
</table>
<h3>Protocols</h3>
<p>
The table below breaks down requests to unpkg from{" "}
<strong>{formatDate(since, "MMM D")}</strong> to{" "}
<strong>{formatDate(until, "MMM D")}</strong> by HTTP protocol.
</p>
<table cellSpacing="0" cellPadding="0" style={{ width: "100%" }}>
<thead>
<tr>
<th>
<strong>Protocol</strong>
</th>
<th>
<strong>Requests (% of total)</strong>
</th>
</tr>
</thead>
<tbody>{protocolRows}</tbody>
</table>
</div>
);
}
}
module.exports = Stats;

View File

@ -0,0 +1,35 @@
const React = require("react");
const PropTypes = require("prop-types");
const addEvent = require("../utils/addEvent");
const removeEvent = require("../utils/removeEvent");
const resizeEvent = "resize";
class WindowSize extends React.Component {
static propTypes = {
onChange: PropTypes.func
};
handleWindowResize = () => {
if (this.props.onChange)
this.props.onChange({
width: window.innerWidth,
height: window.innerHeight
});
};
componentDidMount() {
addEvent(window, resizeEvent, this.handleWindowResize);
}
componentWillUnmount() {
removeEvent(window, resizeEvent, this.handleWindowResize);
}
render() {
return null;
}
}
module.exports = WindowSize;

View File

@ -0,0 +1,9 @@
function addEvent(node, type, handler) {
if (node.addEventListener) {
node.addEventListener(type, handler, false);
} else if (node.attachEvent) {
node.attachEvent("on" + type, handler);
}
}
module.exports = addEvent;

View File

@ -0,0 +1,5 @@
function createHTML(code) {
return { __html: code };
}
module.exports = createHTML;

View File

@ -0,0 +1,9 @@
const React = require("react");
const h = require("./createHTML");
function execScript(code) {
return <script dangerouslySetInnerHTML={h(code)} />;
}
module.exports = execScript;

View File

@ -0,0 +1,12 @@
function formatNumber(n) {
const digits = String(n).split("");
const groups = [];
while (digits.length) {
groups.unshift(digits.splice(-3).join(""));
}
return groups.join(",");
}
module.exports = formatNumber;

View File

@ -0,0 +1,5 @@
function formatPercent(n, fixed = 1) {
return String((n.toPrecision(2) * 100).toFixed(fixed));
}
module.exports = formatPercent;

View File

@ -0,0 +1,5 @@
function parseNumber(s) {
return parseInt(s.replace(/,/g, ""), 10) || 0;
}
module.exports = parseNumber;

View File

@ -0,0 +1,9 @@
function removeEvent(node, type, handler) {
if (node.removeEventListener) {
node.removeEventListener(type, handler, false);
} else if (node.detachEvent) {
node.detachEvent("on" + type, handler);
}
}
module.exports = removeEvent;