Prettier everything up
This commit is contained in:
parent
f3e041ace6
commit
2d57d96e62
|
@ -1,7 +1,8 @@
|
|||
import React from 'react'
|
||||
import contentHTML from './About.md'
|
||||
|
||||
const About = () =>
|
||||
<div className="wrapper" dangerouslySetInnerHTML={{ __html: contentHTML }}/>
|
||||
const About = () => (
|
||||
<div className="wrapper" dangerouslySetInnerHTML={{ __html: contentHTML }} />
|
||||
)
|
||||
|
||||
export default About
|
||||
|
|
|
@ -2,9 +2,10 @@ import React from 'react'
|
|||
import { HashRouter } from 'react-router-dom'
|
||||
import Layout from './Layout'
|
||||
|
||||
const App = () =>
|
||||
const App = () => (
|
||||
<HashRouter>
|
||||
<Layout/>
|
||||
<Layout />
|
||||
</HashRouter>
|
||||
)
|
||||
|
||||
export default App
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react'
|
||||
import contentHTML from './Home.md'
|
||||
|
||||
const Home = () =>
|
||||
<div className="wrapper" dangerouslySetInnerHTML={{ __html: contentHTML }}/>
|
||||
const Home = () => (
|
||||
<div className="wrapper" dangerouslySetInnerHTML={{ __html: contentHTML }} />
|
||||
)
|
||||
|
||||
export default Home
|
||||
|
|
|
@ -54,8 +54,7 @@ class Layout extends React.Component {
|
|||
if (window.localStorage) {
|
||||
const savedStats = window.localStorage.savedStats
|
||||
|
||||
if (savedStats)
|
||||
this.setState({ stats: JSON.parse(savedStats) })
|
||||
if (savedStats) this.setState({ stats: JSON.parse(savedStats) })
|
||||
|
||||
window.onbeforeunload = () => {
|
||||
localStorage.savedStats = JSON.stringify(this.state.stats)
|
||||
|
@ -72,21 +71,32 @@ class Layout extends React.Component {
|
|||
const { underlineLeft, underlineWidth, useSpring } = this.state
|
||||
|
||||
const style = {
|
||||
left: useSpring ? spring(underlineLeft, { stiffness: 220 }) : underlineLeft,
|
||||
left: useSpring
|
||||
? spring(underlineLeft, { stiffness: 220 })
|
||||
: underlineLeft,
|
||||
width: useSpring ? spring(underlineWidth) : underlineWidth
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<WindowSize onChange={this.adjustUnderline}/>
|
||||
<WindowSize onChange={this.adjustUnderline} />
|
||||
<div className="wrapper">
|
||||
<header>
|
||||
<h1 className="layout-title">unpkg</h1>
|
||||
<nav className="layout-nav">
|
||||
<ol className="layout-nav-list" ref={node => this.listNode = node}>
|
||||
<li><Link to="/">Home</Link></li>
|
||||
<li><Link to="/stats">Stats</Link></li>
|
||||
<li><Link to="/about">About</Link></li>
|
||||
<ol
|
||||
className="layout-nav-list"
|
||||
ref={node => (this.listNode = node)}
|
||||
>
|
||||
<li>
|
||||
<Link to="/">Home</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/stats">Stats</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/about">About</Link>
|
||||
</li>
|
||||
</ol>
|
||||
<Motion
|
||||
defaultStyle={{ left: underlineLeft, width: underlineWidth }}
|
||||
|
@ -107,9 +117,12 @@ class Layout extends React.Component {
|
|||
</div>
|
||||
|
||||
<Switch>
|
||||
<Route path="/stats" render={() => <Stats data={this.state.stats}/>}/>
|
||||
<Route path="/about" component={About}/>
|
||||
<Route path="/" component={Home}/>
|
||||
<Route
|
||||
path="/stats"
|
||||
render={() => <Stats data={this.state.stats} />}
|
||||
/>
|
||||
<Route path="/about" component={About} />
|
||||
<Route path="/" component={Home} />
|
||||
</Switch>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -24,12 +24,11 @@ class NumberTextInput extends React.Component {
|
|||
this.setState({ value: this.props.value })
|
||||
}
|
||||
|
||||
handleChange = (event) => {
|
||||
handleChange = event => {
|
||||
const value = this.props.parseNumber(event.target.value)
|
||||
|
||||
this.setState({ value }, () => {
|
||||
if (this.props.onChange)
|
||||
this.props.onChange(value)
|
||||
if (this.props.onChange) this.props.onChange(value)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -39,7 +38,12 @@ class NumberTextInput extends React.Component {
|
|||
const displayValue = formatNumber(value)
|
||||
|
||||
return (
|
||||
<input {...props} type="text" value={displayValue} onChange={this.handleChange}/>
|
||||
<input
|
||||
{...props}
|
||||
type="text"
|
||||
value={displayValue}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
200
client/Stats.js
200
client/Stats.js
|
@ -8,13 +8,15 @@ import formatPercent from './utils/formatPercent'
|
|||
|
||||
import { continents, countries } from 'countries-list'
|
||||
|
||||
const getCountriesByContinent = (continent) =>
|
||||
Object.keys(countries).filter(country => countries[country].continent === continent)
|
||||
const getCountriesByContinent = continent =>
|
||||
Object.keys(countries).filter(
|
||||
country => countries[country].continent === continent
|
||||
)
|
||||
|
||||
const sumKeyValues = (hash, keys) =>
|
||||
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)
|
||||
|
||||
class Stats extends React.Component {
|
||||
|
@ -30,8 +32,7 @@ class Stats extends React.Component {
|
|||
render() {
|
||||
const { data } = this.props
|
||||
|
||||
if (data == null)
|
||||
return null
|
||||
if (data == null) return null
|
||||
|
||||
const totals = data.totals
|
||||
|
||||
|
@ -42,25 +43,43 @@ class Stats extends React.Component {
|
|||
// Packages
|
||||
const packageRows = []
|
||||
|
||||
Object.keys(totals.requests.package).sort((a, b) => {
|
||||
return totals.requests.package[b] - totals.requests.package[a]
|
||||
}).forEach(packageName => {
|
||||
const requests = totals.requests.package[packageName]
|
||||
const bandwidth = totals.bandwidth.package[packageName]
|
||||
Object.keys(totals.requests.package)
|
||||
.sort((a, b) => {
|
||||
return totals.requests.package[b] - totals.requests.package[a]
|
||||
})
|
||||
.forEach(packageName => {
|
||||
const requests = totals.requests.package[packageName]
|
||||
const bandwidth = totals.bandwidth.package[packageName]
|
||||
|
||||
if (requests >= this.state.minPackageRequests) {
|
||||
packageRows.push(
|
||||
<tr key={packageName}>
|
||||
<td><a href={`https://npmjs.org/package/${packageName}`} title={`${packageName} on npm`}>{packageName}</a></td>
|
||||
<td>{formatNumber(requests)} ({formatPercent(requests / totals.requests.all)}%)</td>
|
||||
{bandwidth
|
||||
? <td>{formatBytes(bandwidth)} ({formatPercent(bandwidth / totals.bandwidth.all)}%)</td>
|
||||
: <td>-</td>
|
||||
}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
})
|
||||
if (requests >= this.state.minPackageRequests) {
|
||||
packageRows.push(
|
||||
<tr key={packageName}>
|
||||
<td>
|
||||
<a
|
||||
href={`https://npmjs.org/package/${packageName}`}
|
||||
title={`${packageName} on npm`}
|
||||
>
|
||||
{packageName}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{formatNumber(requests)} ({formatPercent(
|
||||
requests / totals.requests.all
|
||||
)}%)
|
||||
</td>
|
||||
{bandwidth ? (
|
||||
<td>
|
||||
{formatBytes(bandwidth)} ({formatPercent(
|
||||
bandwidth / totals.bandwidth.all
|
||||
)}%)
|
||||
</td>
|
||||
) : (
|
||||
<td>-</td>
|
||||
)}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Regions
|
||||
const regionRows = []
|
||||
|
@ -85,12 +104,23 @@ class Stats extends React.Component {
|
|||
const continentName = continents[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(
|
||||
<tr key={continent} className="continent-row">
|
||||
<td>{continentName}</td>
|
||||
<td>{formatNumber(continentData.requests)} ({formatPercent(continentData.requests / totals.requests.all)}%)</td>
|
||||
<td>{formatBytes(continentData.bandwidth)} ({formatPercent(continentData.bandwidth / totals.bandwidth.all)}%)</td>
|
||||
<td>
|
||||
{formatNumber(continentData.requests)} ({formatPercent(
|
||||
continentData.requests / totals.requests.all
|
||||
)}%)
|
||||
</td>
|
||||
<td>
|
||||
{formatBytes(continentData.bandwidth)} ({formatPercent(
|
||||
continentData.bandwidth / totals.bandwidth.all
|
||||
)}%)
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
|
@ -106,8 +136,16 @@ class Stats extends React.Component {
|
|||
regionRows.push(
|
||||
<tr key={continent + country} className="country-row">
|
||||
<td className="country-name">{countries[country].name}</td>
|
||||
<td>{formatNumber(countryRequests)} ({formatPercent(countryRequests / totals.requests.all)}%)</td>
|
||||
<td>{formatBytes(countryBandwidth)} ({formatPercent(countryBandwidth / totals.bandwidth.all)}%)</td>
|
||||
<td>
|
||||
{formatNumber(countryRequests)} ({formatPercent(
|
||||
countryRequests / totals.requests.all
|
||||
)}%)
|
||||
</td>
|
||||
<td>
|
||||
{formatBytes(countryBandwidth)} ({formatPercent(
|
||||
countryBandwidth / totals.bandwidth.all
|
||||
)}%)
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
@ -116,30 +154,58 @@ class Stats extends React.Component {
|
|||
})
|
||||
|
||||
// Protocols
|
||||
const protocolRows = Object.keys(totals.requests.protocol).sort((a, b) => {
|
||||
return totals.requests.protocol[b] - totals.requests.protocol[a]
|
||||
}).map(protocol => {
|
||||
const requests = totals.requests.protocol[protocol]
|
||||
const protocolRows = Object.keys(totals.requests.protocol)
|
||||
.sort((a, b) => {
|
||||
return totals.requests.protocol[b] - totals.requests.protocol[a]
|
||||
})
|
||||
.map(protocol => {
|
||||
const requests = totals.requests.protocol[protocol]
|
||||
|
||||
return (
|
||||
<tr key={protocol}>
|
||||
<td>{protocol}</td>
|
||||
<td>{formatNumber(requests)} ({formatPercent(requests / sumValues(totals.requests.protocol))}%)</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<tr key={protocol}>
|
||||
<td>{protocol}</td>
|
||||
<td>
|
||||
{formatNumber(requests)} ({formatPercent(
|
||||
requests / sumValues(totals.requests.protocol)
|
||||
)}%)
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="wrapper">
|
||||
<p>From <strong>{formatDate(since, 'MMM D')}</strong> to <strong>{formatDate(until, 'MMM D')}</strong> unpkg served <strong>{formatNumber(totals.requests.all)}</strong> requests and a total of <strong>{formatBytes(totals.bandwidth.all)}</strong> of data to <strong>{formatNumber(totals.uniques.all)}</strong> unique visitors, <strong>{formatPercent(totals.requests.cached / totals.requests.all, 0)}%</strong> of which were served from the cache.</p>
|
||||
<p>
|
||||
From <strong>{formatDate(since, 'MMM D')}</strong> to{' '}
|
||||
<strong>{formatDate(until, 'MMM D')}</strong> unpkg served{' '}
|
||||
<strong>{formatNumber(totals.requests.all)}</strong> requests and a
|
||||
total of <strong>{formatBytes(totals.bandwidth.all)}</strong> of data
|
||||
to <strong>{formatNumber(totals.uniques.all)}</strong> unique
|
||||
visitors,{' '}
|
||||
<strong>
|
||||
{formatPercent(totals.requests.cached / totals.requests.all, 0)}%
|
||||
</strong>{' '}
|
||||
of which were served from the cache.
|
||||
</p>
|
||||
|
||||
<h3>Packages</h3>
|
||||
|
||||
<p>The table below shows the most popular packages served by unpkg from <strong>{formatDate(since, 'MMM D')}</strong> to <strong>{formatDate(until, 'MMM D')}</strong>. Only the top {Object.keys(totals.requests.package).length} packages are shown.</p>
|
||||
<p>
|
||||
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}
|
||||
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="1000">1,000</option>
|
||||
|
@ -147,7 +213,8 @@ class Stats extends React.Component {
|
|||
<option value="100000">100,000</option>
|
||||
<option value="1000000">1,000,000</option>
|
||||
<option value="10000000">10,000,000</option>
|
||||
</select> requests.
|
||||
</select>{' '}
|
||||
requests.
|
||||
</p>
|
||||
|
||||
<table cellSpacing="0" cellPadding="0" style={{ width: '100%' }}>
|
||||
|
@ -158,28 +225,42 @@ class Stats extends React.Component {
|
|||
<th>Bandwidth (% of total)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{packageRows}
|
||||
</tbody>
|
||||
<tbody>{packageRows}</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Regions</h3>
|
||||
|
||||
<p>The table below breaks down requests to unpkg from <strong>{formatDate(since, 'MMM D')}</strong> to <strong>{formatDate(until, 'MMM D')}</strong> by geographic region.</p>
|
||||
<p>
|
||||
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}
|
||||
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="100000">100,000</option>
|
||||
<option value="1000000">1,000,000</option>
|
||||
<option value="10000000">10,000,000</option>
|
||||
<option value="100000000">100,000,000</option>
|
||||
</select> requests.
|
||||
</select>{' '}
|
||||
requests.
|
||||
</p>
|
||||
|
||||
<table cellSpacing="0" cellPadding="0" style={{ width: '100%' }} className="regions-table">
|
||||
<table
|
||||
cellSpacing="0"
|
||||
cellPadding="0"
|
||||
style={{ width: '100%' }}
|
||||
className="regions-table"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Region</th>
|
||||
|
@ -187,14 +268,16 @@ class Stats extends React.Component {
|
|||
<th>Bandwidth (% of total)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{regionRows}
|
||||
</tbody>
|
||||
<tbody>{regionRows}</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Protocols</h3>
|
||||
|
||||
<p>The table below breaks down requests to unpkg from <strong>{formatDate(since, 'MMM D')}</strong> to <strong>{formatDate(until, 'MMM D')}</strong> by HTTP protocol.</p>
|
||||
<p>
|
||||
The table below breaks down requests to unpkg from{' '}
|
||||
<strong>{formatDate(since, 'MMM D')}</strong> to{' '}
|
||||
<strong>{formatDate(until, 'MMM D')}</strong> by HTTP protocol.
|
||||
</p>
|
||||
|
||||
<table cellSpacing="0" cellPadding="0" style={{ width: '100%' }}>
|
||||
<thead>
|
||||
|
@ -203,11 +286,8 @@ class Stats extends React.Component {
|
|||
<th>Requests (% of total)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{protocolRows}
|
||||
</tbody>
|
||||
<tbody>{protocolRows}</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,4 @@ import ReactDOM from 'react-dom'
|
|||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.render(
|
||||
<App />,
|
||||
document.getElementById('root')
|
||||
)
|
||||
ReactDOM.render(<App />, document.getElementById('root'))
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
const formatNumber = (n) => {
|
||||
const formatNumber = n => {
|
||||
const digits = String(n).split('')
|
||||
const groups = []
|
||||
|
||||
while (digits.length)
|
||||
groups.unshift(digits.splice(-3).join(''))
|
||||
while (digits.length) groups.unshift(digits.splice(-3).join(''))
|
||||
|
||||
return groups.join(',')
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
const parseNumber = (s) =>
|
||||
parseInt(s.replace(/,/g, ''), 10) || 0
|
||||
const parseNumber = s => parseInt(s.replace(/,/g, ''), 10) || 0
|
||||
|
||||
export default parseNumber
|
||||
|
|
|
@ -114,5 +114,9 @@
|
|||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"single-quote": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,15 +7,9 @@ const CloudflareAPIURL = 'https://api.cloudflare.com'
|
|||
const CloudflareEmail = process.env.CLOUDFLARE_EMAIL
|
||||
const CloudflareKey = process.env.CLOUDFLARE_KEY
|
||||
|
||||
invariant(
|
||||
CloudflareEmail,
|
||||
'Missing the $CLOUDFLARE_EMAIL environment variable'
|
||||
)
|
||||
invariant(CloudflareEmail, 'Missing the $CLOUDFLARE_EMAIL environment variable')
|
||||
|
||||
invariant(
|
||||
CloudflareKey,
|
||||
'Missing the $CLOUDFLARE_KEY environment variable'
|
||||
)
|
||||
invariant(CloudflareKey, 'Missing the $CLOUDFLARE_KEY environment variable')
|
||||
|
||||
function get(path, headers) {
|
||||
return fetch(`${CloudflareAPIURL}/client/v4${path}`, {
|
||||
|
@ -27,26 +21,28 @@ function get(path, headers) {
|
|||
}
|
||||
|
||||
function getJSON(path, headers) {
|
||||
return get(path, headers).then(function (res) {
|
||||
return res.json()
|
||||
}).then(function (data) {
|
||||
if (!data.success) {
|
||||
console.error(`CloudflareAPI.getJSON failed at ${path}`)
|
||||
console.error(data)
|
||||
throw new Error('Failed to getJSON from Cloudflare')
|
||||
}
|
||||
return get(path, headers)
|
||||
.then(function(res) {
|
||||
return res.json()
|
||||
})
|
||||
.then(function(data) {
|
||||
if (!data.success) {
|
||||
console.error(`CloudflareAPI.getJSON failed at ${path}`)
|
||||
console.error(data)
|
||||
throw new Error('Failed to getJSON from Cloudflare')
|
||||
}
|
||||
|
||||
return data.result
|
||||
})
|
||||
return data.result
|
||||
})
|
||||
}
|
||||
|
||||
function getZones(domains) {
|
||||
return Promise.all(
|
||||
(Array.isArray(domains) ? domains : [ domains ]).map(function (domain) {
|
||||
(Array.isArray(domains) ? domains : [domains]).map(function(domain) {
|
||||
return getJSON(`/zones?name=${domain}`)
|
||||
})
|
||||
).then(function (results) {
|
||||
return results.reduce(function (memo, zones) {
|
||||
).then(function(results) {
|
||||
return results.reduce(function(memo, zones) {
|
||||
return memo.concat(zones)
|
||||
})
|
||||
})
|
||||
|
@ -68,26 +64,36 @@ function reduceResults(target, values) {
|
|||
|
||||
function getZoneAnalyticsDashboard(zones, since, until) {
|
||||
return Promise.all(
|
||||
(Array.isArray(zones) ? zones : [ zones ]).map(function (zone) {
|
||||
return getJSON(`/zones/${zone.id}/analytics/dashboard?since=${since.toISOString()}&until=${until.toISOString()}`)
|
||||
(Array.isArray(zones) ? zones : [zones]).map(function(zone) {
|
||||
return getJSON(
|
||||
`/zones/${
|
||||
zone.id
|
||||
}/analytics/dashboard?since=${since.toISOString()}&until=${until.toISOString()}`
|
||||
)
|
||||
})
|
||||
).then(function (results) {
|
||||
).then(function(results) {
|
||||
return results.reduce(reduceResults)
|
||||
})
|
||||
}
|
||||
|
||||
function getJSONStream(path, headers) {
|
||||
const acceptGzipHeaders = Object.assign({}, headers, { 'Accept-Encoding': 'gzip' })
|
||||
|
||||
return get(path, acceptGzipHeaders).then(function (res) {
|
||||
return res.body.pipe(gunzip())
|
||||
}).then(function (stream) {
|
||||
return stream.pipe(ndjson.parse())
|
||||
const acceptGzipHeaders = Object.assign({}, headers, {
|
||||
'Accept-Encoding': 'gzip'
|
||||
})
|
||||
|
||||
return get(path, acceptGzipHeaders)
|
||||
.then(function(res) {
|
||||
return res.body.pipe(gunzip())
|
||||
})
|
||||
.then(function(stream) {
|
||||
return stream.pipe(ndjson.parse())
|
||||
})
|
||||
}
|
||||
|
||||
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 = {
|
||||
|
|
|
@ -2,7 +2,8 @@ const redis = require('redis')
|
|||
|
||||
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)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const db = require('./RedisClient')
|
|||
const PackageBlacklist = require('./PackageBlacklist').blacklist
|
||||
|
||||
function prunePackages(packagesMap) {
|
||||
PackageBlacklist.forEach(function (packageName) {
|
||||
PackageBlacklist.forEach(function(packageName) {
|
||||
delete packagesMap[packageName]
|
||||
})
|
||||
|
||||
|
@ -33,8 +33,8 @@ function createScoresMap(array) {
|
|||
}
|
||||
|
||||
function getScoresMap(key, n = 100) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
db.zrevrange(key, 0, n, 'withscores', function (error, value) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
db.zrevrange(key, 0, n, 'withscores', function(error, value) {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
|
@ -45,11 +45,15 @@ function getScoresMap(key, 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) {
|
||||
return getScoresMap(`stats-packageBytes-${createDayKey(date)}`, n).then(prunePackages)
|
||||
return getScoresMap(`stats-packageBytes-${createDayKey(date)}`, n).then(
|
||||
prunePackages
|
||||
)
|
||||
}
|
||||
|
||||
function getProtocolRequests(date) {
|
||||
|
@ -63,7 +67,7 @@ function addDailyMetricsToTimeseries(timeseries) {
|
|||
getPackageRequests(since),
|
||||
getPackageBandwidth(since),
|
||||
getProtocolRequests(since)
|
||||
]).then(function (results) {
|
||||
]).then(function(results) {
|
||||
timeseries.requests.package = results[0]
|
||||
timeseries.bandwidth.package = results[1]
|
||||
timeseries.requests.protocol = results[2]
|
||||
|
@ -72,8 +76,8 @@ function addDailyMetricsToTimeseries(timeseries) {
|
|||
}
|
||||
|
||||
function sumMaps(maps) {
|
||||
return maps.reduce(function (memo, map) {
|
||||
Object.keys(map).forEach(function (key) {
|
||||
return maps.reduce(function(memo, map) {
|
||||
Object.keys(map).forEach(function(key) {
|
||||
memo[key] = (memo[key] || 0) + map[key]
|
||||
})
|
||||
|
||||
|
@ -82,29 +86,29 @@ function sumMaps(maps) {
|
|||
}
|
||||
|
||||
function addDailyMetrics(result) {
|
||||
return Promise.all(
|
||||
result.timeseries.map(addDailyMetricsToTimeseries)
|
||||
).then(function () {
|
||||
result.totals.requests.package = sumMaps(
|
||||
result.timeseries.map(function (timeseries) {
|
||||
return timeseries.requests.package
|
||||
})
|
||||
)
|
||||
return Promise.all(result.timeseries.map(addDailyMetricsToTimeseries)).then(
|
||||
function() {
|
||||
result.totals.requests.package = sumMaps(
|
||||
result.timeseries.map(function(timeseries) {
|
||||
return timeseries.requests.package
|
||||
})
|
||||
)
|
||||
|
||||
result.totals.bandwidth.package = sumMaps(
|
||||
result.timeseries.map(function (timeseries) {
|
||||
return timeseries.bandwidth.package
|
||||
})
|
||||
)
|
||||
result.totals.bandwidth.package = sumMaps(
|
||||
result.timeseries.map(function(timeseries) {
|
||||
return timeseries.bandwidth.package
|
||||
})
|
||||
)
|
||||
|
||||
result.totals.requests.protocol = sumMaps(
|
||||
result.timeseries.map(function (timeseries) {
|
||||
return timeseries.requests.protocol
|
||||
})
|
||||
)
|
||||
result.totals.requests.protocol = sumMaps(
|
||||
result.timeseries.map(function(timeseries) {
|
||||
return timeseries.requests.protocol
|
||||
})
|
||||
)
|
||||
|
||||
return result
|
||||
})
|
||||
return result
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function extractPublicInfo(data) {
|
||||
|
@ -136,19 +140,18 @@ function extractPublicInfo(data) {
|
|||
}
|
||||
}
|
||||
|
||||
const DomainNames = [
|
||||
'unpkg.com',
|
||||
'npmcdn.com'
|
||||
]
|
||||
const DomainNames = ['unpkg.com', 'npmcdn.com']
|
||||
|
||||
function fetchStats(since, until) {
|
||||
return cf.getZones(DomainNames).then(function (zones) {
|
||||
return cf.getZoneAnalyticsDashboard(zones, since, until).then(function (dashboard) {
|
||||
return {
|
||||
timeseries: dashboard.timeseries.map(extractPublicInfo),
|
||||
totals: extractPublicInfo(dashboard.totals)
|
||||
}
|
||||
})
|
||||
return cf.getZones(DomainNames).then(function(zones) {
|
||||
return cf
|
||||
.getZoneAnalyticsDashboard(zones, since, until)
|
||||
.then(function(dashboard) {
|
||||
return {
|
||||
timeseries: dashboard.timeseries.map(extractPublicInfo),
|
||||
totals: extractPublicInfo(dashboard.totals)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -159,10 +162,9 @@ const oneDay = oneHour * 24
|
|||
function getStats(since, until, callback) {
|
||||
let promise = fetchStats(since, until)
|
||||
|
||||
if ((until - since) > oneDay)
|
||||
promise = promise.then(addDailyMetrics)
|
||||
if (until - since > oneDay) promise = promise.then(addDailyMetrics)
|
||||
|
||||
promise.then(function (value) {
|
||||
promise.then(function(value) {
|
||||
callback(null, value)
|
||||
}, callback)
|
||||
}
|
||||
|
|
|
@ -4,18 +4,23 @@ const npmSearch = require('./npm/search')
|
|||
function createSearchServer() {
|
||||
const app = express()
|
||||
|
||||
app.get('/', function (req, res) {
|
||||
app.get('/', function(req, res) {
|
||||
const { query, page = 0 } = req.query
|
||||
|
||||
if (!query)
|
||||
return res.status(403).send({ error: 'Missing ?query parameter' })
|
||||
|
||||
npmSearch(query, page).then(function (result) {
|
||||
res.send(result)
|
||||
}, function (error) {
|
||||
console.error(error)
|
||||
res.status(500).send({ error: 'There was an error executing the search' })
|
||||
})
|
||||
npmSearch(query, page).then(
|
||||
function(result) {
|
||||
res.send(result)
|
||||
},
|
||||
function(error) {
|
||||
console.error(error)
|
||||
res
|
||||
.status(500)
|
||||
.send({ error: 'There was an error executing the search' })
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return app
|
||||
|
|
|
@ -1,32 +1,38 @@
|
|||
const request = require('supertest')
|
||||
const createServer = require('./createServer')
|
||||
|
||||
describe('The server app', function () {
|
||||
describe('The server app', function() {
|
||||
let app
|
||||
beforeEach(function () {
|
||||
beforeEach(function() {
|
||||
app = createServer()
|
||||
})
|
||||
|
||||
it('rejects invalid package names', function (done) {
|
||||
request(app).get('/_invalid/index.js').then(function (res) {
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
})
|
||||
it('rejects invalid package names', function(done) {
|
||||
request(app)
|
||||
.get('/_invalid/index.js')
|
||||
.then(function(res) {
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects invalid query params', function (done) {
|
||||
request(app).get('/react?main=index&invalid').then(function (res) {
|
||||
expect(res.statusCode).toBe(302)
|
||||
expect(res.headers.location).toBe('/react?main=index')
|
||||
done()
|
||||
})
|
||||
it('redirects invalid query params', function(done) {
|
||||
request(app)
|
||||
.get('/react?main=index&invalid')
|
||||
.then(function(res) {
|
||||
expect(res.statusCode).toBe(302)
|
||||
expect(res.headers.location).toBe('/react?main=index')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects /_meta to ?meta', function (done) {
|
||||
request(app).get('/_meta/react?main=index').then(function (res) {
|
||||
expect(res.statusCode).toBe(302)
|
||||
expect(res.headers.location).toBe('/react?main=index&meta')
|
||||
done()
|
||||
})
|
||||
it('redirects /_meta to ?meta', function(done) {
|
||||
request(app)
|
||||
.get('/_meta/react?main=index')
|
||||
.then(function(res) {
|
||||
expect(res.statusCode).toBe(302)
|
||||
expect(res.headers.location).toBe('/react?main=index&meta')
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -5,7 +5,7 @@ const startOfSecond = require('date-fns/start_of_second')
|
|||
const StatsServer = require('./StatsServer')
|
||||
|
||||
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 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' })
|
||||
|
||||
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)
|
||||
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) {
|
||||
console.error(error)
|
||||
res.status(500).send({ error: 'Unable to fetch stats' })
|
||||
} else {
|
||||
res.set({
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Cache-Tag': 'stats'
|
||||
}).send(stats)
|
||||
res
|
||||
.set({
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Cache-Tag': 'stats'
|
||||
})
|
||||
.send(stats)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function servePastDaysStats(days, req, res) {
|
||||
const until = startOfDay(new Date)
|
||||
const until = startOfDay(new Date())
|
||||
const since = subDays(until, days)
|
||||
|
||||
StatsServer.getStats(since, until, function (error, stats) {
|
||||
StatsServer.getStats(since, until, function(error, stats) {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
res.status(500).send({ error: 'Unable to fetch stats' })
|
||||
} else {
|
||||
res.set({
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Cache-Tag': 'stats'
|
||||
}).send(stats)
|
||||
res
|
||||
.set({
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Cache-Tag': 'stats'
|
||||
})
|
||||
.send(stats)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ const oneMinute = oneSecond * 60
|
|||
const oneHour = oneMinute * 60
|
||||
|
||||
function computeCounters(stream) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const counters = {}
|
||||
const expireat = {}
|
||||
|
||||
|
@ -49,7 +49,7 @@ function computeCounters(stream) {
|
|||
|
||||
stream
|
||||
.on('error', reject)
|
||||
.on('data', function (entry) {
|
||||
.on('data', function(entry) {
|
||||
const date = new Date(Math.round(entry.timestamp / 1000000))
|
||||
|
||||
const nextDay = startOfDay(addDays(date, 1))
|
||||
|
@ -66,9 +66,22 @@ function computeCounters(stream) {
|
|||
const url = parsePackageURL(parseURL(clientRequest.uri).pathname)
|
||||
const packageName = url && url.packageName
|
||||
|
||||
if (packageName && validateNPMPackageName(packageName).errors == null) {
|
||||
incr(`stats-packageRequests-${dayKey}`, packageName, 1, thirtyDaysLater)
|
||||
incr(`stats-packageBytes-${dayKey}`, packageName, edgeResponse.bytes, thirtyDaysLater)
|
||||
if (
|
||||
packageName &&
|
||||
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) {
|
||||
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 })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function processLogs(stream) {
|
||||
return computeCounters(stream).then(function ({ counters, expireat }) {
|
||||
Object.keys(counters).forEach(function (key) {
|
||||
return computeCounters(stream).then(function({ counters, expireat }) {
|
||||
Object.keys(counters).forEach(function(key) {
|
||||
const values = counters[key]
|
||||
|
||||
Object.keys(values).forEach(function (member) {
|
||||
Object.keys(values).forEach(function(member) {
|
||||
db.zincrby(key, values[member], member)
|
||||
})
|
||||
|
||||
if (expireat[key])
|
||||
db.expireat(key, expireat[key])
|
||||
if (expireat[key]) db.expireat(key, expireat[key])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function ingestLogs(zone, startSeconds, endSeconds) {
|
||||
return new Promise(function (resolve) {
|
||||
return new Promise(function(resolve) {
|
||||
console.log(
|
||||
'info: Started ingesting logs for %s from %s to %s',
|
||||
zone.name,
|
||||
|
@ -121,7 +138,7 @@ function ingestLogs(zone, startSeconds, endSeconds) {
|
|||
const startFetchTime = Date.now()
|
||||
|
||||
resolve(
|
||||
cf.getLogs(zone.id, startSeconds, endSeconds).then(function (stream) {
|
||||
cf.getLogs(zone.id, startSeconds, endSeconds).then(function(stream) {
|
||||
const endFetchTime = Date.now()
|
||||
|
||||
console.log(
|
||||
|
@ -133,7 +150,7 @@ function ingestLogs(zone, startSeconds, endSeconds) {
|
|||
|
||||
const startProcessTime = Date.now()
|
||||
|
||||
return processLogs(stream).then(function () {
|
||||
return processLogs(stream).then(function() {
|
||||
const endProcessTime = Date.now()
|
||||
|
||||
console.log(
|
||||
|
@ -149,10 +166,13 @@ function ingestLogs(zone, startSeconds, endSeconds) {
|
|||
}
|
||||
|
||||
function startZone(zone) {
|
||||
const startSecondsKey = `ingestLogsWorker-nextStartSeconds-${zone.name.replace('.', '-')}`
|
||||
const startSecondsKey = `ingestLogsWorker-nextStartSeconds-${zone.name.replace(
|
||||
'.',
|
||||
'-'
|
||||
)}`
|
||||
|
||||
function takeATurn() {
|
||||
db.get(startSecondsKey, function (error, value) {
|
||||
db.get(startSecondsKey, function(error, value) {
|
||||
let startSeconds = value && parseInt(value, 10)
|
||||
|
||||
const now = Date.now()
|
||||
|
@ -182,18 +202,21 @@ function startZone(zone) {
|
|||
// 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.
|
||||
// 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) {
|
||||
const endSeconds = startSeconds + LogWindowSeconds
|
||||
|
||||
ingestLogs(zone, startSeconds, endSeconds).then(function () {
|
||||
db.set(startSecondsKey, endSeconds)
|
||||
setTimeout(takeATurn)
|
||||
}, function (error) {
|
||||
console.error(error.stack)
|
||||
process.exit(1)
|
||||
})
|
||||
ingestLogs(zone, startSeconds, endSeconds).then(
|
||||
function() {
|
||||
db.set(startSecondsKey, endSeconds)
|
||||
setTimeout(takeATurn)
|
||||
},
|
||||
function(error) {
|
||||
console.error(error.stack)
|
||||
process.exit(1)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
setTimeout(takeATurn, (startSeconds - maxSeconds) * 1000)
|
||||
}
|
||||
|
@ -203,8 +226,8 @@ function startZone(zone) {
|
|||
takeATurn()
|
||||
}
|
||||
|
||||
Promise.all(DomainNames.map(cf.getZones)).then(function (results) {
|
||||
const zones = results.reduce(function (memo, zones) {
|
||||
Promise.all(DomainNames.map(cf.getZones)).then(function(results) {
|
||||
const zones = results.reduce(function(memo, zones) {
|
||||
return memo.concat(zones)
|
||||
})
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
function checkBlacklist(blacklist) {
|
||||
return function (req, res, next) {
|
||||
return function(req, res, next) {
|
||||
// Do not allow packages that have been blacklisted.
|
||||
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 {
|
||||
next()
|
||||
}
|
||||
|
|
|
@ -4,27 +4,28 @@ const getFileContentType = require('../utils/getFileContentType')
|
|||
|
||||
const e = React.createElement
|
||||
|
||||
const formatTime = (time) =>
|
||||
new Date(time).toISOString()
|
||||
const formatTime = time => new Date(time).toISOString()
|
||||
|
||||
const DirectoryListing = ({ dir, entries }) => {
|
||||
const rows = entries.map(({ file, stats }, index) => {
|
||||
const isDir = stats.isDirectory()
|
||||
const href = file + (isDir ? '/' : '')
|
||||
|
||||
return (
|
||||
e('tr', { key: file, className: index % 2 ? 'odd' : 'even' },
|
||||
e('td', null, e('a', { title: file, href }, file)),
|
||||
e('td', null, isDir ? '-' : getFileContentType(file)),
|
||||
e('td', null, isDir ? '-' : prettyBytes(stats.size)),
|
||||
e('td', null, isDir ? '-' : formatTime(stats.mtime))
|
||||
)
|
||||
return e(
|
||||
'tr',
|
||||
{ key: file, className: index % 2 ? 'odd' : 'even' },
|
||||
e('td', null, e('a', { title: file, href }, file)),
|
||||
e('td', null, isDir ? '-' : getFileContentType(file)),
|
||||
e('td', null, isDir ? '-' : prettyBytes(stats.size)),
|
||||
e('td', null, isDir ? '-' : formatTime(stats.mtime))
|
||||
)
|
||||
})
|
||||
|
||||
if (dir !== '/')
|
||||
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('td', null, '-'),
|
||||
|
@ -32,18 +33,22 @@ const DirectoryListing = ({ dir, entries }) => {
|
|||
)
|
||||
)
|
||||
|
||||
return (
|
||||
e('table', null,
|
||||
e('thead', null,
|
||||
e('tr', null,
|
||||
e('th', null, 'Name'),
|
||||
e('th', null, 'Type'),
|
||||
e('th', null, 'Size'),
|
||||
e('th', null, 'Last Modified')
|
||||
)
|
||||
),
|
||||
e('tbody', null, rows)
|
||||
)
|
||||
return e(
|
||||
'table',
|
||||
null,
|
||||
e(
|
||||
'thead',
|
||||
null,
|
||||
e(
|
||||
'tr',
|
||||
null,
|
||||
e('th', null, 'Name'),
|
||||
e('th', null, 'Type'),
|
||||
e('th', null, 'Size'),
|
||||
e('th', null, 'Last Modified')
|
||||
)
|
||||
),
|
||||
e('tbody', null, rows)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -13,34 +13,41 @@ s.onchange = function () {
|
|||
}
|
||||
`
|
||||
|
||||
const byVersion = (a, b) =>
|
||||
semver.lt(a, b) ? -1 : (semver.gt(a, b) ? 1 : 0)
|
||||
const byVersion = (a, b) => (semver.lt(a, b) ? -1 : semver.gt(a, b) ? 1 : 0)
|
||||
|
||||
const IndexPage = ({ packageInfo, version, dir, entries }) => {
|
||||
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}`)
|
||||
))
|
||||
)
|
||||
|
||||
return (
|
||||
e('html', null,
|
||||
e('head', null,
|
||||
e('meta', { charSet: 'utf-8' }),
|
||||
e('title', null, `Index of ${dir}`),
|
||||
e('style', { dangerouslySetInnerHTML: { __html: IndexPageStyle } })
|
||||
),
|
||||
e('body', null,
|
||||
e('div', { className: 'content-wrapper' },
|
||||
e('div', { className: 'version-wrapper' },
|
||||
e('select', { id: 'version', defaultValue: version }, options)
|
||||
),
|
||||
e('h1', null, `Index of ${dir}`),
|
||||
e('script', { dangerouslySetInnerHTML: { __html: IndexPageScript } }),
|
||||
e('hr'),
|
||||
e(DirectoryListing, { dir, entries }),
|
||||
e('hr'),
|
||||
e('address', null, `${packageInfo.name}@${version}`)
|
||||
)
|
||||
return e(
|
||||
'html',
|
||||
null,
|
||||
e(
|
||||
'head',
|
||||
null,
|
||||
e('meta', { charSet: 'utf-8' }),
|
||||
e('title', null, `Index of ${dir}`),
|
||||
e('style', { dangerouslySetInnerHTML: { __html: IndexPageStyle } })
|
||||
),
|
||||
e(
|
||||
'body',
|
||||
null,
|
||||
e(
|
||||
'div',
|
||||
{ className: 'content-wrapper' },
|
||||
e(
|
||||
'div',
|
||||
{ className: 'version-wrapper' },
|
||||
e('select', { id: 'version', defaultValue: version }, options)
|
||||
),
|
||||
e('h1', null, `Index of ${dir}`),
|
||||
e('script', { dangerouslySetInnerHTML: { __html: IndexPageScript } }),
|
||||
e('hr'),
|
||||
e(DirectoryListing, { dir, entries }),
|
||||
e('hr'),
|
||||
e('address', null, `${packageInfo.name}@${version}`)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -13,18 +13,18 @@ function getBasename(file) {
|
|||
/**
|
||||
* 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"
|
||||
* depending on which one is available, similar to require('lib/file').
|
||||
*/
|
||||
function findFile(base, useIndex, callback) {
|
||||
FindExtensions.reduceRight(function (next, ext) {
|
||||
FindExtensions.reduceRight(function(next, ext) {
|
||||
const file = base + ext
|
||||
|
||||
return function () {
|
||||
fs.stat(file, function (error, stats) {
|
||||
return function() {
|
||||
fs.stat(file, function(error, stats) {
|
||||
if (error) {
|
||||
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') {
|
||||
next()
|
||||
|
@ -32,7 +32,11 @@ function findFile(base, useIndex, callback) {
|
|||
callback(error)
|
||||
}
|
||||
} 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) {
|
||||
callback(error)
|
||||
} else if (indexFile) {
|
||||
|
@ -55,14 +59,20 @@ function findFile(base, useIndex, callback) {
|
|||
* trailing slash.
|
||||
*/
|
||||
function fetchFile(req, res, next) {
|
||||
getPackageInfo(req.packageName, function (error, packageInfo) {
|
||||
getPackageInfo(req.packageName, function(error, packageInfo) {
|
||||
if (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)
|
||||
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
|
||||
|
||||
|
@ -70,10 +80,13 @@ function fetchFile(req, res, next) {
|
|||
// A valid request for a package we haven't downloaded yet.
|
||||
req.packageConfig = req.packageInfo.versions[req.packageVersion]
|
||||
|
||||
getPackage(req.packageConfig, function (error, outputDir) {
|
||||
getPackage(req.packageConfig, function(error, outputDir) {
|
||||
if (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 {
|
||||
req.packageDir = outputDir
|
||||
|
||||
|
@ -84,12 +97,18 @@ function fetchFile(req, res, next) {
|
|||
// They want an ES module. Try "module", "jsnext:main", and "/"
|
||||
// https://github.com/rollup/rollup/wiki/pkg.module
|
||||
if (!filename)
|
||||
filename = req.packageConfig.module || req.packageConfig['jsnext:main'] || '/'
|
||||
filename =
|
||||
req.packageConfig.module ||
|
||||
req.packageConfig['jsnext:main'] ||
|
||||
'/'
|
||||
} else if (filename) {
|
||||
// They are requesting an explicit filename. Only try to find an
|
||||
// index file if they are NOT requesting an HTML directory listing.
|
||||
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.
|
||||
filename = req.packageConfig[req.query.main]
|
||||
} else if (typeof req.packageConfig.unpkg === 'string') {
|
||||
|
@ -104,23 +123,46 @@ function fetchFile(req, res, next) {
|
|||
filename = req.packageConfig.main || '/'
|
||||
}
|
||||
|
||||
findFile(path.join(req.packageDir, filename), useIndex, function (error, file, stats) {
|
||||
if (error)
|
||||
console.error(error)
|
||||
findFile(path.join(req.packageDir, filename), useIndex, function(
|
||||
error,
|
||||
file,
|
||||
stats
|
||||
) {
|
||||
if (error) console.error(error)
|
||||
|
||||
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, '')
|
||||
|
||||
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
|
||||
// correctly. Cache module redirects for 1 minute.
|
||||
delete req.query.main
|
||||
res.set({
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Cache-Tag': 'redirect,module-redirect'
|
||||
}).redirect(302, createPackageURL(req.packageName, req.packageVersion, filename, createSearch(req.query)))
|
||||
res
|
||||
.set({
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Cache-Tag': 'redirect,module-redirect'
|
||||
})
|
||||
.redirect(
|
||||
302,
|
||||
createPackageURL(
|
||||
req.packageName,
|
||||
req.packageVersion,
|
||||
filename,
|
||||
createSearch(req.query)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
req.filename = filename
|
||||
req.stats = stats
|
||||
|
@ -131,21 +173,47 @@ function fetchFile(req, res, next) {
|
|||
})
|
||||
} else if (req.packageVersion in req.packageInfo['dist-tags']) {
|
||||
// Cache tag redirects for 1 minute.
|
||||
res.set({
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Cache-Tag': 'redirect,tag-redirect'
|
||||
}).redirect(302, createPackageURL(req.packageName, req.packageInfo['dist-tags'][req.packageVersion], req.filename, req.search))
|
||||
res
|
||||
.set({
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Cache-Tag': 'redirect,tag-redirect'
|
||||
})
|
||||
.redirect(
|
||||
302,
|
||||
createPackageURL(
|
||||
req.packageName,
|
||||
req.packageInfo['dist-tags'][req.packageVersion],
|
||||
req.filename,
|
||||
req.search
|
||||
)
|
||||
)
|
||||
} 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) {
|
||||
// Cache semver redirects for 1 minute.
|
||||
res.set({
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Cache-Tag': 'redirect,semver-redirect'
|
||||
}).redirect(302, createPackageURL(req.packageName, maxVersion, req.filename, req.search))
|
||||
res
|
||||
.set({
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Cache-Tag': 'redirect,semver-redirect'
|
||||
})
|
||||
.redirect(
|
||||
302,
|
||||
createPackageURL(
|
||||
req.packageName,
|
||||
maxVersion,
|
||||
req.filename,
|
||||
req.search
|
||||
)
|
||||
)
|
||||
} else {
|
||||
res.status(404).type('text').send(`Cannot find package ${req.packageSpec}`)
|
||||
res
|
||||
.status(404)
|
||||
.type('text')
|
||||
.send(`Cannot find package ${req.packageSpec}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -19,9 +19,8 @@ function queryIsKnown(query) {
|
|||
function sanitizeQuery(query) {
|
||||
const saneQuery = {}
|
||||
|
||||
Object.keys(query).forEach(function (param) {
|
||||
if (isKnownQueryParam(param))
|
||||
saneQuery[param] = query[param]
|
||||
Object.keys(query).forEach(function(param) {
|
||||
if (isKnownQueryParam(param)) saneQuery[param] = query[param]
|
||||
})
|
||||
|
||||
return saneQuery
|
||||
|
@ -54,13 +53,21 @@ function packageURL(req, res, next) {
|
|||
|
||||
// Do not allow invalid URLs.
|
||||
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
|
||||
|
||||
// Do not allow invalid package names.
|
||||
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.packageVersion = url.packageVersion
|
||||
|
|
|
@ -18,36 +18,43 @@ const AutoIndex = !process.env.DISABLE_INDEX
|
|||
const MaximumDepth = 128
|
||||
|
||||
const FileTransforms = {
|
||||
expand: function (file, dependencies, callback) {
|
||||
expand: function(file, dependencies, callback) {
|
||||
const options = {
|
||||
plugins: [
|
||||
unpkgRewrite(dependencies)
|
||||
]
|
||||
plugins: [unpkgRewrite(dependencies)]
|
||||
}
|
||||
|
||||
babel.transformFile(file, options, function (error, result) {
|
||||
babel.transformFile(file, options, function(error, result) {
|
||||
callback(error, result && result.code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send the file, JSON metadata, or HTML directory listing.
|
||||
*/
|
||||
function serveFile(req, res, next) {
|
||||
if (req.query.meta != null) {
|
||||
// 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) {
|
||||
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 {
|
||||
// Cache metadata for 1 year.
|
||||
res.set({
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
'Cache-Tag': 'meta'
|
||||
}).send(metadata)
|
||||
res
|
||||
.set({
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
'Cache-Tag': 'meta'
|
||||
})
|
||||
.send(metadata)
|
||||
}
|
||||
})
|
||||
} else if (req.stats.isFile()) {
|
||||
|
@ -56,39 +63,52 @@ function serveFile(req, res, next) {
|
|||
|
||||
let contentType = getFileContentType(file)
|
||||
|
||||
if (contentType === 'text/html')
|
||||
contentType = 'text/plain' // We can't serve HTML because bad people :(
|
||||
if (contentType === 'text/html') contentType = 'text/plain' // We can't serve HTML because bad people :(
|
||||
|
||||
if (contentType === 'application/javascript' && req.query.module != null) {
|
||||
// Serve a JavaScript module.
|
||||
const dependencies = Object.assign({},
|
||||
const dependencies = Object.assign(
|
||||
{},
|
||||
req.packageConfig.peerDependencies,
|
||||
req.packageConfig.dependencies
|
||||
)
|
||||
|
||||
FileTransforms.expand(file, dependencies, function (error, code) {
|
||||
FileTransforms.expand(file, dependencies, function(error, code) {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
const 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}`)
|
||||
const 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 {
|
||||
// Cache modules for 1 year.
|
||||
res.set({
|
||||
'Content-Type': contentType,
|
||||
'Content-Length': Buffer.byteLength(code),
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
'Cache-Tag': 'file,js-file,js-module'
|
||||
}).send(code)
|
||||
res
|
||||
.set({
|
||||
'Content-Type': contentType,
|
||||
'Content-Length': Buffer.byteLength(code),
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
'Cache-Tag': 'file,js-file,js-module'
|
||||
})
|
||||
.send(code)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Serve some other static file.
|
||||
const tags = [ 'file' ]
|
||||
const tags = ['file']
|
||||
|
||||
const ext = path.extname(req.filename).substr(1)
|
||||
|
||||
if (ext)
|
||||
tags.push(`${ext}-file`)
|
||||
if (ext) tags.push(`${ext}-file`)
|
||||
|
||||
// Cache files for 1 year.
|
||||
res.set({
|
||||
|
@ -96,13 +116,13 @@ function serveFile(req, res, next) {
|
|||
'Content-Length': req.stats.size,
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
'Last-Modified': req.stats.mtime.toUTCString(),
|
||||
'ETag': etag(req.stats),
|
||||
ETag: etag(req.stats),
|
||||
'Cache-Tag': tags.join(',')
|
||||
})
|
||||
|
||||
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(error)
|
||||
res.sendStatus(500)
|
||||
|
@ -112,20 +132,36 @@ function serveFile(req, res, next) {
|
|||
}
|
||||
} else if (AutoIndex && req.stats.isDirectory()) {
|
||||
// Serve an HTML directory listing.
|
||||
getIndexHTML(req.packageInfo, req.packageVersion, req.packageDir, req.filename, function (error, html) {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
res.status(500).type('text').send(`Cannot generate index page for ${req.packageSpec}${req.filename}`)
|
||||
} else {
|
||||
// Cache HTML directory listings for 1 minute.
|
||||
res.set({
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Cache-Tag': 'index'
|
||||
}).send(html)
|
||||
getIndexHTML(
|
||||
req.packageInfo,
|
||||
req.packageVersion,
|
||||
req.packageDir,
|
||||
req.filename,
|
||||
function(error, html) {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
res
|
||||
.status(500)
|
||||
.type('text')
|
||||
.send(
|
||||
`Cannot generate index page for ${req.packageSpec}${req.filename}`
|
||||
)
|
||||
} else {
|
||||
// Cache HTML directory listings for 1 minute.
|
||||
res
|
||||
.set({
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Cache-Tag': 'index'
|
||||
})
|
||||
.send(html)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
} 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`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ function createCache(keyPrefix) {
|
|||
}
|
||||
|
||||
function get(key, callback) {
|
||||
db.get(createKey(key), function (error, value) {
|
||||
db.get(createKey(key), function(error, value) {
|
||||
callback(error, value && JSON.parse(value))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
function createMutex(doWork) {
|
||||
const mutex = {}
|
||||
|
||||
return function (key, payload, callback) {
|
||||
return function(key, payload, callback) {
|
||||
if (mutex[key]) {
|
||||
mutex[key].push(callback)
|
||||
} else {
|
||||
mutex[key] = [ function () {
|
||||
delete mutex[key]
|
||||
}, callback ]
|
||||
mutex[key] = [
|
||||
function() {
|
||||
delete mutex[key]
|
||||
},
|
||||
callback
|
||||
]
|
||||
|
||||
doWork(payload, function (error, value) {
|
||||
mutex[key].forEach(function (callback) {
|
||||
doWork(payload, function(error, value) {
|
||||
mutex[key].forEach(function(callback) {
|
||||
callback(error, value)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
function createSearch(query) {
|
||||
const params = []
|
||||
|
||||
Object.keys(query).forEach(function (param) {
|
||||
Object.keys(query).forEach(function(param) {
|
||||
if (query[param] === '') {
|
||||
params.push(param) // Omit the trailing "=" from param=
|
||||
} else {
|
||||
|
|
|
@ -8,16 +8,16 @@ const IndexPage = require('../components/IndexPage')
|
|||
const e = React.createElement
|
||||
|
||||
function getEntries(dir) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
fs.readdir(dir, function (error, files) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
fs.readdir(dir, function(error, files) {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(
|
||||
Promise.all(
|
||||
files.map(file => getFileStats(path.join(dir, file)))
|
||||
).then(function (statsArray) {
|
||||
return statsArray.map(function (stats, index) {
|
||||
).then(function(statsArray) {
|
||||
return statsArray.map(function(stats, index) {
|
||||
return { file: files[index], stats }
|
||||
})
|
||||
})
|
||||
|
|
|
@ -6,20 +6,27 @@ const getFileStats = require('./getFileStats')
|
|||
const getFileType = require('./getFileType')
|
||||
|
||||
function getEntries(dir, file, maximumDepth) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
fs.readdir(path.join(dir, file), function (error, files) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
fs.readdir(path.join(dir, file), function(error, files) {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(
|
||||
Promise.all(
|
||||
files.map(function (f) {
|
||||
files.map(function(f) {
|
||||
return getFileStats(path.join(dir, file, f))
|
||||
})
|
||||
).then(function (statsArray) {
|
||||
return Promise.all(statsArray.map(function (stats, index) {
|
||||
return getMetadataRecursive(dir, path.join(file, files[index]), stats, maximumDepth - 1)
|
||||
}))
|
||||
).then(function(statsArray) {
|
||||
return Promise.all(
|
||||
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) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
fs.readFile(file, function (error, data) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
fs.readFile(file, function(error, data) {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} 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()) {
|
||||
return getIntegrity(path.join(dir, file)).then(function (integrity) {
|
||||
return getIntegrity(path.join(dir, file)).then(function(integrity) {
|
||||
metadata.integrity = integrity
|
||||
return metadata
|
||||
})
|
||||
|
@ -62,16 +69,19 @@ function getMetadataRecursive(dir, file, stats, maximumDepth) {
|
|||
if (!stats.isDirectory() || maximumDepth === 0)
|
||||
return Promise.resolve(metadata)
|
||||
|
||||
return getEntries(dir, file, maximumDepth).then(function (files) {
|
||||
return getEntries(dir, file, maximumDepth).then(function(files) {
|
||||
metadata.files = files
|
||||
return metadata
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
callback)
|
||||
}
|
||||
|
||||
module.exports = getMetadata
|
||||
|
|
|
@ -26,7 +26,7 @@ function ignoreSymlinks(file, headers) {
|
|||
}
|
||||
|
||||
function extractResponse(response, outputDir) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const extract = tar.extract(outputDir, {
|
||||
readable: true, // All dirs/files should be readable.
|
||||
map: stripNamePrefix,
|
||||
|
@ -44,24 +44,24 @@ function extractResponse(response, outputDir) {
|
|||
function fetchAndExtract(tarballURL, 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)
|
||||
})
|
||||
}
|
||||
|
||||
const fetchMutex = createMutex(function (payload, callback) {
|
||||
const fetchMutex = createMutex(function(payload, callback) {
|
||||
const { tarballURL, outputDir } = payload
|
||||
|
||||
fs.access(outputDir, function (error) {
|
||||
fs.access(outputDir, function(error) {
|
||||
if (error) {
|
||||
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') {
|
||||
// ENOENT or ENOTDIR are to be expected when we haven't yet
|
||||
// fetched a package for the first time. Carry on!
|
||||
mkdirp(outputDir, function (error) {
|
||||
mkdirp(outputDir, function(error) {
|
||||
if (error) {
|
||||
callback(error)
|
||||
} else {
|
||||
fetchAndExtract(tarballURL, outputDir).then(function () {
|
||||
fetchAndExtract(tarballURL, outputDir).then(function() {
|
||||
callback()
|
||||
}, callback)
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ function getPackage(packageConfig, callback) {
|
|||
const tarballURL = packageConfig.dist.tarball
|
||||
const outputDir = createTempPath(packageConfig.name, packageConfig.version)
|
||||
|
||||
fetchMutex(tarballURL, { tarballURL, outputDir }, function (error) {
|
||||
fetchMutex(tarballURL, { tarballURL, outputDir }, function(error) {
|
||||
callback(error, outputDir)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -20,9 +20,9 @@ function fetchPackageInfo(packageName) {
|
|||
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
Accept: 'application/json'
|
||||
}
|
||||
}).then(function (res) {
|
||||
}).then(function(res) {
|
||||
return res.status === 404 ? null : res.json()
|
||||
})
|
||||
}
|
||||
|
@ -31,32 +31,35 @@ const PackageNotFound = 'PackageNotFound'
|
|||
|
||||
// This mutex prevents multiple concurrent requests to
|
||||
// the registry for the same package info.
|
||||
const fetchMutex = createMutex(function (packageName, callback) {
|
||||
fetchPackageInfo(packageName).then(function (value) {
|
||||
if (value == null) {
|
||||
// Cache 404s for 5 minutes. This prevents us from making
|
||||
// unnecessary requests to the registry for bad package names.
|
||||
// In the worst case, a brand new package's info will be
|
||||
// available within 5 minutes.
|
||||
PackageInfoCache.set(packageName, PackageNotFound, 300, function () {
|
||||
callback(null, value)
|
||||
})
|
||||
} else {
|
||||
// Cache valid package info for 1 minute.
|
||||
PackageInfoCache.set(packageName, value, 60, function () {
|
||||
callback(null, value)
|
||||
const fetchMutex = createMutex(function(packageName, callback) {
|
||||
fetchPackageInfo(packageName).then(
|
||||
function(value) {
|
||||
if (value == null) {
|
||||
// Cache 404s for 5 minutes. This prevents us from making
|
||||
// unnecessary requests to the registry for bad package names.
|
||||
// In the worst case, a brand new package's info will be
|
||||
// available within 5 minutes.
|
||||
PackageInfoCache.set(packageName, PackageNotFound, 300, function() {
|
||||
callback(null, value)
|
||||
})
|
||||
} else {
|
||||
// Cache valid package info for 1 minute.
|
||||
PackageInfoCache.set(packageName, value, 60, function() {
|
||||
callback(null, value)
|
||||
})
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
// Do not cache errors.
|
||||
PackageInfoCache.del(packageName, function() {
|
||||
callback(error)
|
||||
})
|
||||
}
|
||||
}, function (error) {
|
||||
// Do not cache errors.
|
||||
PackageInfoCache.del(packageName, function () {
|
||||
callback(error)
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
function getPackageInfo(packageName, callback) {
|
||||
PackageInfoCache.get(packageName, function (error, value) {
|
||||
PackageInfoCache.get(packageName, function(error, value) {
|
||||
if (error || value != null) {
|
||||
callback(error, value === PackageNotFound ? null : value)
|
||||
} else {
|
||||
|
|
|
@ -10,169 +10,96 @@
|
|||
* The range `null` is a catch-all.
|
||||
*/
|
||||
module.exports = {
|
||||
'angular': [
|
||||
[ '>=1.2.27', '/angular.min.js' ],
|
||||
[ null, '/lib/angular.min.js' ]
|
||||
angular: [['>=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': [
|
||||
[ null, '/angular-animate.min.js' ]
|
||||
],
|
||||
'bootstrap-sass': [[null, '/assets/javascripts/bootstrap.min.js']],
|
||||
|
||||
'angular-cookies': [
|
||||
[ null, '/angular-cookies.min.js' ]
|
||||
],
|
||||
bulma: [[null, '/css/bulma.css']],
|
||||
|
||||
'angular-resource': [
|
||||
[ null, '/angular-resource.min.js' ]
|
||||
],
|
||||
'core.js': [[null, '/dist/core.min.js']],
|
||||
|
||||
'angular-sanitize': [
|
||||
[ null, '/angular-sanitize.min.js' ]
|
||||
],
|
||||
'create-react-class': [[null, '/create-react-class.min.js']],
|
||||
|
||||
'angular-ui-bootstrap': [
|
||||
[ null, '/dist/ui-bootstrap.js' ]
|
||||
],
|
||||
d3: [[null, '/build/d3.min.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' ]
|
||||
],
|
||||
|
||||
'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' ]
|
||||
],
|
||||
'ember-source': [[null, '/dist/ember.min.js']],
|
||||
|
||||
'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': [
|
||||
[ null, '/TweenMax.js' ]
|
||||
gsap: [[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': [
|
||||
[ 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-bootstrap': [[null, '/dist/react-bootstrap.min.js']],
|
||||
|
||||
'react-dom': [
|
||||
[ '>=16.0.0-alpha.7', '/umd/react-dom.production.min.js' ],
|
||||
[ null, '/dist/react-dom.min.js' ]
|
||||
['>=16.0.0-alpha.7', '/umd/react-dom.production.min.js'],
|
||||
[null, '/dist/react-dom.min.js']
|
||||
],
|
||||
|
||||
'react-router': [
|
||||
[ '>=4.0.0', '/umd/react-router.min.js' ],
|
||||
[ null, '/umd/ReactRouter.min.js' ]
|
||||
['>=4.0.0', '/umd/react-router.min.js'],
|
||||
[null, '/umd/ReactRouter.min.js']
|
||||
],
|
||||
|
||||
'redux': [
|
||||
[ null, '/dist/redux.min.js' ]
|
||||
],
|
||||
redux: [[null, '/dist/redux.min.js']],
|
||||
|
||||
'redux-saga': [
|
||||
[ null, '/dist/redux-saga.min.js' ]
|
||||
],
|
||||
'redux-saga': [[null, '/dist/redux-saga.min.js']],
|
||||
|
||||
'redux-thunk': [
|
||||
[ null, '/dist/redux-thunk.min.js' ]
|
||||
],
|
||||
'redux-thunk': [[null, '/dist/redux-thunk.min.js']],
|
||||
|
||||
'snapsvg': [
|
||||
[ null, '/snap.svg-min.js' ]
|
||||
],
|
||||
snapsvg: [[null, '/snap.svg-min.js']],
|
||||
|
||||
'systemjs': [
|
||||
[ null, '/dist/system.js' ]
|
||||
],
|
||||
systemjs: [[null, '/dist/system.js']],
|
||||
|
||||
'three': [
|
||||
[ '<=0.77.0', '/three.min.js' ],
|
||||
[ null, '/build/three.min.js' ]
|
||||
],
|
||||
three: [['<=0.77.0', '/three.min.js'], [null, '/build/three.min.js']],
|
||||
|
||||
'underscore': [
|
||||
[ null, '/underscore-min.js' ]
|
||||
],
|
||||
underscore: [[null, '/underscore-min.js']],
|
||||
|
||||
'vue': [
|
||||
[ null, '/dist/vue.min.js' ]
|
||||
],
|
||||
vue: [[null, '/dist/vue.min.js']],
|
||||
|
||||
'zepto': [
|
||||
[ null, '/dist/zepto.min.js' ]
|
||||
],
|
||||
zepto: [[null, '/dist/zepto.min.js']],
|
||||
|
||||
'zingchart': [
|
||||
[ null, '/client/zingchart.min.js' ]
|
||||
],
|
||||
zingchart: [[null, '/client/zingchart.min.js']],
|
||||
|
||||
'zone.js': [
|
||||
[ null, '/dist/zone.js' ]
|
||||
]
|
||||
'zone.js': [[null, '/dist/zone.js']]
|
||||
}
|
||||
|
|
|
@ -5,11 +5,10 @@ function getAssetPaths(packageName, version) {
|
|||
const entries = assetPathsIndex[packageName]
|
||||
|
||||
if (entries) {
|
||||
const matchingEntry = entries.find(function (entry) {
|
||||
const matchingEntry = entries.find(function(entry) {
|
||||
const range = entry[0]
|
||||
|
||||
if (range == null || semver.satisfies(version, range))
|
||||
return entry
|
||||
if (range == null || semver.satisfies(version, range)) return entry
|
||||
})
|
||||
|
||||
return matchingEntry.slice(1)
|
||||
|
|
|
@ -2,13 +2,13 @@ const searchIndex = require('./searchIndex')
|
|||
const getAssetPaths = require('./getAssetPaths')
|
||||
|
||||
function enhanceHit(hit) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const assetPaths = getAssetPaths(hit.name, hit.version)
|
||||
|
||||
if (assetPaths) {
|
||||
// TODO: Double check the package metadata to ensure the files
|
||||
// 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}`
|
||||
})
|
||||
|
||||
|
@ -16,9 +16,7 @@ function enhanceHit(hit) {
|
|||
} else {
|
||||
// We don't have any global paths for this package yet. Try
|
||||
// using the "bare" URL.
|
||||
hit.assets = [
|
||||
`https://unpkg.com/${hit.name}@${hit.version}`
|
||||
]
|
||||
hit.assets = [`https://unpkg.com/${hit.name}@${hit.version}`]
|
||||
|
||||
resolve(hit)
|
||||
}
|
||||
|
@ -32,13 +30,13 @@ function concat(string) {
|
|||
}
|
||||
|
||||
function search(query, page) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const hitsPerPage = 10
|
||||
|
||||
const params = {
|
||||
// typoTolerance: 'min',
|
||||
// optionalFacetFilters: `concatenatedName:${concat(query)}`,
|
||||
facets: [ 'keywords' ],
|
||||
facets: ['keywords'],
|
||||
attributesToHighlight: null,
|
||||
attributesToRetrieve: [
|
||||
'description',
|
||||
|
@ -58,14 +56,12 @@ function search(query, page) {
|
|||
page
|
||||
}
|
||||
|
||||
searchIndex.search(query, params, function (error, value) {
|
||||
searchIndex.search(query, params, function(error, value) {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(
|
||||
Promise.all(
|
||||
value.hits.map(enhanceHit)
|
||||
).then(function (hits) {
|
||||
Promise.all(value.hits.map(enhanceHit)).then(function(hits) {
|
||||
const totalHits = value.nbHits
|
||||
const totalPages = value.nbPages
|
||||
|
||||
|
|
|
@ -14,9 +14,8 @@ invariant(
|
|||
'Missing $ALGOLIA_NPM_SEARCH_API_KEY environment variable'
|
||||
)
|
||||
|
||||
const index = algolia(
|
||||
AlgoliaNpmSearchAppId,
|
||||
AlgoliaNpmSearchApiKey
|
||||
).initIndex('npm-search')
|
||||
const index = algolia(AlgoliaNpmSearchAppId, AlgoliaNpmSearchApiKey).initIndex(
|
||||
'npm-search'
|
||||
)
|
||||
|
||||
module.exports = index
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
function createPackageURL(packageName, version, filename, search) {
|
||||
let pathname = `/${packageName}`
|
||||
|
||||
if (version != null)
|
||||
pathname += `@${version}`
|
||||
if (version != null) pathname += `@${version}`
|
||||
|
||||
if (filename)
|
||||
pathname += filename
|
||||
if (filename) pathname += filename
|
||||
|
||||
if (search)
|
||||
pathname += search
|
||||
if (search) pathname += search
|
||||
|
||||
return pathname
|
||||
}
|
||||
|
|
|
@ -19,20 +19,20 @@ function parsePackageURL(packageURL) {
|
|||
|
||||
const match = URLFormat.exec(pathname)
|
||||
|
||||
if (match == null)
|
||||
return null
|
||||
if (match == null) return null
|
||||
|
||||
const packageName = match[1]
|
||||
const packageVersion = decodeParam(match[2]) || 'latest'
|
||||
const filename = decodeParam(match[3])
|
||||
|
||||
return { // If the URL is /@scope/name@version/file.js?main=browser:
|
||||
pathname, // /@scope/name@version/path.js
|
||||
search, // ?main=browser
|
||||
query, // { main: 'browser' }
|
||||
packageName, // @scope/name
|
||||
return {
|
||||
// If the URL is /@scope/name@version/file.js?main=browser:
|
||||
pathname, // /@scope/name@version/path.js
|
||||
search, // ?main=browser
|
||||
query, // { main: 'browser' }
|
||||
packageName, // @scope/name
|
||||
packageVersion, // version
|
||||
filename // /file.js
|
||||
filename // /file.js
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue