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,21 +71,32 @@ 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
} }
return ( return (
<div> <div>
<WindowSize onChange={this.adjustUnderline}/> <WindowSize onChange={this.adjustUnderline} />
<div className="wrapper"> <div className="wrapper">
<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,9 +117,12 @@ class Layout extends React.Component {
</div> </div>
<Switch> <Switch>
<Route path="/stats" render={() => <Stats data={this.state.stats}/>}/> <Route
<Route path="/about" component={About}/> path="/stats"
<Route path="/" component={Home}/> render={() => <Stats data={this.state.stats} />}
/>
<Route path="/about" component={About} />
<Route path="/" component={Home} />
</Switch> </Switch>
</div> </div>
) )

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)
@ -42,11 +38,11 @@ function getJSON(path, headers) {
function getZones(domains) { function getZones(domains) {
return Promise.all( return Promise.all(
(Array.isArray(domains) ? domains : [ domains ]).map(function (domain) { (Array.isArray(domains) ? domains : [domains]).map(function(domain) {
return getJSON(`/zones?name=${domain}`) return getJSON(`/zones?name=${domain}`)
}) })
).then(function (results) { ).then(function(results) {
return results.reduce(function (memo, zones) { return results.reduce(function(memo, zones) {
return memo.concat(zones) return memo.concat(zones)
}) })
}) })
@ -68,26 +64,36 @@ 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)
}) })
} }
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

@ -4,7 +4,7 @@ const db = require('./RedisClient')
const PackageBlacklist = require('./PackageBlacklist').blacklist const PackageBlacklist = require('./PackageBlacklist').blacklist
function prunePackages(packagesMap) { function prunePackages(packagesMap) {
PackageBlacklist.forEach(function (packageName) { PackageBlacklist.forEach(function(packageName) {
delete packagesMap[packageName] delete packagesMap[packageName]
}) })
@ -33,8 +33,8 @@ function createScoresMap(array) {
} }
function getScoresMap(key, n = 100) { function getScoresMap(key, n = 100) {
return new Promise(function (resolve, reject) { return new Promise(function(resolve, reject) {
db.zrevrange(key, 0, n, 'withscores', function (error, value) { db.zrevrange(key, 0, n, 'withscores', function(error, value) {
if (error) { if (error) {
reject(error) reject(error)
} else { } else {
@ -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) {
@ -63,7 +67,7 @@ function addDailyMetricsToTimeseries(timeseries) {
getPackageRequests(since), getPackageRequests(since),
getPackageBandwidth(since), getPackageBandwidth(since),
getProtocolRequests(since) getProtocolRequests(since)
]).then(function (results) { ]).then(function(results) {
timeseries.requests.package = results[0] timeseries.requests.package = results[0]
timeseries.bandwidth.package = results[1] timeseries.bandwidth.package = results[1]
timeseries.requests.protocol = results[2] timeseries.requests.protocol = results[2]
@ -72,8 +76,8 @@ function addDailyMetricsToTimeseries(timeseries) {
} }
function sumMaps(maps) { function sumMaps(maps) {
return maps.reduce(function (memo, map) { return maps.reduce(function(memo, map) {
Object.keys(map).forEach(function (key) { Object.keys(map).forEach(function(key) {
memo[key] = (memo[key] || 0) + map[key] memo[key] = (memo[key] || 0) + map[key]
}) })
@ -82,29 +86,29 @@ 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
}) })
) )
result.totals.bandwidth.package = sumMaps( result.totals.bandwidth.package = sumMaps(
result.timeseries.map(function (timeseries) { result.timeseries.map(function(timeseries) {
return timeseries.bandwidth.package return timeseries.bandwidth.package
}) })
) )
result.totals.requests.protocol = sumMaps( result.totals.requests.protocol = sumMaps(
result.timeseries.map(function (timeseries) { result.timeseries.map(function(timeseries) {
return timeseries.requests.protocol return timeseries.requests.protocol
}) })
) )
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,10 +162,9 @@ 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)
}, callback) }, callback)
} }

View File

@ -4,18 +4,23 @@ const npmSearch = require('./npm/search')
function createSearchServer() { function createSearchServer() {
const app = express() const app = express()
app.get('/', function (req, res) { app.get('/', function(req, res) {
const { query, page = 0 } = req.query const { query, page = 0 } = req.query
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

@ -1,29 +1,35 @@
const request = require('supertest') const request = require('supertest')
const createServer = require('./createServer') const createServer = require('./createServer')
describe('The server app', function () { describe('The server app', function() {
let app let app
beforeEach(function () { beforeEach(function() {
app = createServer() app = createServer()
}) })
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()
}) })
}) })
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,37 +16,43 @@ 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' })
StatsServer.getStats(since, until, function (error, stats) { StatsServer.getStats(since, until, function(error, stats) {
if (error) { if (error) {
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) {
if (error) { if (error) {
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

@ -37,7 +37,7 @@ const oneMinute = oneSecond * 60
const oneHour = oneMinute * 60 const oneHour = oneMinute * 60
function computeCounters(stream) { function computeCounters(stream) {
return new Promise(function (resolve, reject) { return new Promise(function(resolve, reject) {
const counters = {} const counters = {}
const expireat = {} const expireat = {}
@ -49,7 +49,7 @@ function computeCounters(stream) {
stream stream
.on('error', reject) .on('error', reject)
.on('data', function (entry) { .on('data', function(entry) {
const date = new Date(Math.round(entry.timestamp / 1000000)) const date = new Date(Math.round(entry.timestamp / 1000000))
const nextDay = startOfDay(addDays(date, 1)) const nextDay = startOfDay(addDays(date, 1))
@ -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,32 +98,36 @@ 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() {
resolve({ counters, expireat }) resolve({ counters, expireat })
}) })
}) })
} }
function processLogs(stream) { function processLogs(stream) {
return computeCounters(stream).then(function ({ counters, expireat }) { return computeCounters(stream).then(function({ counters, expireat }) {
Object.keys(counters).forEach(function (key) { Object.keys(counters).forEach(function(key) {
const values = counters[key] const values = counters[key]
Object.keys(values).forEach(function (member) { Object.keys(values).forEach(function(member) {
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])
}) })
}) })
} }
function ingestLogs(zone, startSeconds, endSeconds) { function ingestLogs(zone, startSeconds, endSeconds) {
return new Promise(function (resolve) { return new Promise(function(resolve) {
console.log( console.log(
'info: Started ingesting logs for %s from %s to %s', 'info: Started ingesting logs for %s from %s to %s',
zone.name, zone.name,
@ -121,7 +138,7 @@ function ingestLogs(zone, startSeconds, endSeconds) {
const startFetchTime = Date.now() const startFetchTime = Date.now()
resolve( resolve(
cf.getLogs(zone.id, startSeconds, endSeconds).then(function (stream) { cf.getLogs(zone.id, startSeconds, endSeconds).then(function(stream) {
const endFetchTime = Date.now() const endFetchTime = Date.now()
console.log( console.log(
@ -133,7 +150,7 @@ function ingestLogs(zone, startSeconds, endSeconds) {
const startProcessTime = Date.now() const startProcessTime = Date.now()
return processLogs(stream).then(function () { return processLogs(stream).then(function() {
const endProcessTime = Date.now() const endProcessTime = Date.now()
console.log( console.log(
@ -149,10 +166,13 @@ 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) {
let startSeconds = value && parseInt(value, 10) let startSeconds = value && parseInt(value, 10)
const now = Date.now() const now = Date.now()
@ -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)
} }
@ -203,8 +226,8 @@ function startZone(zone) {
takeATurn() takeATurn()
} }
Promise.all(DomainNames.map(cf.getZones)).then(function (results) { Promise.all(DomainNames.map(cf.getZones)).then(function(results) {
const zones = results.reduce(function (memo, zones) { const zones = results.reduce(function(memo, zones) {
return memo.concat(zones) return memo.concat(zones)
}) })

View File

@ -1,8 +1,11 @@
function checkBlacklist(blacklist) { 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

@ -13,18 +13,18 @@ function getBasename(file) {
/** /**
* File extensions to look for when automatically resolving. * File extensions to look for when automatically resolving.
*/ */
const FindExtensions = [ '', '.js', '.json' ] const FindExtensions = ['', '.js', '.json']
/** /**
* Resolves a path like "lib/file" into "lib/file.js" or "lib/file.json" * Resolves a path like "lib/file" into "lib/file.js" or "lib/file.json"
* depending on which one is available, similar to require('lib/file'). * depending on which one is available, similar to require('lib/file').
*/ */
function findFile(base, useIndex, callback) { function findFile(base, useIndex, callback) {
FindExtensions.reduceRight(function (next, ext) { FindExtensions.reduceRight(function(next, ext) {
const file = base + ext const file = base + ext
return function () { return function() {
fs.stat(file, function (error, stats) { fs.stat(file, function(error, stats) {
if (error) { if (error) {
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') {
next() next()
@ -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) {
@ -55,14 +59,20 @@ function findFile(base, useIndex, callback) {
* trailing slash. * trailing slash.
*/ */
function fetchFile(req, res, next) { 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
@ -70,10 +80,13 @@ function fetchFile(req, res, next) {
// A valid request for a package we haven't downloaded yet. // A valid request for a package we haven't downloaded yet.
req.packageConfig = req.packageInfo.versions[req.packageVersion] req.packageConfig = req.packageInfo.versions[req.packageVersion]
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

@ -19,9 +19,8 @@ function queryIsKnown(query) {
function sanitizeQuery(query) { 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

@ -18,36 +18,43 @@ const AutoIndex = !process.env.DISABLE_INDEX
const MaximumDepth = 128 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) {
callback(error, result && result.code) callback(error, result && result.code)
}) })
} }
} }
/** /**
* 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,39 +63,52 @@ 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
) )
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 {
// Serve some other static file. // Serve some other static file.
const tags = [ 'file' ] const tags = ['file']
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,13 +116,13 @@ 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(',')
}) })
const stream = fs.createReadStream(file) const stream = fs.createReadStream(file)
stream.on('error', function (error) { stream.on('error', function(error) {
console.error(`Cannot send file ${req.packageSpec}${req.filename}`) console.error(`Cannot send file ${req.packageSpec}${req.filename}`)
console.error(error) console.error(error)
res.sendStatus(500) res.sendStatus(500)
@ -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

@ -10,7 +10,7 @@ function createCache(keyPrefix) {
} }
function get(key, callback) { function get(key, callback) {
db.get(createKey(key), function (error, value) { db.get(createKey(key), function(error, value) {
callback(error, value && JSON.parse(value)) callback(error, value && JSON.parse(value))
}) })
} }

View File

@ -1,16 +1,19 @@
function createMutex(doWork) { function createMutex(doWork) {
const mutex = {} const mutex = {}
return function (key, payload, callback) { return function(key, payload, callback) {
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) {
callback(error, value) callback(error, value)
}) })
}) })

View File

@ -1,7 +1,7 @@
function createSearch(query) { function createSearch(query) {
const params = [] const params = []
Object.keys(query).forEach(function (param) { Object.keys(query).forEach(function(param) {
if (query[param] === '') { if (query[param] === '') {
params.push(param) // Omit the trailing "=" from param= params.push(param) // Omit the trailing "=" from param=
} else { } else {

View File

@ -8,16 +8,16 @@ const IndexPage = require('../components/IndexPage')
const e = React.createElement const e = React.createElement
function getEntries(dir) { function getEntries(dir) {
return new Promise(function (resolve, reject) { return new Promise(function(resolve, reject) {
fs.readdir(dir, function (error, files) { fs.readdir(dir, function(error, files) {
if (error) { if (error) {
reject(error) reject(error)
} else { } else {
resolve( resolve(
Promise.all( Promise.all(
files.map(file => getFileStats(path.join(dir, file))) files.map(file => getFileStats(path.join(dir, file)))
).then(function (statsArray) { ).then(function(statsArray) {
return statsArray.map(function (stats, index) { return statsArray.map(function(stats, index) {
return { file: files[index], stats } return { file: files[index], stats }
}) })
}) })

View File

@ -6,20 +6,27 @@ const getFileStats = require('./getFileStats')
const getFileType = require('./getFileType') const getFileType = require('./getFileType')
function getEntries(dir, file, maximumDepth) { function getEntries(dir, file, maximumDepth) {
return new Promise(function (resolve, reject) { return new Promise(function(resolve, reject) {
fs.readdir(path.join(dir, file), function (error, files) { fs.readdir(path.join(dir, file), function(error, files) {
if (error) { if (error) {
reject(error) reject(error)
} else { } else {
resolve( resolve(
Promise.all( Promise.all(
files.map(function (f) { files.map(function(f) {
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
)
})
)
}) })
) )
} }
@ -32,12 +39,12 @@ function formatTime(time) {
} }
function getIntegrity(file) { function getIntegrity(file) {
return new Promise(function (resolve, reject) { return new Promise(function(resolve, reject) {
fs.readFile(file, function (error, data) { fs.readFile(file, function(error, data) {
if (error) { if (error) {
reject(error) reject(error)
} else { } else {
resolve(SRIToolbox.generate({ algorithms: [ 'sha384' ] }, data)) resolve(SRIToolbox.generate({ algorithms: ['sha384'] }, data))
} }
}) })
}) })
@ -53,7 +60,7 @@ function getMetadataRecursive(dir, file, stats, maximumDepth) {
} }
if (stats.isFile()) { if (stats.isFile()) {
return getIntegrity(path.join(dir, file)).then(function (integrity) { return getIntegrity(path.join(dir, file)).then(function(integrity) {
metadata.integrity = integrity metadata.integrity = integrity
return metadata return metadata
}) })
@ -62,16 +69,19 @@ function getMetadataRecursive(dir, file, stats, maximumDepth) {
if (!stats.isDirectory() || maximumDepth === 0) if (!stats.isDirectory() || maximumDepth === 0)
return Promise.resolve(metadata) return Promise.resolve(metadata)
return getEntries(dir, file, maximumDepth).then(function (files) { return getEntries(dir, file, maximumDepth).then(function(files) {
metadata.files = files metadata.files = files
return metadata return metadata
}) })
} }
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

@ -26,7 +26,7 @@ function ignoreSymlinks(file, headers) {
} }
function extractResponse(response, outputDir) { function extractResponse(response, outputDir) {
return new Promise(function (resolve, reject) { return new Promise(function(resolve, reject) {
const extract = tar.extract(outputDir, { const extract = tar.extract(outputDir, {
readable: true, // All dirs/files should be readable. readable: true, // All dirs/files should be readable.
map: stripNamePrefix, map: stripNamePrefix,
@ -44,24 +44,24 @@ function extractResponse(response, outputDir) {
function fetchAndExtract(tarballURL, outputDir) { function fetchAndExtract(tarballURL, outputDir) {
console.log(`info: Fetching ${tarballURL} and extracting to ${outputDir}`) console.log(`info: Fetching ${tarballURL} and extracting to ${outputDir}`)
return fetch(tarballURL).then(function (response) { return fetch(tarballURL).then(function(response) {
return extractResponse(response, outputDir) return extractResponse(response, outputDir)
}) })
} }
const fetchMutex = createMutex(function (payload, callback) { const fetchMutex = createMutex(function(payload, callback) {
const { tarballURL, outputDir } = payload const { tarballURL, outputDir } = payload
fs.access(outputDir, function (error) { fs.access(outputDir, function(error) {
if (error) { if (error) {
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') {
// ENOENT or ENOTDIR are to be expected when we haven't yet // ENOENT or ENOTDIR are to be expected when we haven't yet
// fetched a package for the first time. Carry on! // fetched a package for the first time. Carry on!
mkdirp(outputDir, function (error) { mkdirp(outputDir, function(error) {
if (error) { if (error) {
callback(error) callback(error)
} else { } else {
fetchAndExtract(tarballURL, outputDir).then(function () { fetchAndExtract(tarballURL, outputDir).then(function() {
callback() callback()
}, callback) }, callback)
} }
@ -80,7 +80,7 @@ function getPackage(packageConfig, callback) {
const tarballURL = packageConfig.dist.tarball const tarballURL = packageConfig.dist.tarball
const outputDir = createTempPath(packageConfig.name, packageConfig.version) const outputDir = createTempPath(packageConfig.name, packageConfig.version)
fetchMutex(tarballURL, { tarballURL, outputDir }, function (error) { fetchMutex(tarballURL, { tarballURL, outputDir }, function(error) {
callback(error, outputDir) callback(error, outputDir)
}) })
} }

View File

@ -20,9 +20,9 @@ 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()
}) })
} }
@ -31,32 +31,35 @@ 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.
// In the worst case, a brand new package's info will be // In the worst case, a brand new package's info will be
// available within 5 minutes. // available within 5 minutes.
PackageInfoCache.set(packageName, PackageNotFound, 300, function () { PackageInfoCache.set(packageName, PackageNotFound, 300, function() {
callback(null, value) callback(null, value)
}) })
} else { } else {
// Cache valid package info for 1 minute. // Cache valid package info for 1 minute.
PackageInfoCache.set(packageName, value, 60, function () { PackageInfoCache.set(packageName, value, 60, function() {
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) {
PackageInfoCache.get(packageName, function (error, value) { PackageInfoCache.get(packageName, function(error, value) {
if (error || value != null) { if (error || value != null) {
callback(error, value === PackageNotFound ? null : value) callback(error, value === PackageNotFound ? null : value)
} else { } else {

View File

@ -10,169 +10,96 @@
* 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': [[null, '/angular-animate.min.js']],
'angular-cookies': [[null, '/angular-cookies.min.js']],
'angular-resource': [[null, '/angular-resource.min.js']],
'angular-sanitize': [[null, '/angular-sanitize.min.js']],
'angular-ui-bootstrap': [[null, '/dist/ui-bootstrap.js']],
'animate.css': [[null, '/animate.min.css']],
'babel-standalone': [[null, '/babel.min.js']],
backbone: [[null, '/backbone-min.js']],
bootstrap: [
[null, '/dist/css/bootstrap.min.css', '/dist/js/bootstrap.min.js']
], ],
'angular-animate': [ 'bootstrap-sass': [[null, '/assets/javascripts/bootstrap.min.js']],
[ null, '/angular-animate.min.js' ]
],
'angular-cookies': [ bulma: [[null, '/css/bulma.css']],
[ null, '/angular-cookies.min.js' ]
],
'angular-resource': [ 'core.js': [[null, '/dist/core.min.js']],
[ null, '/angular-resource.min.js' ]
],
'angular-sanitize': [ 'create-react-class': [[null, '/create-react-class.min.js']],
[ null, '/angular-sanitize.min.js' ]
],
'angular-ui-bootstrap': [ d3: [[null, '/build/d3.min.js']],
[ null, '/dist/ui-bootstrap.js' ]
],
'animate.css': [ 'ember-source': [[null, '/dist/ember.min.js']],
[ null, '/animate.min.css' ]
],
'babel-standalone': [
[ null, '/babel.min.js' ]
],
'backbone': [
[ null, '/backbone-min.js' ]
],
'bootstrap': [
[ null, '/dist/css/bootstrap.min.css', '/dist/js/bootstrap.min.js' ]
],
'bootstrap-sass': [
[ null, '/assets/javascripts/bootstrap.min.js' ]
],
'bulma': [
[ null, '/css/bulma.css' ]
],
'core.js': [
[ null, '/dist/core.min.js' ]
],
'create-react-class': [
[ null, '/create-react-class.min.js' ]
],
'd3': [
[ null, '/build/d3.min.js' ]
],
'ember-source': [
[ 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: [[null, '/dist/handlebars.min.js']],
jquery: [[null, '/dist/jquery.min.js']],
fastclick: [[null, '/lib/fastclick.js']],
lodash: [['<3', '/dist/lodash.min.js'], [null, '/lodash.min.js']],
'masonry-layout': [[null, '/dist/masonry.pkgd.min.js']],
'materialize-css': [[null, '/dist/css/materialize.min.css']],
'ngx-bootstrap': [[null, '/bundles/ngx-bootstrap.umd.js']],
react: [
['>=16.0.0-alpha.7', '/umd/react.production.min.js'],
[null, '/dist/react.min.js']
], ],
'handlebars': [ 'react-bootstrap': [[null, '/dist/react-bootstrap.min.js']],
[ null, '/dist/handlebars.min.js' ]
],
'jquery': [
[ null, '/dist/jquery.min.js' ]
],
'fastclick': [
[ null, '/lib/fastclick.js' ]
],
'lodash': [
[ '<3', '/dist/lodash.min.js' ],
[ null, '/lodash.min.js' ]
],
'masonry-layout': [
[ null, '/dist/masonry.pkgd.min.js' ]
],
'materialize-css': [
[ null, '/dist/css/materialize.min.css' ]
],
'ngx-bootstrap': [
[ null, '/bundles/ngx-bootstrap.umd.js' ]
],
'react': [
[ '>=16.0.0-alpha.7', '/umd/react.production.min.js' ],
[ null, '/dist/react.min.js' ]
],
'react-bootstrap': [
[ 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'],
[ null, '/dist/react-dom.min.js' ] [null, '/dist/react-dom.min.js']
], ],
'react-router': [ 'react-router': [
[ '>=4.0.0', '/umd/react-router.min.js' ], ['>=4.0.0', '/umd/react-router.min.js'],
[ 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

@ -5,11 +5,10 @@ function getAssetPaths(packageName, version) {
const entries = assetPathsIndex[packageName] const entries = assetPathsIndex[packageName]
if (entries) { if (entries) {
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

@ -2,13 +2,13 @@ const searchIndex = require('./searchIndex')
const getAssetPaths = require('./getAssetPaths') const getAssetPaths = require('./getAssetPaths')
function enhanceHit(hit) { function enhanceHit(hit) {
return new Promise(function (resolve, reject) { return new Promise(function(resolve, reject) {
const assetPaths = getAssetPaths(hit.name, hit.version) const assetPaths = getAssetPaths(hit.name, hit.version)
if (assetPaths) { if (assetPaths) {
// TODO: Double check the package metadata to ensure the files // TODO: Double check the package metadata to ensure the files
// haven't moved from the paths in the index? // haven't moved from the paths in the index?
hit.assets = assetPaths.map(function (path) { hit.assets = assetPaths.map(function(path) {
return `https://unpkg.com/${hit.name}@${hit.version}${path}` return `https://unpkg.com/${hit.name}@${hit.version}${path}`
}) })
@ -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)
} }
@ -32,13 +30,13 @@ function concat(string) {
} }
function search(query, page) { function search(query, page) {
return new Promise(function (resolve, reject) { return new Promise(function(resolve, reject) {
const hitsPerPage = 10 const hitsPerPage = 10
const params = { const params = {
// typoTolerance: 'min', // typoTolerance: 'min',
// optionalFacetFilters: `concatenatedName:${concat(query)}`, // optionalFacetFilters: `concatenatedName:${concat(query)}`,
facets: [ 'keywords' ], facets: ['keywords'],
attributesToHighlight: null, attributesToHighlight: null,
attributesToRetrieve: [ attributesToRetrieve: [
'description', 'description',
@ -58,14 +56,12 @@ function search(query, page) {
page page
} }
searchIndex.search(query, params, function (error, value) { searchIndex.search(query, params, function(error, value) {
if (error) { if (error) {
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' }