Prettier everything up

This commit is contained in:
MICHAEL JACKSON 2017-11-08 08:57:15 -08:00
parent f3e041ace6
commit 2d57d96e62
36 changed files with 785 additions and 577 deletions

View File

@ -1,7 +1,8 @@
import React from 'react' import React from 'react'
import contentHTML from './About.md' import contentHTML from './About.md'
const About = () => const About = () => (
<div className="wrapper" dangerouslySetInnerHTML={{ __html: contentHTML }} /> <div className="wrapper" dangerouslySetInnerHTML={{ __html: contentHTML }} />
)
export default About export default About

View File

@ -2,9 +2,10 @@ import React from 'react'
import { HashRouter } from 'react-router-dom' import { HashRouter } from 'react-router-dom'
import Layout from './Layout' import Layout from './Layout'
const App = () => const App = () => (
<HashRouter> <HashRouter>
<Layout /> <Layout />
</HashRouter> </HashRouter>
)
export default App export default App

View File

@ -1,7 +1,8 @@
import React from 'react' import React from 'react'
import contentHTML from './Home.md' import contentHTML from './Home.md'
const Home = () => const Home = () => (
<div className="wrapper" dangerouslySetInnerHTML={{ __html: contentHTML }} /> <div className="wrapper" dangerouslySetInnerHTML={{ __html: contentHTML }} />
)
export default Home export default Home

View File

@ -54,8 +54,7 @@ class Layout extends React.Component {
if (window.localStorage) { if (window.localStorage) {
const savedStats = window.localStorage.savedStats const savedStats = window.localStorage.savedStats
if (savedStats) if (savedStats) this.setState({ stats: JSON.parse(savedStats) })
this.setState({ stats: JSON.parse(savedStats) })
window.onbeforeunload = () => { window.onbeforeunload = () => {
localStorage.savedStats = JSON.stringify(this.state.stats) localStorage.savedStats = JSON.stringify(this.state.stats)
@ -72,7 +71,9 @@ class Layout extends React.Component {
const { underlineLeft, underlineWidth, useSpring } = this.state const { underlineLeft, underlineWidth, useSpring } = this.state
const style = { const style = {
left: useSpring ? spring(underlineLeft, { stiffness: 220 }) : underlineLeft, left: useSpring
? spring(underlineLeft, { stiffness: 220 })
: underlineLeft,
width: useSpring ? spring(underlineWidth) : underlineWidth width: useSpring ? spring(underlineWidth) : underlineWidth
} }
@ -83,10 +84,19 @@ class Layout extends React.Component {
<header> <header>
<h1 className="layout-title">unpkg</h1> <h1 className="layout-title">unpkg</h1>
<nav className="layout-nav"> <nav className="layout-nav">
<ol className="layout-nav-list" ref={node => this.listNode = node}> <ol
<li><Link to="/">Home</Link></li> className="layout-nav-list"
<li><Link to="/stats">Stats</Link></li> ref={node => (this.listNode = node)}
<li><Link to="/about">About</Link></li> >
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/stats">Stats</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
</ol> </ol>
<Motion <Motion
defaultStyle={{ left: underlineLeft, width: underlineWidth }} defaultStyle={{ left: underlineLeft, width: underlineWidth }}
@ -107,7 +117,10 @@ class Layout extends React.Component {
</div> </div>
<Switch> <Switch>
<Route path="/stats" render={() => <Stats data={this.state.stats}/>}/> <Route
path="/stats"
render={() => <Stats data={this.state.stats} />}
/>
<Route path="/about" component={About} /> <Route path="/about" component={About} />
<Route path="/" component={Home} /> <Route path="/" component={Home} />
</Switch> </Switch>

View File

@ -24,12 +24,11 @@ class NumberTextInput extends React.Component {
this.setState({ value: this.props.value }) this.setState({ value: this.props.value })
} }
handleChange = (event) => { handleChange = event => {
const value = this.props.parseNumber(event.target.value) const value = this.props.parseNumber(event.target.value)
this.setState({ value }, () => { this.setState({ value }, () => {
if (this.props.onChange) if (this.props.onChange) this.props.onChange(value)
this.props.onChange(value)
}) })
} }
@ -39,7 +38,12 @@ class NumberTextInput extends React.Component {
const displayValue = formatNumber(value) const displayValue = formatNumber(value)
return ( return (
<input {...props} type="text" value={displayValue} onChange={this.handleChange}/> <input
{...props}
type="text"
value={displayValue}
onChange={this.handleChange}
/>
) )
} }
} }

View File

@ -8,13 +8,15 @@ import formatPercent from './utils/formatPercent'
import { continents, countries } from 'countries-list' import { continents, countries } from 'countries-list'
const getCountriesByContinent = (continent) => const getCountriesByContinent = continent =>
Object.keys(countries).filter(country => countries[country].continent === continent) Object.keys(countries).filter(
country => countries[country].continent === continent
)
const sumKeyValues = (hash, keys) => const sumKeyValues = (hash, keys) =>
keys.reduce((n, key) => n + (hash[key] || 0), 0) keys.reduce((n, key) => n + (hash[key] || 0), 0)
const sumValues = (hash) => const sumValues = hash =>
Object.keys(hash).reduce((memo, key) => memo + hash[key], 0) Object.keys(hash).reduce((memo, key) => memo + hash[key], 0)
class Stats extends React.Component { class Stats extends React.Component {
@ -30,8 +32,7 @@ class Stats extends React.Component {
render() { render() {
const { data } = this.props const { data } = this.props
if (data == null) if (data == null) return null
return null
const totals = data.totals const totals = data.totals
@ -42,21 +43,39 @@ class Stats extends React.Component {
// Packages // Packages
const packageRows = [] const packageRows = []
Object.keys(totals.requests.package).sort((a, b) => { Object.keys(totals.requests.package)
.sort((a, b) => {
return totals.requests.package[b] - totals.requests.package[a] return totals.requests.package[b] - totals.requests.package[a]
}).forEach(packageName => { })
.forEach(packageName => {
const requests = totals.requests.package[packageName] const requests = totals.requests.package[packageName]
const bandwidth = totals.bandwidth.package[packageName] const bandwidth = totals.bandwidth.package[packageName]
if (requests >= this.state.minPackageRequests) { if (requests >= this.state.minPackageRequests) {
packageRows.push( packageRows.push(
<tr key={packageName}> <tr key={packageName}>
<td><a href={`https://npmjs.org/package/${packageName}`} title={`${packageName} on npm`}>{packageName}</a></td> <td>
<td>{formatNumber(requests)} ({formatPercent(requests / totals.requests.all)}%)</td> <a
{bandwidth href={`https://npmjs.org/package/${packageName}`}
? <td>{formatBytes(bandwidth)} ({formatPercent(bandwidth / totals.bandwidth.all)}%)</td> title={`${packageName} on npm`}
: <td>-</td> >
} {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> </tr>
) )
} }
@ -85,12 +104,23 @@ class Stats extends React.Component {
const continentName = continents[continent] const continentName = continents[continent]
const continentData = continentsData[continent] const continentData = continentsData[continent]
if (continentData.requests > this.state.minCountryRequests && continentData.bandwidth !== 0) { if (
continentData.requests > this.state.minCountryRequests &&
continentData.bandwidth !== 0
) {
regionRows.push( regionRows.push(
<tr key={continent} className="continent-row"> <tr key={continent} className="continent-row">
<td>{continentName}</td> <td>{continentName}</td>
<td>{formatNumber(continentData.requests)} ({formatPercent(continentData.requests / totals.requests.all)}%)</td> <td>
<td>{formatBytes(continentData.bandwidth)} ({formatPercent(continentData.bandwidth / totals.bandwidth.all)}%)</td> {formatNumber(continentData.requests)} ({formatPercent(
continentData.requests / totals.requests.all
)}%)
</td>
<td>
{formatBytes(continentData.bandwidth)} ({formatPercent(
continentData.bandwidth / totals.bandwidth.all
)}%)
</td>
</tr> </tr>
) )
@ -106,8 +136,16 @@ class Stats extends React.Component {
regionRows.push( regionRows.push(
<tr key={continent + country} className="country-row"> <tr key={continent + country} className="country-row">
<td className="country-name">{countries[country].name}</td> <td className="country-name">{countries[country].name}</td>
<td>{formatNumber(countryRequests)} ({formatPercent(countryRequests / totals.requests.all)}%)</td> <td>
<td>{formatBytes(countryBandwidth)} ({formatPercent(countryBandwidth / totals.bandwidth.all)}%)</td> {formatNumber(countryRequests)} ({formatPercent(
countryRequests / totals.requests.all
)}%)
</td>
<td>
{formatBytes(countryBandwidth)} ({formatPercent(
countryBandwidth / totals.bandwidth.all
)}%)
</td>
</tr> </tr>
) )
} }
@ -116,30 +154,58 @@ class Stats extends React.Component {
}) })
// Protocols // Protocols
const protocolRows = Object.keys(totals.requests.protocol).sort((a, b) => { const protocolRows = Object.keys(totals.requests.protocol)
.sort((a, b) => {
return totals.requests.protocol[b] - totals.requests.protocol[a] return totals.requests.protocol[b] - totals.requests.protocol[a]
}).map(protocol => { })
.map(protocol => {
const requests = totals.requests.protocol[protocol] const requests = totals.requests.protocol[protocol]
return ( return (
<tr key={protocol}> <tr key={protocol}>
<td>{protocol}</td> <td>{protocol}</td>
<td>{formatNumber(requests)} ({formatPercent(requests / sumValues(totals.requests.protocol))}%)</td> <td>
{formatNumber(requests)} ({formatPercent(
requests / sumValues(totals.requests.protocol)
)}%)
</td>
</tr> </tr>
) )
}) })
return ( return (
<div className="wrapper"> <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> <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> <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>
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 <p className="table-filter">
Include only packages that received at least{' '}
<select
value={this.state.minPackageRequests} value={this.state.minPackageRequests}
onChange={event => this.setState({ minPackageRequests: parseInt(event.target.value, 10) })} onChange={event =>
this.setState({
minPackageRequests: parseInt(event.target.value, 10)
})
}
> >
<option value="0">0</option> <option value="0">0</option>
<option value="1000">1,000</option> <option value="1000">1,000</option>
@ -147,7 +213,8 @@ class Stats extends React.Component {
<option value="100000">100,000</option> <option value="100000">100,000</option>
<option value="1000000">1,000,000</option> <option value="1000000">1,000,000</option>
<option value="10000000">10,000,000</option> <option value="10000000">10,000,000</option>
</select> requests. </select>{' '}
requests.
</p> </p>
<table cellSpacing="0" cellPadding="0" style={{ width: '100%' }}> <table cellSpacing="0" cellPadding="0" style={{ width: '100%' }}>
@ -158,28 +225,42 @@ class Stats extends React.Component {
<th>Bandwidth (% of total)</th> <th>Bandwidth (% of total)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>{packageRows}</tbody>
{packageRows}
</tbody>
</table> </table>
<h3>Regions</h3> <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>
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 <p className="table-filter">
Include only countries that made at least{' '}
<select
value={this.state.minCountryRequests} value={this.state.minCountryRequests}
onChange={event => this.setState({ minCountryRequests: parseInt(event.target.value, 10) })} onChange={event =>
this.setState({
minCountryRequests: parseInt(event.target.value, 10)
})
}
> >
<option value="0">0</option> <option value="0">0</option>
<option value="100000">100,000</option> <option value="100000">100,000</option>
<option value="1000000">1,000,000</option> <option value="1000000">1,000,000</option>
<option value="10000000">10,000,000</option> <option value="10000000">10,000,000</option>
<option value="100000000">100,000,000</option> <option value="100000000">100,000,000</option>
</select> requests. </select>{' '}
requests.
</p> </p>
<table cellSpacing="0" cellPadding="0" style={{ width: '100%' }} className="regions-table"> <table
cellSpacing="0"
cellPadding="0"
style={{ width: '100%' }}
className="regions-table"
>
<thead> <thead>
<tr> <tr>
<th>Region</th> <th>Region</th>
@ -187,14 +268,16 @@ class Stats extends React.Component {
<th>Bandwidth (% of total)</th> <th>Bandwidth (% of total)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>{regionRows}</tbody>
{regionRows}
</tbody>
</table> </table>
<h3>Protocols</h3> <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> <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%' }}> <table cellSpacing="0" cellPadding="0" style={{ width: '100%' }}>
<thead> <thead>
@ -203,11 +286,8 @@ class Stats extends React.Component {
<th>Requests (% of total)</th> <th>Requests (% of total)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>{protocolRows}</tbody>
{protocolRows}
</tbody>
</table> </table>
</div> </div>
) )
} }

View File

@ -3,7 +3,4 @@ import ReactDOM from 'react-dom'
import App from './App' import App from './App'
import './index.css' import './index.css'
ReactDOM.render( ReactDOM.render(<App />, document.getElementById('root'))
<App />,
document.getElementById('root')
)

View File

@ -1,9 +1,8 @@
const formatNumber = (n) => { const formatNumber = n => {
const digits = String(n).split('') const digits = String(n).split('')
const groups = [] const groups = []
while (digits.length) while (digits.length) groups.unshift(digits.splice(-3).join(''))
groups.unshift(digits.splice(-3).join(''))
return groups.join(',') return groups.join(',')
} }

View File

@ -1,4 +1,3 @@
const parseNumber = (s) => const parseNumber = s => parseInt(s.replace(/,/g, ''), 10) || 0
parseInt(s.replace(/,/g, ''), 10) || 0
export default parseNumber export default parseNumber

View File

@ -114,5 +114,9 @@
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"
},
"prettier": {
"semi": false,
"single-quote": true
} }
} }

View File

@ -7,15 +7,9 @@ const CloudflareAPIURL = 'https://api.cloudflare.com'
const CloudflareEmail = process.env.CLOUDFLARE_EMAIL const CloudflareEmail = process.env.CLOUDFLARE_EMAIL
const CloudflareKey = process.env.CLOUDFLARE_KEY const CloudflareKey = process.env.CLOUDFLARE_KEY
invariant( invariant(CloudflareEmail, 'Missing the $CLOUDFLARE_EMAIL environment variable')
CloudflareEmail,
'Missing the $CLOUDFLARE_EMAIL environment variable'
)
invariant( invariant(CloudflareKey, 'Missing the $CLOUDFLARE_KEY environment variable')
CloudflareKey,
'Missing the $CLOUDFLARE_KEY environment variable'
)
function get(path, headers) { function get(path, headers) {
return fetch(`${CloudflareAPIURL}/client/v4${path}`, { return fetch(`${CloudflareAPIURL}/client/v4${path}`, {
@ -27,9 +21,11 @@ function get(path, headers) {
} }
function getJSON(path, headers) { function getJSON(path, headers) {
return get(path, headers).then(function (res) { return get(path, headers)
.then(function(res) {
return res.json() return res.json()
}).then(function (data) { })
.then(function(data) {
if (!data.success) { if (!data.success) {
console.error(`CloudflareAPI.getJSON failed at ${path}`) console.error(`CloudflareAPI.getJSON failed at ${path}`)
console.error(data) console.error(data)
@ -69,7 +65,11 @@ function reduceResults(target, values) {
function getZoneAnalyticsDashboard(zones, since, until) { function getZoneAnalyticsDashboard(zones, since, until) {
return Promise.all( return Promise.all(
(Array.isArray(zones) ? zones : [zones]).map(function(zone) { (Array.isArray(zones) ? zones : [zones]).map(function(zone) {
return getJSON(`/zones/${zone.id}/analytics/dashboard?since=${since.toISOString()}&until=${until.toISOString()}`) return getJSON(
`/zones/${
zone.id
}/analytics/dashboard?since=${since.toISOString()}&until=${until.toISOString()}`
)
}) })
).then(function(results) { ).then(function(results) {
return results.reduce(reduceResults) return results.reduce(reduceResults)
@ -77,17 +77,23 @@ function getZoneAnalyticsDashboard(zones, since, until) {
} }
function getJSONStream(path, headers) { function getJSONStream(path, headers) {
const acceptGzipHeaders = Object.assign({}, headers, { 'Accept-Encoding': 'gzip' }) const acceptGzipHeaders = Object.assign({}, headers, {
'Accept-Encoding': 'gzip'
})
return get(path, acceptGzipHeaders).then(function (res) { return get(path, acceptGzipHeaders)
.then(function(res) {
return res.body.pipe(gunzip()) return res.body.pipe(gunzip())
}).then(function (stream) { })
.then(function(stream) {
return stream.pipe(ndjson.parse()) return stream.pipe(ndjson.parse())
}) })
} }
function getLogs(zoneId, startTime, endTime) { function getLogs(zoneId, startTime, endTime) {
return getJSONStream(`/zones/${zoneId}/logs/requests?start=${startTime}&end=${endTime}`) return getJSONStream(
`/zones/${zoneId}/logs/requests?start=${startTime}&end=${endTime}`
)
} }
module.exports = { module.exports = {

View File

@ -2,7 +2,8 @@ const redis = require('redis')
redis.debug_mode = process.env.DEBUG_REDIS != null redis.debug_mode = process.env.DEBUG_REDIS != null
const RedisURL = process.env.OPENREDIS_URL || process.env.REDIS_URL || 'redis://localhost:6379' const RedisURL =
process.env.OPENREDIS_URL || process.env.REDIS_URL || 'redis://localhost:6379'
const client = redis.createClient(RedisURL) const client = redis.createClient(RedisURL)

View File

@ -45,11 +45,15 @@ function getScoresMap(key, n = 100) {
} }
function getPackageRequests(date, n = 100) { function getPackageRequests(date, n = 100) {
return getScoresMap(`stats-packageRequests-${createDayKey(date)}`, n).then(prunePackages) return getScoresMap(`stats-packageRequests-${createDayKey(date)}`, n).then(
prunePackages
)
} }
function getPackageBandwidth(date, n = 100) { function getPackageBandwidth(date, n = 100) {
return getScoresMap(`stats-packageBytes-${createDayKey(date)}`, n).then(prunePackages) return getScoresMap(`stats-packageBytes-${createDayKey(date)}`, n).then(
prunePackages
)
} }
function getProtocolRequests(date) { function getProtocolRequests(date) {
@ -82,9 +86,8 @@ function sumMaps(maps) {
} }
function addDailyMetrics(result) { function addDailyMetrics(result) {
return Promise.all( return Promise.all(result.timeseries.map(addDailyMetricsToTimeseries)).then(
result.timeseries.map(addDailyMetricsToTimeseries) function() {
).then(function () {
result.totals.requests.package = sumMaps( result.totals.requests.package = sumMaps(
result.timeseries.map(function(timeseries) { result.timeseries.map(function(timeseries) {
return timeseries.requests.package return timeseries.requests.package
@ -104,7 +107,8 @@ function addDailyMetrics(result) {
) )
return result return result
}) }
)
} }
function extractPublicInfo(data) { function extractPublicInfo(data) {
@ -136,14 +140,13 @@ function extractPublicInfo(data) {
} }
} }
const DomainNames = [ const DomainNames = ['unpkg.com', 'npmcdn.com']
'unpkg.com',
'npmcdn.com'
]
function fetchStats(since, until) { function fetchStats(since, until) {
return cf.getZones(DomainNames).then(function(zones) { return cf.getZones(DomainNames).then(function(zones) {
return cf.getZoneAnalyticsDashboard(zones, since, until).then(function (dashboard) { return cf
.getZoneAnalyticsDashboard(zones, since, until)
.then(function(dashboard) {
return { return {
timeseries: dashboard.timeseries.map(extractPublicInfo), timeseries: dashboard.timeseries.map(extractPublicInfo),
totals: extractPublicInfo(dashboard.totals) totals: extractPublicInfo(dashboard.totals)
@ -159,8 +162,7 @@ const oneDay = oneHour * 24
function getStats(since, until, callback) { function getStats(since, until, callback) {
let promise = fetchStats(since, until) let promise = fetchStats(since, until)
if ((until - since) > oneDay) if (until - since > oneDay) promise = promise.then(addDailyMetrics)
promise = promise.then(addDailyMetrics)
promise.then(function(value) { promise.then(function(value) {
callback(null, value) callback(null, value)

View File

@ -10,12 +10,17 @@ function createSearchServer() {
if (!query) if (!query)
return res.status(403).send({ error: 'Missing ?query parameter' }) return res.status(403).send({ error: 'Missing ?query parameter' })
npmSearch(query, page).then(function (result) { npmSearch(query, page).then(
function(result) {
res.send(result) res.send(result)
}, function (error) { },
function(error) {
console.error(error) console.error(error)
res.status(500).send({ error: 'There was an error executing the search' }) res
}) .status(500)
.send({ error: 'There was an error executing the search' })
}
)
}) })
return app return app

View File

@ -8,14 +8,18 @@ describe('The server app', function () {
}) })
it('rejects invalid package names', function(done) { it('rejects invalid package names', function(done) {
request(app).get('/_invalid/index.js').then(function (res) { request(app)
.get('/_invalid/index.js')
.then(function(res) {
expect(res.statusCode).toBe(403) expect(res.statusCode).toBe(403)
done() done()
}) })
}) })
it('redirects invalid query params', function(done) { it('redirects invalid query params', function(done) {
request(app).get('/react?main=index&invalid').then(function (res) { request(app)
.get('/react?main=index&invalid')
.then(function(res) {
expect(res.statusCode).toBe(302) expect(res.statusCode).toBe(302)
expect(res.headers.location).toBe('/react?main=index') expect(res.headers.location).toBe('/react?main=index')
done() done()
@ -23,7 +27,9 @@ describe('The server app', function () {
}) })
it('redirects /_meta to ?meta', function(done) { it('redirects /_meta to ?meta', function(done) {
request(app).get('/_meta/react?main=index').then(function (res) { request(app)
.get('/_meta/react?main=index')
.then(function(res) {
expect(res.statusCode).toBe(302) expect(res.statusCode).toBe(302)
expect(res.headers.location).toBe('/react?main=index&meta') expect(res.headers.location).toBe('/react?main=index&meta')
done() done()

View File

@ -5,7 +5,7 @@ const startOfSecond = require('date-fns/start_of_second')
const StatsServer = require('./StatsServer') const StatsServer = require('./StatsServer')
function serveArbitraryStats(req, res) { function serveArbitraryStats(req, res) {
const now = startOfSecond(new Date) const now = startOfSecond(new Date())
const since = req.query.since ? new Date(req.query.since) : subDays(now, 30) const since = req.query.since ? new Date(req.query.since) : subDays(now, 30)
const until = req.query.until ? new Date(req.query.until) : now const until = req.query.until ? new Date(req.query.until) : now
@ -16,7 +16,9 @@ function serveArbitraryStats(req, res) {
return res.status(403).send({ error: '?until is not a valid date' }) return res.status(403).send({ error: '?until is not a valid date' })
if (until <= since) if (until <= since)
return res.status(403).send({ error: '?until date must come after ?since date' }) return res
.status(403)
.send({ error: '?until date must come after ?since date' })
if (until > now) if (until > now)
return res.status(403).send({ error: '?until must be a date in the past' }) return res.status(403).send({ error: '?until must be a date in the past' })
@ -26,16 +28,18 @@ function serveArbitraryStats(req, res) {
console.error(error) console.error(error)
res.status(500).send({ error: 'Unable to fetch stats' }) res.status(500).send({ error: 'Unable to fetch stats' })
} else { } else {
res.set({ res
.set({
'Cache-Control': 'public, max-age=60', 'Cache-Control': 'public, max-age=60',
'Cache-Tag': 'stats' 'Cache-Tag': 'stats'
}).send(stats) })
.send(stats)
} }
}) })
} }
function servePastDaysStats(days, req, res) { function servePastDaysStats(days, req, res) {
const until = startOfDay(new Date) const until = startOfDay(new Date())
const since = subDays(until, days) const since = subDays(until, days)
StatsServer.getStats(since, until, function(error, stats) { StatsServer.getStats(since, until, function(error, stats) {
@ -43,10 +47,12 @@ function servePastDaysStats(days, req, res) {
console.error(error) console.error(error)
res.status(500).send({ error: 'Unable to fetch stats' }) res.status(500).send({ error: 'Unable to fetch stats' })
} else { } else {
res.set({ res
.set({
'Cache-Control': 'public, max-age=60', 'Cache-Control': 'public, max-age=60',
'Cache-Tag': 'stats' 'Cache-Tag': 'stats'
}).send(stats) })
.send(stats)
} }
}) })
} }

View File

@ -66,9 +66,22 @@ function computeCounters(stream) {
const url = parsePackageURL(parseURL(clientRequest.uri).pathname) const url = parsePackageURL(parseURL(clientRequest.uri).pathname)
const packageName = url && url.packageName const packageName = url && url.packageName
if (packageName && validateNPMPackageName(packageName).errors == null) { if (
incr(`stats-packageRequests-${dayKey}`, packageName, 1, thirtyDaysLater) packageName &&
incr(`stats-packageBytes-${dayKey}`, packageName, edgeResponse.bytes, thirtyDaysLater) validateNPMPackageName(packageName).errors == null
) {
incr(
`stats-packageRequests-${dayKey}`,
packageName,
1,
thirtyDaysLater
)
incr(
`stats-packageBytes-${dayKey}`,
packageName,
edgeResponse.bytes,
thirtyDaysLater
)
} }
} }
@ -85,7 +98,12 @@ function computeCounters(stream) {
if (hostname) { if (hostname) {
incr(`stats-hostnameRequests-${dayKey}`, hostname, 1, sevenDaysLater) incr(`stats-hostnameRequests-${dayKey}`, hostname, 1, sevenDaysLater)
incr(`stats-hostnameBytes-${dayKey}`, hostname, edgeResponse.bytes, sevenDaysLater) incr(
`stats-hostnameBytes-${dayKey}`,
hostname,
edgeResponse.bytes,
sevenDaysLater
)
} }
}) })
.on('end', function() { .on('end', function() {
@ -103,8 +121,7 @@ function processLogs(stream) {
db.zincrby(key, values[member], member) db.zincrby(key, values[member], member)
}) })
if (expireat[key]) if (expireat[key]) db.expireat(key, expireat[key])
db.expireat(key, expireat[key])
}) })
}) })
} }
@ -149,7 +166,10 @@ function ingestLogs(zone, startSeconds, endSeconds) {
} }
function startZone(zone) { function startZone(zone) {
const startSecondsKey = `ingestLogsWorker-nextStartSeconds-${zone.name.replace('.', '-')}` const startSecondsKey = `ingestLogsWorker-nextStartSeconds-${zone.name.replace(
'.',
'-'
)}`
function takeATurn() { function takeATurn() {
db.get(startSecondsKey, function(error, value) { db.get(startSecondsKey, function(error, value) {
@ -182,18 +202,21 @@ function startZone(zone) {
// set of logs. This will help ensure that any congestion in the log // set of logs. This will help ensure that any congestion in the log
// pipeline has passed and a full set of logs can be ingested. // pipeline has passed and a full set of logs can be ingested.
// https://support.cloudflare.com/hc/en-us/articles/216672448-Enterprise-Log-Share-REST-API // https://support.cloudflare.com/hc/en-us/articles/216672448-Enterprise-Log-Share-REST-API
const maxSeconds = toSeconds(now - (oneMinute * 30)) const maxSeconds = toSeconds(now - oneMinute * 30)
if (startSeconds < maxSeconds) { if (startSeconds < maxSeconds) {
const endSeconds = startSeconds + LogWindowSeconds const endSeconds = startSeconds + LogWindowSeconds
ingestLogs(zone, startSeconds, endSeconds).then(function () { ingestLogs(zone, startSeconds, endSeconds).then(
function() {
db.set(startSecondsKey, endSeconds) db.set(startSecondsKey, endSeconds)
setTimeout(takeATurn) setTimeout(takeATurn)
}, function (error) { },
function(error) {
console.error(error.stack) console.error(error.stack)
process.exit(1) process.exit(1)
}) }
)
} else { } else {
setTimeout(takeATurn, (startSeconds - maxSeconds) * 1000) setTimeout(takeATurn, (startSeconds - maxSeconds) * 1000)
} }

View File

@ -2,7 +2,10 @@ function checkBlacklist(blacklist) {
return function(req, res, next) { return function(req, res, next) {
// Do not allow packages that have been blacklisted. // Do not allow packages that have been blacklisted.
if (blacklist.includes(req.packageName)) { if (blacklist.includes(req.packageName)) {
res.status(403).type('text').send(`Package "${req.packageName}" is blacklisted`) res
.status(403)
.type('text')
.send(`Package "${req.packageName}" is blacklisted`)
} else { } else {
next() next()
} }

View File

@ -4,27 +4,28 @@ const getFileContentType = require('../utils/getFileContentType')
const e = React.createElement const e = React.createElement
const formatTime = (time) => const formatTime = time => new Date(time).toISOString()
new Date(time).toISOString()
const DirectoryListing = ({ dir, entries }) => { const DirectoryListing = ({ dir, entries }) => {
const rows = entries.map(({ file, stats }, index) => { const rows = entries.map(({ file, stats }, index) => {
const isDir = stats.isDirectory() const isDir = stats.isDirectory()
const href = file + (isDir ? '/' : '') const href = file + (isDir ? '/' : '')
return ( return e(
e('tr', { key: file, className: index % 2 ? 'odd' : 'even' }, 'tr',
{ key: file, className: index % 2 ? 'odd' : 'even' },
e('td', null, e('a', { title: file, href }, file)), e('td', null, e('a', { title: file, href }, file)),
e('td', null, isDir ? '-' : getFileContentType(file)), e('td', null, isDir ? '-' : getFileContentType(file)),
e('td', null, isDir ? '-' : prettyBytes(stats.size)), e('td', null, isDir ? '-' : prettyBytes(stats.size)),
e('td', null, isDir ? '-' : formatTime(stats.mtime)) e('td', null, isDir ? '-' : formatTime(stats.mtime))
) )
)
}) })
if (dir !== '/') if (dir !== '/')
rows.unshift( rows.unshift(
e('tr', { key: '..', className: 'odd' }, e(
'tr',
{ key: '..', className: 'odd' },
e('td', null, e('a', { title: 'Parent directory', href: '../' }, '..')), e('td', null, e('a', { title: 'Parent directory', href: '../' }, '..')),
e('td', null, '-'), e('td', null, '-'),
e('td', null, '-'), e('td', null, '-'),
@ -32,10 +33,15 @@ const DirectoryListing = ({ dir, entries }) => {
) )
) )
return ( return e(
e('table', null, 'table',
e('thead', null, null,
e('tr', null, e(
'thead',
null,
e(
'tr',
null,
e('th', null, 'Name'), e('th', null, 'Name'),
e('th', null, 'Type'), e('th', null, 'Type'),
e('th', null, 'Size'), e('th', null, 'Size'),
@ -44,7 +50,6 @@ const DirectoryListing = ({ dir, entries }) => {
), ),
e('tbody', null, rows) e('tbody', null, rows)
) )
)
} }
module.exports = DirectoryListing module.exports = DirectoryListing

View File

@ -13,25 +13,33 @@ s.onchange = function () {
} }
` `
const byVersion = (a, b) => const byVersion = (a, b) => (semver.lt(a, b) ? -1 : semver.gt(a, b) ? 1 : 0)
semver.lt(a, b) ? -1 : (semver.gt(a, b) ? 1 : 0)
const IndexPage = ({ packageInfo, version, dir, entries }) => { const IndexPage = ({ packageInfo, version, dir, entries }) => {
const versions = Object.keys(packageInfo.versions).sort(byVersion) const versions = Object.keys(packageInfo.versions).sort(byVersion)
const options = versions.map(v => ( const options = versions.map(v =>
e('option', { key: v, value: v }, `${packageInfo.name}@${v}`) e('option', { key: v, value: v }, `${packageInfo.name}@${v}`)
)) )
return ( return e(
e('html', null, 'html',
e('head', null, null,
e(
'head',
null,
e('meta', { charSet: 'utf-8' }), e('meta', { charSet: 'utf-8' }),
e('title', null, `Index of ${dir}`), e('title', null, `Index of ${dir}`),
e('style', { dangerouslySetInnerHTML: { __html: IndexPageStyle } }) e('style', { dangerouslySetInnerHTML: { __html: IndexPageStyle } })
), ),
e('body', null, e(
e('div', { className: 'content-wrapper' }, 'body',
e('div', { className: 'version-wrapper' }, null,
e(
'div',
{ className: 'content-wrapper' },
e(
'div',
{ className: 'version-wrapper' },
e('select', { id: 'version', defaultValue: version }, options) e('select', { id: 'version', defaultValue: version }, options)
), ),
e('h1', null, `Index of ${dir}`), e('h1', null, `Index of ${dir}`),
@ -43,7 +51,6 @@ const IndexPage = ({ packageInfo, version, dir, entries }) => {
) )
) )
) )
)
} }
module.exports = IndexPage module.exports = IndexPage

View File

@ -32,7 +32,11 @@ function findFile(base, useIndex, callback) {
callback(error) callback(error)
} }
} else if (useIndex && stats.isDirectory()) { } else if (useIndex && stats.isDirectory()) {
findFile(path.join(file, 'index'), false, function (error, indexFile, indexStats) { findFile(path.join(file, 'index'), false, function(
error,
indexFile,
indexStats
) {
if (error) { if (error) {
callback(error) callback(error)
} else if (indexFile) { } else if (indexFile) {
@ -58,11 +62,17 @@ function fetchFile(req, res, next) {
getPackageInfo(req.packageName, function(error, packageInfo) { getPackageInfo(req.packageName, function(error, packageInfo) {
if (error) { if (error) {
console.error(error) console.error(error)
return res.status(500).type('text').send(`Cannot get info for package "${req.packageName}"`) return res
.status(500)
.type('text')
.send(`Cannot get info for package "${req.packageName}"`)
} }
if (packageInfo == null || packageInfo.versions == null) if (packageInfo == null || packageInfo.versions == null)
return res.status(404).type('text').send(`Cannot find package "${req.packageName}"`) return res
.status(404)
.type('text')
.send(`Cannot find package "${req.packageName}"`)
req.packageInfo = packageInfo req.packageInfo = packageInfo
@ -73,7 +83,10 @@ function fetchFile(req, res, next) {
getPackage(req.packageConfig, function(error, outputDir) { getPackage(req.packageConfig, function(error, outputDir) {
if (error) { if (error) {
console.error(error) console.error(error)
res.status(500).type('text').send(`Cannot fetch package ${req.packageSpec}`) res
.status(500)
.type('text')
.send(`Cannot fetch package ${req.packageSpec}`)
} else { } else {
req.packageDir = outputDir req.packageDir = outputDir
@ -84,12 +97,18 @@ function fetchFile(req, res, next) {
// They want an ES module. Try "module", "jsnext:main", and "/" // They want an ES module. Try "module", "jsnext:main", and "/"
// https://github.com/rollup/rollup/wiki/pkg.module // https://github.com/rollup/rollup/wiki/pkg.module
if (!filename) if (!filename)
filename = req.packageConfig.module || req.packageConfig['jsnext:main'] || '/' filename =
req.packageConfig.module ||
req.packageConfig['jsnext:main'] ||
'/'
} else if (filename) { } else if (filename) {
// They are requesting an explicit filename. Only try to find an // They are requesting an explicit filename. Only try to find an
// index file if they are NOT requesting an HTML directory listing. // index file if they are NOT requesting an HTML directory listing.
useIndex = filename[filename.length - 1] !== '/' useIndex = filename[filename.length - 1] !== '/'
} else if (req.query.main && typeof req.packageConfig[req.query.main] === 'string') { } else if (
req.query.main &&
typeof req.packageConfig[req.query.main] === 'string'
) {
// They specified a custom ?main field. // They specified a custom ?main field.
filename = req.packageConfig[req.query.main] filename = req.packageConfig[req.query.main]
} else if (typeof req.packageConfig.unpkg === 'string') { } else if (typeof req.packageConfig.unpkg === 'string') {
@ -104,23 +123,46 @@ function fetchFile(req, res, next) {
filename = req.packageConfig.main || '/' filename = req.packageConfig.main || '/'
} }
findFile(path.join(req.packageDir, filename), useIndex, function (error, file, stats) { findFile(path.join(req.packageDir, filename), useIndex, function(
if (error) error,
console.error(error) file,
stats
) {
if (error) console.error(error)
if (file == null) if (file == null)
return res.status(404).type('text').send(`Cannot find module "${filename}" in package ${req.packageSpec}`) return res
.status(404)
.type('text')
.send(
`Cannot find module "${filename}" in package ${
req.packageSpec
}`
)
filename = file.replace(req.packageDir, '') filename = file.replace(req.packageDir, '')
if (req.query.main != null || getBasename(req.filename) !== getBasename(filename)) { if (
req.query.main != null ||
getBasename(req.filename) !== getBasename(filename)
) {
// Need to redirect to the module file so relative imports resolve // Need to redirect to the module file so relative imports resolve
// correctly. Cache module redirects for 1 minute. // correctly. Cache module redirects for 1 minute.
delete req.query.main delete req.query.main
res.set({ res
.set({
'Cache-Control': 'public, max-age=60', 'Cache-Control': 'public, max-age=60',
'Cache-Tag': 'redirect,module-redirect' 'Cache-Tag': 'redirect,module-redirect'
}).redirect(302, createPackageURL(req.packageName, req.packageVersion, filename, createSearch(req.query))) })
.redirect(
302,
createPackageURL(
req.packageName,
req.packageVersion,
filename,
createSearch(req.query)
)
)
} else { } else {
req.filename = filename req.filename = filename
req.stats = stats req.stats = stats
@ -131,21 +173,47 @@ function fetchFile(req, res, next) {
}) })
} else if (req.packageVersion in req.packageInfo['dist-tags']) { } else if (req.packageVersion in req.packageInfo['dist-tags']) {
// Cache tag redirects for 1 minute. // Cache tag redirects for 1 minute.
res.set({ res
.set({
'Cache-Control': 'public, max-age=60', 'Cache-Control': 'public, max-age=60',
'Cache-Tag': 'redirect,tag-redirect' 'Cache-Tag': 'redirect,tag-redirect'
}).redirect(302, createPackageURL(req.packageName, req.packageInfo['dist-tags'][req.packageVersion], req.filename, req.search)) })
.redirect(
302,
createPackageURL(
req.packageName,
req.packageInfo['dist-tags'][req.packageVersion],
req.filename,
req.search
)
)
} else { } else {
const maxVersion = semver.maxSatisfying(Object.keys(req.packageInfo.versions), req.packageVersion) const maxVersion = semver.maxSatisfying(
Object.keys(req.packageInfo.versions),
req.packageVersion
)
if (maxVersion) { if (maxVersion) {
// Cache semver redirects for 1 minute. // Cache semver redirects for 1 minute.
res.set({ res
.set({
'Cache-Control': 'public, max-age=60', 'Cache-Control': 'public, max-age=60',
'Cache-Tag': 'redirect,semver-redirect' 'Cache-Tag': 'redirect,semver-redirect'
}).redirect(302, createPackageURL(req.packageName, maxVersion, req.filename, req.search)) })
.redirect(
302,
createPackageURL(
req.packageName,
maxVersion,
req.filename,
req.search
)
)
} else { } else {
res.status(404).type('text').send(`Cannot find package ${req.packageSpec}`) res
.status(404)
.type('text')
.send(`Cannot find package ${req.packageSpec}`)
} }
} }
}) })

View File

@ -20,8 +20,7 @@ function sanitizeQuery(query) {
const saneQuery = {} const saneQuery = {}
Object.keys(query).forEach(function(param) { Object.keys(query).forEach(function(param) {
if (isKnownQueryParam(param)) if (isKnownQueryParam(param)) saneQuery[param] = query[param]
saneQuery[param] = query[param]
}) })
return saneQuery return saneQuery
@ -54,13 +53,21 @@ function packageURL(req, res, next) {
// Do not allow invalid URLs. // Do not allow invalid URLs.
if (url == null) if (url == null)
return res.status(403).type('text').send(`Invalid URL: ${req.url}`) return res
.status(403)
.type('text')
.send(`Invalid URL: ${req.url}`)
const nameErrors = validateNPMPackageName(url.packageName).errors const nameErrors = validateNPMPackageName(url.packageName).errors
// Do not allow invalid package names. // Do not allow invalid package names.
if (nameErrors) if (nameErrors)
return res.status(403).type('text').send(`Invalid package name: ${url.packageName} (${nameErrors.join(', ')})`) return res
.status(403)
.type('text')
.send(
`Invalid package name: ${url.packageName} (${nameErrors.join(', ')})`
)
req.packageName = url.packageName req.packageName = url.packageName
req.packageVersion = url.packageVersion req.packageVersion = url.packageVersion

View File

@ -20,9 +20,7 @@ const MaximumDepth = 128
const FileTransforms = { const FileTransforms = {
expand: function(file, dependencies, callback) { expand: function(file, dependencies, callback) {
const options = { const options = {
plugins: [ plugins: [unpkgRewrite(dependencies)]
unpkgRewrite(dependencies)
]
} }
babel.transformFile(file, options, function(error, result) { babel.transformFile(file, options, function(error, result) {
@ -31,23 +29,32 @@ const FileTransforms = {
} }
} }
/** /**
* Send the file, JSON metadata, or HTML directory listing. * Send the file, JSON metadata, or HTML directory listing.
*/ */
function serveFile(req, res, next) { function serveFile(req, res, next) {
if (req.query.meta != null) { if (req.query.meta != null) {
// Serve JSON metadata. // Serve JSON metadata.
getMetadata(req.packageDir, req.filename, req.stats, MaximumDepth, function (error, metadata) { getMetadata(req.packageDir, req.filename, req.stats, MaximumDepth, function(
error,
metadata
) {
if (error) { if (error) {
console.error(error) console.error(error)
res.status(500).type('text').send(`Cannot generate metadata for ${req.packageSpec}${req.filename}`) res
.status(500)
.type('text')
.send(
`Cannot generate metadata for ${req.packageSpec}${req.filename}`
)
} else { } else {
// Cache metadata for 1 year. // Cache metadata for 1 year.
res.set({ res
.set({
'Cache-Control': 'public, max-age=31536000', 'Cache-Control': 'public, max-age=31536000',
'Cache-Tag': 'meta' 'Cache-Tag': 'meta'
}).send(metadata) })
.send(metadata)
} }
}) })
} else if (req.stats.isFile()) { } else if (req.stats.isFile()) {
@ -56,12 +63,12 @@ function serveFile(req, res, next) {
let contentType = getFileContentType(file) let contentType = getFileContentType(file)
if (contentType === 'text/html') if (contentType === 'text/html') contentType = 'text/plain' // We can't serve HTML because bad people :(
contentType = 'text/plain' // We can't serve HTML because bad people :(
if (contentType === 'application/javascript' && req.query.module != null) { if (contentType === 'application/javascript' && req.query.module != null) {
// Serve a JavaScript module. // Serve a JavaScript module.
const dependencies = Object.assign({}, const dependencies = Object.assign(
{},
req.packageConfig.peerDependencies, req.packageConfig.peerDependencies,
req.packageConfig.dependencies req.packageConfig.dependencies
) )
@ -69,16 +76,30 @@ function serveFile(req, res, next) {
FileTransforms.expand(file, dependencies, function(error, code) { FileTransforms.expand(file, dependencies, function(error, code) {
if (error) { if (error) {
console.error(error) console.error(error)
const debugInfo = error.constructor.name + ': ' + error.message.replace(/^.*?\/unpkg-.+?\//, `/${req.packageSpec}/`) + '\n\n' + error.codeFrame const debugInfo =
res.status(500).type('text').send(`Cannot generate module for ${req.packageSpec}${req.filename}\n\n${debugInfo}`) error.constructor.name +
': ' +
error.message.replace(/^.*?\/unpkg-.+?\//, `/${req.packageSpec}/`) +
'\n\n' +
error.codeFrame
res
.status(500)
.type('text')
.send(
`Cannot generate module for ${req.packageSpec}${
req.filename
}\n\n${debugInfo}`
)
} else { } else {
// Cache modules for 1 year. // Cache modules for 1 year.
res.set({ res
.set({
'Content-Type': contentType, 'Content-Type': contentType,
'Content-Length': Buffer.byteLength(code), 'Content-Length': Buffer.byteLength(code),
'Cache-Control': 'public, max-age=31536000', 'Cache-Control': 'public, max-age=31536000',
'Cache-Tag': 'file,js-file,js-module' 'Cache-Tag': 'file,js-file,js-module'
}).send(code) })
.send(code)
} }
}) })
} else { } else {
@ -87,8 +108,7 @@ function serveFile(req, res, next) {
const ext = path.extname(req.filename).substr(1) const ext = path.extname(req.filename).substr(1)
if (ext) if (ext) tags.push(`${ext}-file`)
tags.push(`${ext}-file`)
// Cache files for 1 year. // Cache files for 1 year.
res.set({ res.set({
@ -96,7 +116,7 @@ function serveFile(req, res, next) {
'Content-Length': req.stats.size, 'Content-Length': req.stats.size,
'Cache-Control': 'public, max-age=31536000', 'Cache-Control': 'public, max-age=31536000',
'Last-Modified': req.stats.mtime.toUTCString(), 'Last-Modified': req.stats.mtime.toUTCString(),
'ETag': etag(req.stats), ETag: etag(req.stats),
'Cache-Tag': tags.join(',') 'Cache-Tag': tags.join(',')
}) })
@ -112,20 +132,36 @@ function serveFile(req, res, next) {
} }
} else if (AutoIndex && req.stats.isDirectory()) { } else if (AutoIndex && req.stats.isDirectory()) {
// Serve an HTML directory listing. // Serve an HTML directory listing.
getIndexHTML(req.packageInfo, req.packageVersion, req.packageDir, req.filename, function (error, html) { getIndexHTML(
req.packageInfo,
req.packageVersion,
req.packageDir,
req.filename,
function(error, html) {
if (error) { if (error) {
console.error(error) console.error(error)
res.status(500).type('text').send(`Cannot generate index page for ${req.packageSpec}${req.filename}`) res
.status(500)
.type('text')
.send(
`Cannot generate index page for ${req.packageSpec}${req.filename}`
)
} else { } else {
// Cache HTML directory listings for 1 minute. // Cache HTML directory listings for 1 minute.
res.set({ res
.set({
'Cache-Control': 'public, max-age=60', 'Cache-Control': 'public, max-age=60',
'Cache-Tag': 'index' 'Cache-Tag': 'index'
}).send(html)
}
}) })
.send(html)
}
}
)
} else { } else {
res.status(403).type('text').send(`Cannot serve ${req.packageSpec}${req.filename}; it's not a file`) res
.status(403)
.type('text')
.send(`Cannot serve ${req.packageSpec}${req.filename}; it's not a file`)
} }
} }

View File

@ -5,9 +5,12 @@ function createMutex(doWork) {
if (mutex[key]) { if (mutex[key]) {
mutex[key].push(callback) mutex[key].push(callback)
} else { } else {
mutex[key] = [ function () { mutex[key] = [
function() {
delete mutex[key] delete mutex[key]
}, callback ] },
callback
]
doWork(payload, function(error, value) { doWork(payload, function(error, value) {
mutex[key].forEach(function(callback) { mutex[key].forEach(function(callback) {

View File

@ -17,9 +17,16 @@ function getEntries(dir, file, maximumDepth) {
return getFileStats(path.join(dir, file, f)) return getFileStats(path.join(dir, file, f))
}) })
).then(function(statsArray) { ).then(function(statsArray) {
return Promise.all(statsArray.map(function (stats, index) { return Promise.all(
return getMetadataRecursive(dir, path.join(file, files[index]), stats, maximumDepth - 1) statsArray.map(function(stats, index) {
})) return getMetadataRecursive(
dir,
path.join(file, files[index]),
stats,
maximumDepth - 1
)
})
)
}) })
) )
} }
@ -69,9 +76,12 @@ function getMetadataRecursive(dir, file, stats, maximumDepth) {
} }
function getMetadata(baseDir, path, stats, maximumDepth, callback) { function getMetadata(baseDir, path, stats, maximumDepth, callback) {
getMetadataRecursive(baseDir, path, stats, maximumDepth).then(function (metadata) { getMetadataRecursive(baseDir, path, stats, maximumDepth).then(function(
metadata
) {
callback(null, metadata) callback(null, metadata)
}, callback) },
callback)
} }
module.exports = getMetadata module.exports = getMetadata

View File

@ -20,7 +20,7 @@ function fetchPackageInfo(packageName) {
return fetch(url, { return fetch(url, {
headers: { headers: {
'Accept': 'application/json' Accept: 'application/json'
} }
}).then(function(res) { }).then(function(res) {
return res.status === 404 ? null : res.json() return res.status === 404 ? null : res.json()
@ -32,7 +32,8 @@ const PackageNotFound = 'PackageNotFound'
// This mutex prevents multiple concurrent requests to // This mutex prevents multiple concurrent requests to
// the registry for the same package info. // the registry for the same package info.
const fetchMutex = createMutex(function(packageName, callback) { const fetchMutex = createMutex(function(packageName, callback) {
fetchPackageInfo(packageName).then(function (value) { fetchPackageInfo(packageName).then(
function(value) {
if (value == null) { if (value == null) {
// Cache 404s for 5 minutes. This prevents us from making // Cache 404s for 5 minutes. This prevents us from making
// unnecessary requests to the registry for bad package names. // unnecessary requests to the registry for bad package names.
@ -47,12 +48,14 @@ const fetchMutex = createMutex(function (packageName, callback) {
callback(null, value) callback(null, value)
}) })
} }
}, function (error) { },
function(error) {
// Do not cache errors. // Do not cache errors.
PackageInfoCache.del(packageName, function() { PackageInfoCache.del(packageName, function() {
callback(error) callback(error)
}) })
}) }
)
}) })
function getPackageInfo(packageName, callback) { function getPackageInfo(packageName, callback) {

View File

@ -10,116 +10,66 @@
* The range `null` is a catch-all. * The range `null` is a catch-all.
*/ */
module.exports = { module.exports = {
'angular': [ angular: [['>=1.2.27', '/angular.min.js'], [null, '/lib/angular.min.js']],
[ '>=1.2.27', '/angular.min.js' ],
[ null, '/lib/angular.min.js' ]
],
'angular-animate': [ 'angular-animate': [[null, '/angular-animate.min.js']],
[ null, '/angular-animate.min.js' ]
],
'angular-cookies': [ 'angular-cookies': [[null, '/angular-cookies.min.js']],
[ null, '/angular-cookies.min.js' ]
],
'angular-resource': [ 'angular-resource': [[null, '/angular-resource.min.js']],
[ null, '/angular-resource.min.js' ]
],
'angular-sanitize': [ 'angular-sanitize': [[null, '/angular-sanitize.min.js']],
[ null, '/angular-sanitize.min.js' ]
],
'angular-ui-bootstrap': [ 'angular-ui-bootstrap': [[null, '/dist/ui-bootstrap.js']],
[ null, '/dist/ui-bootstrap.js' ]
],
'animate.css': [ 'animate.css': [[null, '/animate.min.css']],
[ null, '/animate.min.css' ]
],
'babel-standalone': [ 'babel-standalone': [[null, '/babel.min.js']],
[ null, '/babel.min.js' ]
],
'backbone': [ backbone: [[null, '/backbone-min.js']],
[ null, '/backbone-min.js' ]
],
'bootstrap': [ bootstrap: [
[null, '/dist/css/bootstrap.min.css', '/dist/js/bootstrap.min.js'] [null, '/dist/css/bootstrap.min.css', '/dist/js/bootstrap.min.js']
], ],
'bootstrap-sass': [ 'bootstrap-sass': [[null, '/assets/javascripts/bootstrap.min.js']],
[ null, '/assets/javascripts/bootstrap.min.js' ]
],
'bulma': [ bulma: [[null, '/css/bulma.css']],
[ null, '/css/bulma.css' ]
],
'core.js': [ 'core.js': [[null, '/dist/core.min.js']],
[ null, '/dist/core.min.js' ]
],
'create-react-class': [ 'create-react-class': [[null, '/create-react-class.min.js']],
[ null, '/create-react-class.min.js' ]
],
'd3': [ d3: [[null, '/build/d3.min.js']],
[ null, '/build/d3.min.js' ]
],
'ember-source': [ 'ember-source': [[null, '/dist/ember.min.js']],
[ null, '/dist/ember.min.js' ]
],
'foundation-sites': [ 'foundation-sites': [
[null, '/dist/css/foundation.min.css', '/dist/js/foundation.min.js'] [null, '/dist/css/foundation.min.css', '/dist/js/foundation.min.js']
], ],
'gsap': [ gsap: [[null, '/TweenMax.js']],
[ null, '/TweenMax.js' ]
],
'handlebars': [ handlebars: [[null, '/dist/handlebars.min.js']],
[ null, '/dist/handlebars.min.js' ]
],
'jquery': [ jquery: [[null, '/dist/jquery.min.js']],
[ null, '/dist/jquery.min.js' ]
],
'fastclick': [ fastclick: [[null, '/lib/fastclick.js']],
[ null, '/lib/fastclick.js' ]
],
'lodash': [ lodash: [['<3', '/dist/lodash.min.js'], [null, '/lodash.min.js']],
[ '<3', '/dist/lodash.min.js' ],
[ null, '/lodash.min.js' ]
],
'masonry-layout': [ 'masonry-layout': [[null, '/dist/masonry.pkgd.min.js']],
[ null, '/dist/masonry.pkgd.min.js' ]
],
'materialize-css': [ 'materialize-css': [[null, '/dist/css/materialize.min.css']],
[ null, '/dist/css/materialize.min.css' ]
],
'ngx-bootstrap': [ 'ngx-bootstrap': [[null, '/bundles/ngx-bootstrap.umd.js']],
[ null, '/bundles/ngx-bootstrap.umd.js' ]
],
'react': [ react: [
['>=16.0.0-alpha.7', '/umd/react.production.min.js'], ['>=16.0.0-alpha.7', '/umd/react.production.min.js'],
[null, '/dist/react.min.js'] [null, '/dist/react.min.js']
], ],
'react-bootstrap': [ 'react-bootstrap': [[null, '/dist/react-bootstrap.min.js']],
[ null, '/dist/react-bootstrap.min.js' ]
],
'react-dom': [ 'react-dom': [
['>=16.0.0-alpha.7', '/umd/react-dom.production.min.js'], ['>=16.0.0-alpha.7', '/umd/react-dom.production.min.js'],
@ -131,48 +81,25 @@ module.exports = {
[null, '/umd/ReactRouter.min.js'] [null, '/umd/ReactRouter.min.js']
], ],
'redux': [ redux: [[null, '/dist/redux.min.js']],
[ null, '/dist/redux.min.js' ]
],
'redux-saga': [ 'redux-saga': [[null, '/dist/redux-saga.min.js']],
[ null, '/dist/redux-saga.min.js' ]
],
'redux-thunk': [ 'redux-thunk': [[null, '/dist/redux-thunk.min.js']],
[ null, '/dist/redux-thunk.min.js' ]
],
'snapsvg': [ snapsvg: [[null, '/snap.svg-min.js']],
[ null, '/snap.svg-min.js' ]
],
'systemjs': [ systemjs: [[null, '/dist/system.js']],
[ null, '/dist/system.js' ]
],
'three': [ three: [['<=0.77.0', '/three.min.js'], [null, '/build/three.min.js']],
[ '<=0.77.0', '/three.min.js' ],
[ null, '/build/three.min.js' ]
],
'underscore': [ underscore: [[null, '/underscore-min.js']],
[ null, '/underscore-min.js' ]
],
'vue': [ vue: [[null, '/dist/vue.min.js']],
[ null, '/dist/vue.min.js' ]
],
'zepto': [ zepto: [[null, '/dist/zepto.min.js']],
[ null, '/dist/zepto.min.js' ]
],
'zingchart': [ zingchart: [[null, '/client/zingchart.min.js']],
[ null, '/client/zingchart.min.js' ]
],
'zone.js': [ 'zone.js': [[null, '/dist/zone.js']]
[ null, '/dist/zone.js' ]
]
} }

View File

@ -8,8 +8,7 @@ function getAssetPaths(packageName, version) {
const matchingEntry = entries.find(function(entry) { const matchingEntry = entries.find(function(entry) {
const range = entry[0] const range = entry[0]
if (range == null || semver.satisfies(version, range)) if (range == null || semver.satisfies(version, range)) return entry
return entry
}) })
return matchingEntry.slice(1) return matchingEntry.slice(1)

View File

@ -16,9 +16,7 @@ function enhanceHit(hit) {
} else { } else {
// We don't have any global paths for this package yet. Try // We don't have any global paths for this package yet. Try
// using the "bare" URL. // using the "bare" URL.
hit.assets = [ hit.assets = [`https://unpkg.com/${hit.name}@${hit.version}`]
`https://unpkg.com/${hit.name}@${hit.version}`
]
resolve(hit) resolve(hit)
} }
@ -63,9 +61,7 @@ function search(query, page) {
reject(error) reject(error)
} else { } else {
resolve( resolve(
Promise.all( Promise.all(value.hits.map(enhanceHit)).then(function(hits) {
value.hits.map(enhanceHit)
).then(function (hits) {
const totalHits = value.nbHits const totalHits = value.nbHits
const totalPages = value.nbPages const totalPages = value.nbPages

View File

@ -14,9 +14,8 @@ invariant(
'Missing $ALGOLIA_NPM_SEARCH_API_KEY environment variable' 'Missing $ALGOLIA_NPM_SEARCH_API_KEY environment variable'
) )
const index = algolia( const index = algolia(AlgoliaNpmSearchAppId, AlgoliaNpmSearchApiKey).initIndex(
AlgoliaNpmSearchAppId, 'npm-search'
AlgoliaNpmSearchApiKey )
).initIndex('npm-search')
module.exports = index module.exports = index

View File

@ -1,14 +1,11 @@
function createPackageURL(packageName, version, filename, search) { function createPackageURL(packageName, version, filename, search) {
let pathname = `/${packageName}` let pathname = `/${packageName}`
if (version != null) if (version != null) pathname += `@${version}`
pathname += `@${version}`
if (filename) if (filename) pathname += filename
pathname += filename
if (search) if (search) pathname += search
pathname += search
return pathname return pathname
} }

View File

@ -19,14 +19,14 @@ function parsePackageURL(packageURL) {
const match = URLFormat.exec(pathname) const match = URLFormat.exec(pathname)
if (match == null) if (match == null) return null
return null
const packageName = match[1] const packageName = match[1]
const packageVersion = decodeParam(match[2]) || 'latest' const packageVersion = decodeParam(match[2]) || 'latest'
const filename = decodeParam(match[3]) const filename = decodeParam(match[3])
return { // If the URL is /@scope/name@version/file.js?main=browser: return {
// If the URL is /@scope/name@version/file.js?main=browser:
pathname, // /@scope/name@version/path.js pathname, // /@scope/name@version/path.js
search, // ?main=browser search, // ?main=browser
query, // { main: 'browser' } query, // { main: 'browser' }