Prettify
This commit is contained in:
parent
f3974b5e2d
commit
3a309241da
|
@ -1,8 +1,6 @@
|
||||||
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
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react'
|
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>
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
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
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import React from 'react'
|
import React from "react"
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from "prop-types"
|
||||||
import { Motion, spring } from 'react-motion'
|
import { Motion, spring } from "react-motion"
|
||||||
import { Switch, Route, Link, withRouter } from 'react-router-dom'
|
import { Switch, Route, Link, withRouter } from "react-router-dom"
|
||||||
import WindowSize from './WindowSize'
|
import WindowSize from "./WindowSize"
|
||||||
import About from './About'
|
import About from "./About"
|
||||||
import Stats from './Stats'
|
import Stats from "./Stats"
|
||||||
import Home from './Home'
|
import Home from "./Home"
|
||||||
|
|
||||||
class Layout extends React.Component {
|
class Layout extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -23,18 +23,18 @@ class Layout extends React.Component {
|
||||||
adjustUnderline = (useSpring = false) => {
|
adjustUnderline = (useSpring = false) => {
|
||||||
let itemIndex
|
let itemIndex
|
||||||
switch (this.props.location.pathname) {
|
switch (this.props.location.pathname) {
|
||||||
case '/stats':
|
case "/stats":
|
||||||
itemIndex = 1
|
itemIndex = 1
|
||||||
break
|
break
|
||||||
case '/about':
|
case "/about":
|
||||||
itemIndex = 2
|
itemIndex = 2
|
||||||
break
|
break
|
||||||
case '/':
|
case "/":
|
||||||
default:
|
default:
|
||||||
itemIndex = 0
|
itemIndex = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemNodes = this.listNode.querySelectorAll('li')
|
const itemNodes = this.listNode.querySelectorAll("li")
|
||||||
const currentNode = itemNodes[itemIndex]
|
const currentNode = itemNodes[itemIndex]
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -47,7 +47,7 @@ class Layout extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.adjustUnderline()
|
this.adjustUnderline()
|
||||||
|
|
||||||
fetch('/_stats?period=last-month')
|
fetch("/_stats?period=last-month")
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(stats => this.setState({ stats }))
|
.then(stats => this.setState({ stats }))
|
||||||
|
|
||||||
|
@ -63,17 +63,14 @@ class Layout extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.location.pathname !== this.props.location.pathname)
|
if (prevProps.location.pathname !== this.props.location.pathname) this.adjustUnderline(true)
|
||||||
this.adjustUnderline(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { underlineLeft, underlineWidth, useSpring } = this.state
|
const { underlineLeft, underlineWidth, useSpring } = this.state
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
left: useSpring
|
left: useSpring ? spring(underlineLeft, { stiffness: 220 }) : underlineLeft,
|
||||||
? spring(underlineLeft, { stiffness: 220 })
|
|
||||||
: underlineLeft,
|
|
||||||
width: useSpring ? spring(underlineWidth) : underlineWidth
|
width: useSpring ? spring(underlineWidth) : underlineWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,10 +81,7 @@ class Layout extends React.Component {
|
||||||
<header>
|
<header>
|
||||||
<h1 className="layout-title">unpkg</h1>
|
<h1 className="layout-title">unpkg</h1>
|
||||||
<nav className="layout-nav">
|
<nav className="layout-nav">
|
||||||
<ol
|
<ol className="layout-nav-list" ref={node => (this.listNode = node)}>
|
||||||
className="layout-nav-list"
|
|
||||||
ref={node => (this.listNode = node)}
|
|
||||||
>
|
|
||||||
<li>
|
<li>
|
||||||
<Link to="/">Home</Link>
|
<Link to="/">Home</Link>
|
||||||
</li>
|
</li>
|
||||||
|
@ -117,10 +111,7 @@ class Layout extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route path="/stats" render={() => <Stats data={this.state.stats} />} />
|
||||||
path="/stats"
|
|
||||||
render={() => <Stats data={this.state.stats} />}
|
|
||||||
/>
|
|
||||||
<Route path="/about" component={About} />
|
<Route path="/about" component={About} />
|
||||||
<Route path="/" component={Home} />
|
<Route path="/" component={Home} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react'
|
import React from "react"
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from "prop-types"
|
||||||
import { parseNumber, formatNumber } from './NumberUtils'
|
import { parseNumber, formatNumber } from "./NumberUtils"
|
||||||
|
|
||||||
class NumberTextInput extends React.Component {
|
class NumberTextInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -37,14 +37,7 @@ class NumberTextInput extends React.Component {
|
||||||
const { parseNumber, formatNumber, ...props } = this.props // eslint-disable-line no-unused-vars
|
const { parseNumber, formatNumber, ...props } = this.props // eslint-disable-line no-unused-vars
|
||||||
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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,19 @@
|
||||||
import React from 'react'
|
import React from "react"
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from "prop-types"
|
||||||
import formatBytes from 'pretty-bytes'
|
import formatBytes from "pretty-bytes"
|
||||||
import formatDate from 'date-fns/format'
|
import formatDate from "date-fns/format"
|
||||||
import parseDate from 'date-fns/parse'
|
import parseDate from "date-fns/parse"
|
||||||
import formatNumber from './utils/formatNumber'
|
import formatNumber from "./utils/formatNumber"
|
||||||
import formatPercent from './utils/formatPercent'
|
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(
|
Object.keys(countries).filter(country => countries[country].continent === continent)
|
||||||
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 {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -63,15 +59,11 @@ class Stats extends React.Component {
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{formatNumber(requests)} ({formatPercent(
|
{formatNumber(requests)} ({formatPercent(requests / totals.requests.all)}%)
|
||||||
requests / totals.requests.all
|
|
||||||
)}%)
|
|
||||||
</td>
|
</td>
|
||||||
{bandwidth ? (
|
{bandwidth ? (
|
||||||
<td>
|
<td>
|
||||||
{formatBytes(bandwidth)} ({formatPercent(
|
{formatBytes(bandwidth)} ({formatPercent(bandwidth / totals.bandwidth.all)}%)
|
||||||
bandwidth / totals.bandwidth.all
|
|
||||||
)}%)
|
|
||||||
</td>
|
</td>
|
||||||
) : (
|
) : (
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
|
@ -104,10 +96,7 @@ class Stats extends React.Component {
|
||||||
const continentName = continents[continent]
|
const continentName = continents[continent]
|
||||||
const continentData = continentsData[continent]
|
const continentData = continentsData[continent]
|
||||||
|
|
||||||
if (
|
if (continentData.requests > this.state.minCountryRequests && continentData.bandwidth !== 0) {
|
||||||
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>
|
||||||
|
@ -176,29 +165,26 @@ class Stats extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="wrapper">
|
<div className="wrapper">
|
||||||
<p>
|
<p>
|
||||||
From <strong>{formatDate(since, 'MMM D')}</strong> to{' '}
|
From <strong>{formatDate(since, "MMM D")}</strong> to{" "}
|
||||||
<strong>{formatDate(until, 'MMM D')}</strong> unpkg served{' '}
|
<strong>{formatDate(until, "MMM D")}</strong> unpkg served{" "}
|
||||||
<strong>{formatNumber(totals.requests.all)}</strong> requests and a
|
<strong>{formatNumber(totals.requests.all)}</strong> requests and a total of{" "}
|
||||||
total of <strong>{formatBytes(totals.bandwidth.all)}</strong> of data
|
<strong>{formatBytes(totals.bandwidth.all)}</strong> of data to{" "}
|
||||||
to <strong>{formatNumber(totals.uniques.all)}</strong> unique
|
<strong>{formatNumber(totals.uniques.all)}</strong> unique visitors,{" "}
|
||||||
visitors,{' '}
|
<strong>{formatPercent(totals.requests.cached / totals.requests.all, 0)}%</strong> of
|
||||||
<strong>
|
which were served from the cache.
|
||||||
{formatPercent(totals.requests.cached / totals.requests.all, 0)}%
|
|
||||||
</strong>{' '}
|
|
||||||
of which were served from the cache.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>Packages</h3>
|
<h3>Packages</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
The table below shows the most popular packages served by unpkg from{' '}
|
The table below shows the most popular packages served by unpkg from{" "}
|
||||||
<strong>{formatDate(since, 'MMM D')}</strong> to{' '}
|
<strong>{formatDate(since, "MMM D")}</strong> to{" "}
|
||||||
<strong>{formatDate(until, 'MMM D')}</strong>. Only the top{' '}
|
<strong>{formatDate(until, "MMM D")}</strong>. Only the top{" "}
|
||||||
{Object.keys(totals.requests.package).length} packages are shown.
|
{Object.keys(totals.requests.package).length} packages are shown.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="table-filter">
|
<p className="table-filter">
|
||||||
Include only packages that received at least{' '}
|
Include only packages that received at least{" "}
|
||||||
<select
|
<select
|
||||||
value={this.state.minPackageRequests}
|
value={this.state.minPackageRequests}
|
||||||
onChange={event =>
|
onChange={event =>
|
||||||
|
@ -213,11 +199,11 @@ 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>{' '}
|
</select>{" "}
|
||||||
requests.
|
requests.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table cellSpacing="0" cellPadding="0" style={{ width: '100%' }}>
|
<table cellSpacing="0" cellPadding="0" style={{ width: "100%" }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Package</th>
|
<th>Package</th>
|
||||||
|
@ -231,13 +217,13 @@ class Stats extends React.Component {
|
||||||
<h3>Regions</h3>
|
<h3>Regions</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
The table below breaks down requests to unpkg from{' '}
|
The table below breaks down requests to unpkg from{" "}
|
||||||
<strong>{formatDate(since, 'MMM D')}</strong> to{' '}
|
<strong>{formatDate(since, "MMM D")}</strong> to{" "}
|
||||||
<strong>{formatDate(until, 'MMM D')}</strong> by geographic region.
|
<strong>{formatDate(until, "MMM D")}</strong> by geographic region.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="table-filter">
|
<p className="table-filter">
|
||||||
Include only countries that made at least{' '}
|
Include only countries that made at least{" "}
|
||||||
<select
|
<select
|
||||||
value={this.state.minCountryRequests}
|
value={this.state.minCountryRequests}
|
||||||
onChange={event =>
|
onChange={event =>
|
||||||
|
@ -251,16 +237,11 @@ class Stats extends React.Component {
|
||||||
<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>{' '}
|
</select>{" "}
|
||||||
requests.
|
requests.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table
|
<table cellSpacing="0" cellPadding="0" style={{ width: "100%" }} className="regions-table">
|
||||||
cellSpacing="0"
|
|
||||||
cellPadding="0"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
className="regions-table"
|
|
||||||
>
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Region</th>
|
<th>Region</th>
|
||||||
|
@ -274,12 +255,12 @@ class Stats extends React.Component {
|
||||||
<h3>Protocols</h3>
|
<h3>Protocols</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
The table below breaks down requests to unpkg from{' '}
|
The table below breaks down requests to unpkg from{" "}
|
||||||
<strong>{formatDate(since, 'MMM D')}</strong> to{' '}
|
<strong>{formatDate(since, "MMM D")}</strong> to{" "}
|
||||||
<strong>{formatDate(until, 'MMM D')}</strong> by HTTP protocol.
|
<strong>{formatDate(until, "MMM D")}</strong> by HTTP protocol.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table cellSpacing="0" cellPadding="0" style={{ width: '100%' }}>
|
<table cellSpacing="0" cellPadding="0" style={{ width: "100%" }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Protocol</th>
|
<th>Protocol</th>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React from 'react'
|
import React from "react"
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from "prop-types"
|
||||||
import addEvent from './utils/addEvent'
|
import addEvent from "./utils/addEvent"
|
||||||
import removeEvent from './utils/removeEvent'
|
import removeEvent from "./utils/removeEvent"
|
||||||
|
|
||||||
const ResizeEvent = 'resize'
|
const ResizeEvent = "resize"
|
||||||
|
|
||||||
class WindowSize extends React.Component {
|
class WindowSize extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react'
|
import React from "react"
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from "react-dom"
|
||||||
import App from './App'
|
import App from "./App"
|
||||||
import './index.css'
|
import "./index.css"
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.getElementById('root'))
|
ReactDOM.render(<App />, document.getElementById("root"))
|
||||||
|
|
|
@ -2,7 +2,7 @@ const addEvent = (node, type, handler) => {
|
||||||
if (node.addEventListener) {
|
if (node.addEventListener) {
|
||||||
node.addEventListener(type, handler, false)
|
node.addEventListener(type, handler, false)
|
||||||
} else if (node.attachEvent) {
|
} else if (node.attachEvent) {
|
||||||
node.attachEvent('on' + type, handler)
|
node.attachEvent("on" + type, handler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
const formatNumber = n => {
|
const formatNumber = n => {
|
||||||
const digits = String(n).split('')
|
const digits = String(n).split("")
|
||||||
const groups = []
|
const groups = []
|
||||||
|
|
||||||
while (digits.length) groups.unshift(digits.splice(-3).join(''))
|
while (digits.length) groups.unshift(digits.splice(-3).join(""))
|
||||||
|
|
||||||
return groups.join(',')
|
return groups.join(",")
|
||||||
}
|
}
|
||||||
|
|
||||||
export default formatNumber
|
export default formatNumber
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
const formatPercent = (n, fixed = 1) =>
|
const formatPercent = (n, fixed = 1) => String((n.toPrecision(2) * 100).toFixed(fixed))
|
||||||
String((n.toPrecision(2) * 100).toFixed(fixed))
|
|
||||||
|
|
||||||
export default formatPercent
|
export default formatPercent
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
const parseNumber = s => parseInt(s.replace(/,/g, ''), 10) || 0
|
const parseNumber = s => parseInt(s.replace(/,/g, ""), 10) || 0
|
||||||
|
|
||||||
export default parseNumber
|
export default parseNumber
|
||||||
|
|
|
@ -2,7 +2,7 @@ const removeEvent = (node, type, handler) => {
|
||||||
if (node.removeEventListener) {
|
if (node.removeEventListener) {
|
||||||
node.removeEventListener(type, handler, false)
|
node.removeEventListener(type, handler, false)
|
||||||
} else if (node.detachEvent) {
|
} else if (node.detachEvent) {
|
||||||
node.detachEvent('on' + type, handler)
|
node.detachEvent("on" + type, handler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -122,7 +122,7 @@
|
||||||
"extends": "react-app"
|
"extends": "react-app"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"semi": false,
|
"printWidth": 100,
|
||||||
"singleQuote": true
|
"semi": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
exports.isBareModuleIdentifier = require('./utils/isBareModuleIdentifier')
|
exports.isBareModuleIdentifier = require("./utils/isBareModuleIdentifier")
|
||||||
exports.parseModuleIdentifier = require('./utils/parseModuleIdentifier')
|
exports.parseModuleIdentifier = require("./utils/parseModuleIdentifier")
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
const isBareModuleIdentifier = require('../isBareModuleIdentifier')
|
const isBareModuleIdentifier = require("../isBareModuleIdentifier")
|
||||||
|
|
||||||
describe('isBareModuleIdentifier', () => {
|
describe("isBareModuleIdentifier", () => {
|
||||||
it('returns true for bare module identifiers', () => {
|
it("returns true for bare module identifiers", () => {
|
||||||
expect(isBareModuleIdentifier('react')).toBe(true)
|
expect(isBareModuleIdentifier("react")).toBe(true)
|
||||||
expect(isBareModuleIdentifier('react-dom')).toBe(true)
|
expect(isBareModuleIdentifier("react-dom")).toBe(true)
|
||||||
expect(isBareModuleIdentifier('react-dom/server')).toBe(true)
|
expect(isBareModuleIdentifier("react-dom/server")).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns false for non-bare module identifiers', () => {
|
it("returns false for non-bare module identifiers", () => {
|
||||||
expect(isBareModuleIdentifier('/absolute-path')).toBe(false)
|
expect(isBareModuleIdentifier("/absolute-path")).toBe(false)
|
||||||
expect(isBareModuleIdentifier('./relative-path')).toBe(false)
|
expect(isBareModuleIdentifier("./relative-path")).toBe(false)
|
||||||
expect(isBareModuleIdentifier('//www.example.com/script.js')).toBe(false)
|
expect(isBareModuleIdentifier("//www.example.com/script.js")).toBe(false)
|
||||||
expect(isBareModuleIdentifier('https://www.example.com/script.js')).toBe(
|
expect(isBareModuleIdentifier("https://www.example.com/script.js")).toBe(false)
|
||||||
false
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,38 +1,38 @@
|
||||||
const parseBareModuleIdentifier = require('../parseBareModuleIdentifier')
|
const parseBareModuleIdentifier = require("../parseBareModuleIdentifier")
|
||||||
|
|
||||||
describe('parseBareModuleIdentifier', () => {
|
describe("parseBareModuleIdentifier", () => {
|
||||||
it('parses simple identifiers', () => {
|
it("parses simple identifiers", () => {
|
||||||
expect(parseBareModuleIdentifier('react')).toEqual({
|
expect(parseBareModuleIdentifier("react")).toEqual({
|
||||||
packageName: 'react',
|
packageName: "react",
|
||||||
file: ''
|
file: ""
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('parses hyphenated identifiers', () => {
|
it("parses hyphenated identifiers", () => {
|
||||||
expect(parseBareModuleIdentifier('react-dom')).toEqual({
|
expect(parseBareModuleIdentifier("react-dom")).toEqual({
|
||||||
packageName: 'react-dom',
|
packageName: "react-dom",
|
||||||
file: ''
|
file: ""
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('parses hyphenated identifiers with a filename', () => {
|
it("parses hyphenated identifiers with a filename", () => {
|
||||||
expect(parseBareModuleIdentifier('react-dom/server')).toEqual({
|
expect(parseBareModuleIdentifier("react-dom/server")).toEqual({
|
||||||
packageName: 'react-dom',
|
packageName: "react-dom",
|
||||||
file: '/server'
|
file: "/server"
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('parses scoped identifiers', () => {
|
it("parses scoped identifiers", () => {
|
||||||
expect(parseBareModuleIdentifier('@babel/core')).toEqual({
|
expect(parseBareModuleIdentifier("@babel/core")).toEqual({
|
||||||
packageName: '@babel/core',
|
packageName: "@babel/core",
|
||||||
file: ''
|
file: ""
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('parses scoped identifiers with a filename', () => {
|
it("parses scoped identifiers with a filename", () => {
|
||||||
expect(parseBareModuleIdentifier('@babel/core/package.json')).toEqual({
|
expect(parseBareModuleIdentifier("@babel/core/package.json")).toEqual({
|
||||||
packageName: '@babel/core',
|
packageName: "@babel/core",
|
||||||
file: '/package.json'
|
file: "/package.json"
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
const URL = require('whatwg-url')
|
const URL = require("whatwg-url")
|
||||||
|
|
||||||
function isBareModuleIdentifier(id) {
|
function isBareModuleIdentifier(id) {
|
||||||
return !(
|
return !(
|
||||||
URL.parseURL(id) !== null || // fully qualified URL
|
URL.parseURL(id) !== null || // fully qualified URL
|
||||||
id.substr(0, 2) === '//' || // URL w/out protocol
|
id.substr(0, 2) === "//" || // URL w/out protocol
|
||||||
['.', '/'].includes(id.charAt(0)) // local path
|
[".", "/"].includes(id.charAt(0))
|
||||||
)
|
) // local path
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = isBareModuleIdentifier
|
module.exports = isBareModuleIdentifier
|
||||||
|
|
|
@ -5,7 +5,7 @@ function parseBareModuleIdentifier(id) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
packageName: match[1],
|
packageName: match[1],
|
||||||
file: match[2] || ''
|
file: match[2] || ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "unpkg",
|
"name": "unpkg",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "The JavaScript API for unpkg",
|
"description": "The JavaScript API for unpkg",
|
||||||
|
"repository": "unpkg/unpkg",
|
||||||
"files": [
|
"files": [
|
||||||
"modules/*.js",
|
"modules/*.js",
|
||||||
"modules/utils/*.js"
|
"modules/utils/*.js"
|
||||||
|
@ -9,5 +10,8 @@
|
||||||
"main": "modules/index.js",
|
"main": "modules/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"whatwg-url": "^6.3.0"
|
"whatwg-url": "^6.3.0"
|
||||||
}
|
},
|
||||||
|
"keywords": [
|
||||||
|
"unpkg"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
const fs = require('fs')
|
const fs = require("fs")
|
||||||
const path = require('path')
|
const path = require("path")
|
||||||
const crypto = require('crypto')
|
const crypto = require("crypto")
|
||||||
const jwt = require('jsonwebtoken')
|
const jwt = require("jsonwebtoken")
|
||||||
const invariant = require('invariant')
|
const invariant = require("invariant")
|
||||||
const forge = require('node-forge')
|
const forge = require("node-forge")
|
||||||
const db = require('./RedisClient')
|
const db = require("./RedisClient")
|
||||||
|
|
||||||
let keys
|
let keys
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === "production") {
|
||||||
keys = {
|
keys = {
|
||||||
public: fs.readFileSync(path.resolve(__dirname, '../public.key'), 'utf8'),
|
public: fs.readFileSync(path.resolve(__dirname, "../public.key"), "utf8"),
|
||||||
private: process.env.PRIVATE_KEY
|
private: process.env.PRIVATE_KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
invariant(keys.private, 'Missing $PRIVATE_KEY environment variable')
|
invariant(keys.private, "Missing $PRIVATE_KEY environment variable")
|
||||||
} else {
|
} else {
|
||||||
// Generate a random keypair for dev/testing.
|
// Generate a random keypair for dev/testing.
|
||||||
// See https://gist.github.com/sebadoom/2b70969e70db5da9a203bebd9cff099f
|
// See https://gist.github.com/sebadoom/2b70969e70db5da9a203bebd9cff099f
|
||||||
|
@ -29,19 +29,19 @@ function getCurrentSeconds() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTokenId() {
|
function createTokenId() {
|
||||||
return crypto.randomBytes(16).toString('hex')
|
return crypto.randomBytes(16).toString("hex")
|
||||||
}
|
}
|
||||||
|
|
||||||
function createToken(scopes = {}) {
|
function createToken(scopes = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const payload = {
|
const payload = {
|
||||||
jti: createTokenId(),
|
jti: createTokenId(),
|
||||||
iss: 'https://unpkg.com',
|
iss: "https://unpkg.com",
|
||||||
iat: getCurrentSeconds(),
|
iat: getCurrentSeconds(),
|
||||||
scopes
|
scopes
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt.sign(payload, keys.private, { algorithm: 'RS256' }, (error, token) => {
|
jwt.sign(payload, keys.private, { algorithm: "RS256" }, (error, token) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error)
|
reject(error)
|
||||||
} else {
|
} else {
|
||||||
|
@ -51,11 +51,11 @@ function createToken(scopes = {}) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const RevokedTokensSet = 'revoked-tokens'
|
const RevokedTokensSet = "revoked-tokens"
|
||||||
|
|
||||||
function verifyToken(token) {
|
function verifyToken(token) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const options = { algorithms: ['RS256'] }
|
const options = { algorithms: ["RS256"] }
|
||||||
|
|
||||||
jwt.verify(token, keys.public, options, (error, payload) => {
|
jwt.verify(token, keys.public, options, (error, payload) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const db = require('./RedisClient')
|
const db = require("./RedisClient")
|
||||||
|
|
||||||
const BlacklistSet = 'blacklisted-packages'
|
const BlacklistSet = "blacklisted-packages"
|
||||||
|
|
||||||
function addPackage(packageName) {
|
function addPackage(packageName) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
require('isomorphic-fetch')
|
require("isomorphic-fetch")
|
||||||
const invariant = require('invariant')
|
const invariant = require("invariant")
|
||||||
const gunzip = require('gunzip-maybe')
|
const gunzip = require("gunzip-maybe")
|
||||||
const ndjson = require('ndjson')
|
const ndjson = require("ndjson")
|
||||||
|
|
||||||
const CloudflareAPIURL = 'https://api.cloudflare.com'
|
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(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) {
|
function get(path, headers) {
|
||||||
return fetch(`${CloudflareAPIURL}/client/v4${path}`, {
|
return fetch(`${CloudflareAPIURL}/client/v4${path}`, {
|
||||||
headers: Object.assign({}, headers, {
|
headers: Object.assign({}, headers, {
|
||||||
'X-Auth-Email': CloudflareEmail,
|
"X-Auth-Email": CloudflareEmail,
|
||||||
'X-Auth-Key': CloudflareKey
|
"X-Auth-Key": CloudflareKey
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ function getJSON(path, headers) {
|
||||||
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)
|
||||||
throw new Error('Failed to getJSON from Cloudflare')
|
throw new Error("Failed to getJSON from Cloudflare")
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.result
|
return data.result
|
||||||
|
@ -52,9 +52,9 @@ function reduceResults(target, values) {
|
||||||
Object.keys(values).forEach(key => {
|
Object.keys(values).forEach(key => {
|
||||||
const value = values[key]
|
const value = values[key]
|
||||||
|
|
||||||
if (typeof value === 'object' && value) {
|
if (typeof value === "object" && value) {
|
||||||
target[key] = reduceResults(target[key] || {}, value)
|
target[key] = reduceResults(target[key] || {}, value)
|
||||||
} else if (typeof value === 'number') {
|
} else if (typeof value === "number") {
|
||||||
target[key] = (target[key] || 0) + values[key]
|
target[key] = (target[key] || 0) + values[key]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -78,7 +78,7 @@ function getZoneAnalyticsDashboard(zones, since, until) {
|
||||||
|
|
||||||
function getJSONStream(path, headers) {
|
function getJSONStream(path, headers) {
|
||||||
const acceptGzipHeaders = Object.assign({}, headers, {
|
const acceptGzipHeaders = Object.assign({}, headers, {
|
||||||
'Accept-Encoding': 'gzip'
|
"Accept-Encoding": "gzip"
|
||||||
})
|
})
|
||||||
|
|
||||||
return get(path, acceptGzipHeaders)
|
return get(path, acceptGzipHeaders)
|
||||||
|
@ -91,9 +91,7 @@ function getJSONStream(path, headers) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLogs(zoneId, startTime, endTime) {
|
function getLogs(zoneId, startTime, endTime) {
|
||||||
return getJSONStream(
|
return getJSONStream(`/zones/${zoneId}/logs/requests?start=${startTime}&end=${endTime}`)
|
||||||
`/zones/${zoneId}/logs/requests?start=${startTime}&end=${endTime}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
const redis = require('redis')
|
const redis = require("redis")
|
||||||
|
|
||||||
redis.debug_mode = process.env.DEBUG_REDIS != null
|
redis.debug_mode = process.env.DEBUG_REDIS != null
|
||||||
|
|
||||||
const RedisURL =
|
const RedisURL = process.env.OPENREDIS_URL || process.env.REDIS_URL || "redis://localhost:6379"
|
||||||
process.env.OPENREDIS_URL || process.env.REDIS_URL || 'redis://localhost:6379'
|
|
||||||
|
|
||||||
const client = redis.createClient(RedisURL)
|
const client = redis.createClient(RedisURL)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const db = require('./RedisClient')
|
const db = require("./RedisClient")
|
||||||
const CloudflareAPI = require('./CloudflareAPI')
|
const CloudflareAPI = require("./CloudflareAPI")
|
||||||
const BlacklistAPI = require('./BlacklistAPI')
|
const BlacklistAPI = require("./BlacklistAPI")
|
||||||
|
|
||||||
function prunePackages(packagesMap) {
|
function prunePackages(packagesMap) {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
|
@ -38,7 +38,7 @@ function createScoresMap(array) {
|
||||||
|
|
||||||
function getScoresMap(key, n = 100) {
|
function getScoresMap(key, n = 100) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.zrevrange(key, 0, n, 'withscores', (error, value) => {
|
db.zrevrange(key, 0, n, "withscores", (error, value) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error)
|
reject(error)
|
||||||
} else {
|
} else {
|
||||||
|
@ -49,15 +49,11 @@ function getScoresMap(key, n = 100) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPackageRequests(date, n = 100) {
|
function getPackageRequests(date, n = 100) {
|
||||||
return getScoresMap(`stats-packageRequests-${createDayKey(date)}`, n).then(
|
return getScoresMap(`stats-packageRequests-${createDayKey(date)}`, n).then(prunePackages)
|
||||||
prunePackages
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPackageBandwidth(date, n = 100) {
|
function getPackageBandwidth(date, n = 100) {
|
||||||
return getScoresMap(`stats-packageBytes-${createDayKey(date)}`, n).then(
|
return getScoresMap(`stats-packageBytes-${createDayKey(date)}`, n).then(prunePackages)
|
||||||
prunePackages
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProtocolRequests(date) {
|
function getProtocolRequests(date) {
|
||||||
|
@ -90,9 +86,7 @@ function sumMaps(maps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDailyMetrics(result) {
|
function addDailyMetrics(result) {
|
||||||
return Promise.all(
|
return Promise.all(result.timeseries.map(addDailyMetricsToTimeseries)).then(() => {
|
||||||
result.timeseries.map(addDailyMetricsToTimeseries)
|
|
||||||
).then(() => {
|
|
||||||
result.totals.requests.package = sumMaps(
|
result.totals.requests.package = sumMaps(
|
||||||
result.timeseries.map(timeseries => {
|
result.timeseries.map(timeseries => {
|
||||||
return timeseries.requests.package
|
return timeseries.requests.package
|
||||||
|
@ -140,15 +134,11 @@ function extractPublicInfo(data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DomainNames = ['unpkg.com', 'npmcdn.com']
|
const DomainNames = ["unpkg.com", "npmcdn.com"]
|
||||||
|
|
||||||
function fetchStats(since, until) {
|
function fetchStats(since, until) {
|
||||||
return CloudflareAPI.getZones(DomainNames).then(zones => {
|
return CloudflareAPI.getZones(DomainNames).then(zones => {
|
||||||
return CloudflareAPI.getZoneAnalyticsDashboard(
|
return CloudflareAPI.getZoneAnalyticsDashboard(zones, since, until).then(dashboard => {
|
||||||
zones,
|
|
||||||
since,
|
|
||||||
until
|
|
||||||
).then(dashboard => {
|
|
||||||
return {
|
return {
|
||||||
timeseries: dashboard.timeseries.map(extractPublicInfo),
|
timeseries: dashboard.timeseries.map(extractPublicInfo),
|
||||||
totals: extractPublicInfo(dashboard.totals)
|
totals: extractPublicInfo(dashboard.totals)
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
const AuthAPI = require('../AuthAPI')
|
const AuthAPI = require("../AuthAPI")
|
||||||
|
|
||||||
describe('Auth API', () => {
|
describe("Auth API", () => {
|
||||||
beforeEach(done => {
|
beforeEach(done => {
|
||||||
AuthAPI.removeAllRevokedTokens().then(() => done(), done)
|
AuthAPI.removeAllRevokedTokens().then(() => done(), done)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates tokens with the right scopes', done => {
|
it("creates tokens with the right scopes", done => {
|
||||||
const scopes = {
|
const scopes = {
|
||||||
blacklist: {
|
blacklist: {
|
||||||
add: true,
|
add: true,
|
||||||
|
@ -24,7 +24,7 @@ describe('Auth API', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('refuses to verify revoked tokens', done => {
|
it("refuses to verify revoked tokens", done => {
|
||||||
const scopes = {}
|
const scopes = {}
|
||||||
|
|
||||||
AuthAPI.createToken(scopes).then(token => {
|
AuthAPI.createToken(scopes).then(token => {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
const BlacklistAPI = require('../BlacklistAPI')
|
const BlacklistAPI = require("../BlacklistAPI")
|
||||||
|
|
||||||
describe('Blacklist API', () => {
|
describe("Blacklist API", () => {
|
||||||
beforeEach(done => {
|
beforeEach(done => {
|
||||||
BlacklistAPI.removeAllPackages().then(() => done(), done)
|
BlacklistAPI.removeAllPackages().then(() => done(), done)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('adds and removes packages to/from the blacklist', done => {
|
it("adds and removes packages to/from the blacklist", done => {
|
||||||
const packageName = 'bad-package'
|
const packageName = "bad-package"
|
||||||
|
|
||||||
BlacklistAPI.addPackage(packageName).then(() => {
|
BlacklistAPI.addPackage(packageName).then(() => {
|
||||||
BlacklistAPI.getPackages().then(packageNames => {
|
BlacklistAPI.getPackages().then(packageNames => {
|
||||||
|
|
|
@ -1,49 +1,49 @@
|
||||||
const request = require('supertest')
|
const request = require("supertest")
|
||||||
const createServer = require('../createServer')
|
const createServer = require("../createServer")
|
||||||
const clearBlacklist = require('./utils/clearBlacklist')
|
const clearBlacklist = require("./utils/clearBlacklist")
|
||||||
const withBlacklist = require('./utils/withBlacklist')
|
const withBlacklist = require("./utils/withBlacklist")
|
||||||
const withRevokedToken = require('./utils/withRevokedToken')
|
const withRevokedToken = require("./utils/withRevokedToken")
|
||||||
const withToken = require('./utils/withToken')
|
const withToken = require("./utils/withToken")
|
||||||
|
|
||||||
describe('The server', () => {
|
describe("The server", () => {
|
||||||
let server
|
let server
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
server = createServer()
|
server = createServer()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('rejects invalid package names', done => {
|
it("rejects invalid package names", done => {
|
||||||
request(server)
|
request(server)
|
||||||
.get('/_invalid/index.js')
|
.get("/_invalid/index.js")
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.statusCode).toBe(403)
|
expect(res.statusCode).toBe(403)
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('redirects invalid query params', done => {
|
it("redirects invalid query params", done => {
|
||||||
request(server)
|
request(server)
|
||||||
.get('/react?main=index&invalid')
|
.get("/react?main=index&invalid")
|
||||||
.end((err, res) => {
|
.end((err, 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', done => {
|
it("redirects /_meta to ?meta", done => {
|
||||||
request(server)
|
request(server)
|
||||||
.get('/_meta/react?main=index')
|
.get("/_meta/react?main=index")
|
||||||
.end((err, res) => {
|
.end((err, 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()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not serve blacklisted packages', done => {
|
it("does not serve blacklisted packages", done => {
|
||||||
withBlacklist(['bad-package'], () => {
|
withBlacklist(["bad-package"], () => {
|
||||||
request(server)
|
request(server)
|
||||||
.get('/bad-package/index.js')
|
.get("/bad-package/index.js")
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.statusCode).toBe(403)
|
expect(res.statusCode).toBe(403)
|
||||||
done()
|
done()
|
||||||
|
@ -51,37 +51,37 @@ describe('The server', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('POST /_auth', () => {
|
describe("POST /_auth", () => {
|
||||||
it('creates a new auth token', done => {
|
it("creates a new auth token", done => {
|
||||||
request(server)
|
request(server)
|
||||||
.post('/_auth')
|
.post("/_auth")
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.body).toHaveProperty('token')
|
expect(res.body).toHaveProperty("token")
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('GET /_auth', () => {
|
describe("GET /_auth", () => {
|
||||||
describe('with no auth', () => {
|
describe("with no auth", () => {
|
||||||
it('echoes back null', done => {
|
it("echoes back null", done => {
|
||||||
request(server)
|
request(server)
|
||||||
.get('/_auth')
|
.get("/_auth")
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.body).toHaveProperty('auth')
|
expect(res.body).toHaveProperty("auth")
|
||||||
expect(res.body.auth).toBe(null)
|
expect(res.body.auth).toBe(null)
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with a revoked auth token', () => {
|
describe("with a revoked auth token", () => {
|
||||||
it('echoes back null', done => {
|
it("echoes back null", done => {
|
||||||
withRevokedToken({ some: { scope: true } }, token => {
|
withRevokedToken({ some: { scope: true } }, token => {
|
||||||
request(server)
|
request(server)
|
||||||
.get('/_auth?token=' + token)
|
.get("/_auth?token=" + token)
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.body).toHaveProperty('auth')
|
expect(res.body).toHaveProperty("auth")
|
||||||
expect(res.body.auth).toBe(null)
|
expect(res.body.auth).toBe(null)
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
|
@ -89,14 +89,14 @@ describe('The server', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with a valid auth token', () => {
|
describe("with a valid auth token", () => {
|
||||||
it('echoes back the auth payload', done => {
|
it("echoes back the auth payload", done => {
|
||||||
withToken({ some: { scope: true } }, token => {
|
withToken({ some: { scope: true } }, token => {
|
||||||
request(server)
|
request(server)
|
||||||
.get('/_auth?token=' + token)
|
.get("/_auth?token=" + token)
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.body).toHaveProperty('auth')
|
expect(res.body).toHaveProperty("auth")
|
||||||
expect(typeof res.body.auth).toBe('object')
|
expect(typeof res.body.auth).toBe("object")
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -104,10 +104,10 @@ describe('The server', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('GET /_publicKey', () => {
|
describe("GET /_publicKey", () => {
|
||||||
it('echoes the public key', done => {
|
it("echoes the public key", done => {
|
||||||
request(server)
|
request(server)
|
||||||
.get('/_publicKey')
|
.get("/_publicKey")
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.text).toMatch(/PUBLIC KEY/)
|
expect(res.text).toMatch(/PUBLIC KEY/)
|
||||||
done()
|
done()
|
||||||
|
@ -115,13 +115,13 @@ describe('The server', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('POST /_blacklist', () => {
|
describe("POST /_blacklist", () => {
|
||||||
afterEach(clearBlacklist)
|
afterEach(clearBlacklist)
|
||||||
|
|
||||||
describe('with no auth', () => {
|
describe("with no auth", () => {
|
||||||
it('is forbidden', done => {
|
it("is forbidden", done => {
|
||||||
request(server)
|
request(server)
|
||||||
.post('/_blacklist')
|
.post("/_blacklist")
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.statusCode).toBe(403)
|
expect(res.statusCode).toBe(403)
|
||||||
done()
|
done()
|
||||||
|
@ -130,16 +130,14 @@ describe('The server', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with the "blacklist.add" scope', () => {
|
describe('with the "blacklist.add" scope', () => {
|
||||||
it('can add to the blacklist', done => {
|
it("can add to the blacklist", done => {
|
||||||
withToken({ blacklist: { add: true } }, token => {
|
withToken({ blacklist: { add: true } }, token => {
|
||||||
request(server)
|
request(server)
|
||||||
.post('/_blacklist')
|
.post("/_blacklist")
|
||||||
.send({ token, packageName: 'bad-package' })
|
.send({ token, packageName: "bad-package" })
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.statusCode).toBe(200)
|
expect(res.statusCode).toBe(200)
|
||||||
expect(res.headers['content-location']).toEqual(
|
expect(res.headers["content-location"]).toEqual("/_blacklist/bad-package")
|
||||||
'/_blacklist/bad-package'
|
|
||||||
)
|
|
||||||
expect(res.body.ok).toBe(true)
|
expect(res.body.ok).toBe(true)
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
|
@ -148,11 +146,11 @@ describe('The server', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('GET /_blacklist', () => {
|
describe("GET /_blacklist", () => {
|
||||||
describe('with no auth', () => {
|
describe("with no auth", () => {
|
||||||
it('is forbidden', done => {
|
it("is forbidden", done => {
|
||||||
request(server)
|
request(server)
|
||||||
.get('/_blacklist')
|
.get("/_blacklist")
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.statusCode).toBe(403)
|
expect(res.statusCode).toBe(403)
|
||||||
done()
|
done()
|
||||||
|
@ -161,10 +159,10 @@ describe('The server', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with the "blacklist.read" scope', () => {
|
describe('with the "blacklist.read" scope', () => {
|
||||||
it('can read the blacklist', done => {
|
it("can read the blacklist", done => {
|
||||||
withToken({ blacklist: { read: true } }, token => {
|
withToken({ blacklist: { read: true } }, token => {
|
||||||
request(server)
|
request(server)
|
||||||
.get('/_blacklist?token=' + token)
|
.get("/_blacklist?token=" + token)
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.statusCode).toBe(200)
|
expect(res.statusCode).toBe(200)
|
||||||
done()
|
done()
|
||||||
|
@ -174,11 +172,11 @@ describe('The server', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('DELETE /_blacklist/:packageName', () => {
|
describe("DELETE /_blacklist/:packageName", () => {
|
||||||
describe('with no auth', () => {
|
describe("with no auth", () => {
|
||||||
it('is forbidden', done => {
|
it("is forbidden", done => {
|
||||||
request(server)
|
request(server)
|
||||||
.delete('/_blacklist/bad-package')
|
.delete("/_blacklist/bad-package")
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.statusCode).toBe(403)
|
expect(res.statusCode).toBe(403)
|
||||||
done()
|
done()
|
||||||
|
@ -187,10 +185,10 @@ describe('The server', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with the "blacklist.remove" scope', () => {
|
describe('with the "blacklist.remove" scope', () => {
|
||||||
it('can remove a package from the blacklist', done => {
|
it("can remove a package from the blacklist", done => {
|
||||||
withToken({ blacklist: { remove: true } }, token => {
|
withToken({ blacklist: { remove: true } }, token => {
|
||||||
request(server)
|
request(server)
|
||||||
.delete('/_blacklist/bad-package')
|
.delete("/_blacklist/bad-package")
|
||||||
.send({ token })
|
.send({ token })
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.statusCode).toBe(200)
|
expect(res.statusCode).toBe(200)
|
||||||
|
@ -200,10 +198,10 @@ describe('The server', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can remove a scoped package from the blacklist', done => {
|
it("can remove a scoped package from the blacklist", done => {
|
||||||
withToken({ blacklist: { remove: true } }, token => {
|
withToken({ blacklist: { remove: true } }, token => {
|
||||||
request(server)
|
request(server)
|
||||||
.delete('/_blacklist/@scope/bad-package')
|
.delete("/_blacklist/@scope/bad-package")
|
||||||
.send({ token })
|
.send({ token })
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.statusCode).toBe(200)
|
expect(res.statusCode).toBe(200)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const BlacklistAPI = require('../../BlacklistAPI')
|
const BlacklistAPI = require("../../BlacklistAPI")
|
||||||
|
|
||||||
function clearBlacklist(done) {
|
function clearBlacklist(done) {
|
||||||
BlacklistAPI.removeAllPackages().then(done, done)
|
BlacklistAPI.removeAllPackages().then(done, done)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const BlacklistAPI = require('../../BlacklistAPI')
|
const BlacklistAPI = require("../../BlacklistAPI")
|
||||||
|
|
||||||
function withBlacklist(blacklist, callback) {
|
function withBlacklist(blacklist, callback) {
|
||||||
return Promise.all(blacklist.map(BlacklistAPI.addPackage)).then(callback)
|
return Promise.all(blacklist.map(BlacklistAPI.addPackage)).then(callback)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const withToken = require('./withToken')
|
const withToken = require("./withToken")
|
||||||
const AuthAPI = require('../../AuthAPI')
|
const AuthAPI = require("../../AuthAPI")
|
||||||
|
|
||||||
function withRevokedToken(scopes, callback) {
|
function withRevokedToken(scopes, callback) {
|
||||||
withToken(scopes, token => {
|
withToken(scopes, token => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const AuthAPI = require('../../AuthAPI')
|
const AuthAPI = require("../../AuthAPI")
|
||||||
|
|
||||||
function withToken(scopes, callback) {
|
function withToken(scopes, callback) {
|
||||||
AuthAPI.createToken(scopes).then(callback)
|
AuthAPI.createToken(scopes).then(callback)
|
||||||
|
|
|
@ -1,20 +1,18 @@
|
||||||
const validateNpmPackageName = require('validate-npm-package-name')
|
const validateNpmPackageName = require("validate-npm-package-name")
|
||||||
const BlacklistAPI = require('../BlacklistAPI')
|
const BlacklistAPI = require("../BlacklistAPI")
|
||||||
|
|
||||||
function addToBlacklist(req, res) {
|
function addToBlacklist(req, res) {
|
||||||
const packageName = req.body.packageName
|
const packageName = req.body.packageName
|
||||||
|
|
||||||
if (!packageName) {
|
if (!packageName) {
|
||||||
return res
|
return res.status(403).send({ error: 'Missing "packageName" body parameter' })
|
||||||
.status(403)
|
|
||||||
.send({ error: 'Missing "packageName" body parameter' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameErrors = validateNpmPackageName(packageName).errors
|
const nameErrors = validateNpmPackageName(packageName).errors
|
||||||
|
|
||||||
// Disallow invalid package names.
|
// Disallow invalid package names.
|
||||||
if (nameErrors) {
|
if (nameErrors) {
|
||||||
const reason = nameErrors.join(', ')
|
const reason = nameErrors.join(", ")
|
||||||
return res.status(403).send({
|
return res.status(403).send({
|
||||||
error: `Invalid package name "${packageName}" (${reason})`
|
error: `Invalid package name "${packageName}" (${reason})`
|
||||||
})
|
})
|
||||||
|
@ -24,16 +22,12 @@ function addToBlacklist(req, res) {
|
||||||
added => {
|
added => {
|
||||||
if (added) {
|
if (added) {
|
||||||
const userId = req.user.jti
|
const userId = req.user.jti
|
||||||
console.log(
|
console.log(`Package "${packageName}" was added to the blacklist by ${userId}`)
|
||||||
`Package "${packageName}" was added to the blacklist by ${userId}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.set({ 'Content-Location': `/_blacklist/${packageName}` }).send({
|
res.set({ "Content-Location": `/_blacklist/${packageName}` }).send({
|
||||||
ok: true,
|
ok: true,
|
||||||
message: `Package "${packageName}" was ${added
|
message: `Package "${packageName}" was ${added ? "added to" : "already in"} the blacklist`
|
||||||
? 'added to'
|
|
||||||
: 'already in'} the blacklist`
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const AuthAPI = require('../AuthAPI')
|
const AuthAPI = require("../AuthAPI")
|
||||||
|
|
||||||
const defaultScopes = {
|
const defaultScopes = {
|
||||||
blacklist: {
|
blacklist: {
|
||||||
|
@ -15,7 +15,7 @@ function createAuth(req, res) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
||||||
res.status(500).send({
|
res.status(500).send({
|
||||||
error: 'Unable to generate auth token'
|
error: "Unable to generate auth token"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const validateNpmPackageName = require('validate-npm-package-name')
|
const validateNpmPackageName = require("validate-npm-package-name")
|
||||||
const BlacklistAPI = require('../BlacklistAPI')
|
const BlacklistAPI = require("../BlacklistAPI")
|
||||||
|
|
||||||
function removeFromBlacklist(req, res) {
|
function removeFromBlacklist(req, res) {
|
||||||
const packageName = req.packageName
|
const packageName = req.packageName
|
||||||
|
@ -8,16 +8,12 @@ function removeFromBlacklist(req, res) {
|
||||||
removed => {
|
removed => {
|
||||||
if (removed) {
|
if (removed) {
|
||||||
const userId = req.user.jti
|
const userId = req.user.jti
|
||||||
console.log(
|
console.log(`Package "${packageName}" was removed from the blacklist by ${userId}`)
|
||||||
`Package "${packageName}" was removed from the blacklist by ${userId}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
ok: true,
|
ok: true,
|
||||||
message: `Package "${packageName}" was ${
|
message: `Package "${packageName}" was ${removed ? "removed from" : "not in"} the blacklist`
|
||||||
removed ? 'removed from' : 'not in'
|
|
||||||
} the blacklist`
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const BlacklistAPI = require('../BlacklistAPI')
|
const BlacklistAPI = require("../BlacklistAPI")
|
||||||
|
|
||||||
function showBlacklist(req, res) {
|
function showBlacklist(req, res) {
|
||||||
BlacklistAPI.getPackages().then(
|
BlacklistAPI.getPackages().then(
|
||||||
|
@ -8,7 +8,7 @@ function showBlacklist(req, res) {
|
||||||
error => {
|
error => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
res.status(500).send({
|
res.status(500).send({
|
||||||
error: 'Unable to fetch blacklist'
|
error: "Unable to fetch blacklist"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const AuthAPI = require('../AuthAPI')
|
const AuthAPI = require("../AuthAPI")
|
||||||
|
|
||||||
function showPublicKey(req, res) {
|
function showPublicKey(req, res) {
|
||||||
res.send({ publicKey: AuthAPI.getPublicKey() })
|
res.send({ publicKey: AuthAPI.getPublicKey() })
|
||||||
|
|
|
@ -1,60 +1,56 @@
|
||||||
const subDays = require('date-fns/sub_days')
|
const subDays = require("date-fns/sub_days")
|
||||||
const startOfDay = require('date-fns/start_of_day')
|
const startOfDay = require("date-fns/start_of_day")
|
||||||
const startOfSecond = require('date-fns/start_of_second')
|
const startOfSecond = require("date-fns/start_of_second")
|
||||||
const StatsAPI = require('../StatsAPI')
|
const StatsAPI = require("../StatsAPI")
|
||||||
|
|
||||||
function showStats(req, res) {
|
function showStats(req, res) {
|
||||||
let since, until
|
let since, until
|
||||||
switch (req.query.period) {
|
switch (req.query.period) {
|
||||||
case 'last-day':
|
case "last-day":
|
||||||
until = startOfDay(new Date())
|
until = startOfDay(new Date())
|
||||||
since = subDays(until, 1)
|
since = subDays(until, 1)
|
||||||
break
|
break
|
||||||
case 'last-week':
|
case "last-week":
|
||||||
until = startOfDay(new Date())
|
until = startOfDay(new Date())
|
||||||
since = subDays(until, 7)
|
since = subDays(until, 7)
|
||||||
break
|
break
|
||||||
case 'last-month':
|
case "last-month":
|
||||||
until = startOfDay(new Date())
|
until = startOfDay(new Date())
|
||||||
since = subDays(until, 30)
|
since = subDays(until, 30)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
until = req.query.until
|
until = req.query.until ? new Date(req.query.until) : startOfSecond(new Date())
|
||||||
? new Date(req.query.until)
|
|
||||||
: startOfSecond(new Date())
|
|
||||||
since = new Date(req.query.since)
|
since = new Date(req.query.since)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNaN(since.getTime())) {
|
if (isNaN(since.getTime())) {
|
||||||
return res.status(403).send({ error: '?since is not a valid date' })
|
return res.status(403).send({ error: "?since is not a valid date" })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNaN(until.getTime())) {
|
if (isNaN(until.getTime())) {
|
||||||
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
|
return res.status(403).send({ error: "?until date must come after ?since date" })
|
||||||
.status(403)
|
|
||||||
.send({ error: '?until date must come after ?since date' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (until >= new Date()) {
|
if (until >= new Date()) {
|
||||||
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" })
|
||||||
}
|
}
|
||||||
|
|
||||||
StatsAPI.getStats(since, until).then(
|
StatsAPI.getStats(since, until).then(
|
||||||
stats => {
|
stats => {
|
||||||
res
|
res
|
||||||
.set({
|
.set({
|
||||||
'Cache-Control': 'public, max-age=60',
|
"Cache-Control": "public, max-age=60",
|
||||||
'Cache-Tag': 'stats'
|
"Cache-Tag": "stats"
|
||||||
})
|
})
|
||||||
.send(stats)
|
.send(stats)
|
||||||
},
|
},
|
||||||
error => {
|
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" })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
const fs = require('fs')
|
const fs = require("fs")
|
||||||
const path = require('path')
|
const path = require("path")
|
||||||
const express = require('express')
|
const express = require("express")
|
||||||
const bodyParser = require('body-parser')
|
const bodyParser = require("body-parser")
|
||||||
const cors = require('cors')
|
const cors = require("cors")
|
||||||
const morgan = require('morgan')
|
const morgan = require("morgan")
|
||||||
|
|
||||||
const checkBlacklist = require('./middleware/checkBlacklist')
|
const checkBlacklist = require("./middleware/checkBlacklist")
|
||||||
const fetchFile = require('./middleware/fetchFile')
|
const fetchFile = require("./middleware/fetchFile")
|
||||||
const parseURL = require('./middleware/parseURL')
|
const parseURL = require("./middleware/parseURL")
|
||||||
const requireAuth = require('./middleware/requireAuth')
|
const requireAuth = require("./middleware/requireAuth")
|
||||||
const serveFile = require('./middleware/serveFile')
|
const serveFile = require("./middleware/serveFile")
|
||||||
const userToken = require('./middleware/userToken')
|
const userToken = require("./middleware/userToken")
|
||||||
const validatePackageURL = require('./middleware/validatePackageURL')
|
const validatePackageURL = require("./middleware/validatePackageURL")
|
||||||
|
|
||||||
morgan.token('fwd', function(req) {
|
morgan.token("fwd", function(req) {
|
||||||
return req.get('x-forwarded-for').replace(/\s/g, '')
|
return req.get("x-forwarded-for").replace(/\s/g, "")
|
||||||
})
|
})
|
||||||
|
|
||||||
function errorHandler(err, req, res, next) {
|
function errorHandler(err, req, res, next) {
|
||||||
|
@ -22,8 +22,8 @@ function errorHandler(err, req, res, next) {
|
||||||
|
|
||||||
res
|
res
|
||||||
.status(500)
|
.status(500)
|
||||||
.type('text')
|
.type("text")
|
||||||
.send('Internal Server Error')
|
.send("Internal Server Error")
|
||||||
|
|
||||||
next(err)
|
next(err)
|
||||||
}
|
}
|
||||||
|
@ -37,16 +37,16 @@ function createRouter(setup) {
|
||||||
function createServer() {
|
function createServer() {
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
app.disable('x-powered-by')
|
app.disable("x-powered-by")
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== "test") {
|
||||||
app.use(
|
app.use(
|
||||||
morgan(
|
morgan(
|
||||||
process.env.NODE_ENV === 'production'
|
process.env.NODE_ENV === "production"
|
||||||
? // Modified version of the Heroku router's log format
|
? // Modified version of the Heroku router's log format
|
||||||
// https://devcenter.heroku.com/articles/http-routing#heroku-router-log-format
|
// https://devcenter.heroku.com/articles/http-routing#heroku-router-log-format
|
||||||
'method=:method path=":url" host=:req[host] request_id=:req[x-request-id] cf_ray=:req[cf-ray] fwd=:fwd status=:status bytes=:res[content-length]'
|
'method=:method path=":url" host=:req[host] request_id=:req[x-request-id] cf_ray=:req[cf-ray] fwd=:fwd status=:status bytes=:res[content-length]'
|
||||||
: 'dev'
|
: "dev"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -54,8 +54,8 @@ function createServer() {
|
||||||
app.use(errorHandler)
|
app.use(errorHandler)
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
express.static('build', {
|
express.static("build", {
|
||||||
maxAge: '365d'
|
maxAge: "365d"
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -63,43 +63,35 @@ function createServer() {
|
||||||
app.use(bodyParser.json())
|
app.use(bodyParser.json())
|
||||||
app.use(userToken)
|
app.use(userToken)
|
||||||
|
|
||||||
app.get('/_publicKey', require('./actions/showPublicKey'))
|
app.get("/_publicKey", require("./actions/showPublicKey"))
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
'/_auth',
|
"/_auth",
|
||||||
createRouter(app => {
|
createRouter(app => {
|
||||||
app.post('/', require('./actions/createAuth'))
|
app.post("/", require("./actions/createAuth"))
|
||||||
app.get('/', require('./actions/showAuth'))
|
app.get("/", require("./actions/showAuth"))
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
'/_blacklist',
|
"/_blacklist",
|
||||||
createRouter(app => {
|
createRouter(app => {
|
||||||
app.post(
|
app.post("/", requireAuth("blacklist.add"), require("./actions/addToBlacklist"))
|
||||||
'/',
|
app.get("/", requireAuth("blacklist.read"), require("./actions/showBlacklist"))
|
||||||
requireAuth('blacklist.add'),
|
|
||||||
require('./actions/addToBlacklist')
|
|
||||||
)
|
|
||||||
app.get(
|
|
||||||
'/',
|
|
||||||
requireAuth('blacklist.read'),
|
|
||||||
require('./actions/showBlacklist')
|
|
||||||
)
|
|
||||||
app.delete(
|
app.delete(
|
||||||
/.*/,
|
/.*/,
|
||||||
requireAuth('blacklist.remove'),
|
requireAuth("blacklist.remove"),
|
||||||
validatePackageURL,
|
validatePackageURL,
|
||||||
require('./actions/removeFromBlacklist')
|
require("./actions/removeFromBlacklist")
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== "test") {
|
||||||
app.get('/_stats', require('./actions/showStats'))
|
app.get("/_stats", require("./actions/showStats"))
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use('/', parseURL, checkBlacklist, fetchFile, serveFile)
|
app.use("/", parseURL, checkBlacklist, fetchFile, serveFile)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
const parseURL = require('url').parse
|
const parseURL = require("url").parse
|
||||||
const startOfDay = require('date-fns/start_of_day')
|
const startOfDay = require("date-fns/start_of_day")
|
||||||
const addDays = require('date-fns/add_days')
|
const addDays = require("date-fns/add_days")
|
||||||
const parsePackageURL = require('./utils/parsePackageURL')
|
const parsePackageURL = require("./utils/parsePackageURL")
|
||||||
const CloudflareAPI = require('./CloudflareAPI')
|
const CloudflareAPI = require("./CloudflareAPI")
|
||||||
const StatsAPI = require('./StatsAPI')
|
const StatsAPI = require("./StatsAPI")
|
||||||
|
|
||||||
const db = require('./RedisClient')
|
const db = require("./RedisClient")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Domains we want to analyze.
|
* Domains we want to analyze.
|
||||||
*/
|
*/
|
||||||
const DomainNames = [
|
const DomainNames = [
|
||||||
'unpkg.com'
|
"unpkg.com"
|
||||||
//'npmcdn.com' // We don't have log data on npmcdn.com yet :/
|
//'npmcdn.com' // We don't have log data on npmcdn.com yet :/
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -48,8 +48,8 @@ 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))
|
||||||
|
@ -67,26 +67,15 @@ function computeCounters(stream) {
|
||||||
const packageName = url && url.packageName
|
const packageName = url && url.packageName
|
||||||
|
|
||||||
if (packageName) {
|
if (packageName) {
|
||||||
incr(
|
incr(`stats-packageRequests-${dayKey}`, packageName, 1, thirtyDaysLater)
|
||||||
`stats-packageRequests-${dayKey}`,
|
incr(`stats-packageBytes-${dayKey}`, packageName, edgeResponse.bytes, thirtyDaysLater)
|
||||||
packageName,
|
|
||||||
1,
|
|
||||||
thirtyDaysLater
|
|
||||||
)
|
|
||||||
incr(
|
|
||||||
`stats-packageBytes-${dayKey}`,
|
|
||||||
packageName,
|
|
||||||
edgeResponse.bytes,
|
|
||||||
thirtyDaysLater
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Q: How many requests per day do we receive via a protocol?
|
// Q: How many requests per day do we receive via a protocol?
|
||||||
const protocol = clientRequest.httpProtocol
|
const protocol = clientRequest.httpProtocol
|
||||||
|
|
||||||
if (protocol)
|
if (protocol) incr(`stats-protocolRequests-${dayKey}`, protocol, 1, thirtyDaysLater)
|
||||||
incr(`stats-protocolRequests-${dayKey}`, protocol, 1, thirtyDaysLater)
|
|
||||||
|
|
||||||
// Q: How many requests do we receive from a hostname per day?
|
// Q: How many requests do we receive from a hostname per day?
|
||||||
// Q: How many bytes do we serve to a hostname per day?
|
// Q: How many bytes do we serve to a hostname per day?
|
||||||
|
@ -95,15 +84,10 @@ function computeCounters(stream) {
|
||||||
|
|
||||||
if (hostname) {
|
if (hostname) {
|
||||||
incr(`stats-hostnameRequests-${dayKey}`, hostname, 1, sevenDaysLater)
|
incr(`stats-hostnameRequests-${dayKey}`, hostname, 1, sevenDaysLater)
|
||||||
incr(
|
incr(`stats-hostnameBytes-${dayKey}`, hostname, edgeResponse.bytes, sevenDaysLater)
|
||||||
`stats-hostnameBytes-${dayKey}`,
|
|
||||||
hostname,
|
|
||||||
edgeResponse.bytes,
|
|
||||||
sevenDaysLater
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('end', function() {
|
.on("end", function() {
|
||||||
resolve({ counters, expireat })
|
resolve({ counters, expireat })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -126,7 +110,7 @@ function processLogs(stream) {
|
||||||
function ingestLogs(zone, startSeconds, endSeconds) {
|
function ingestLogs(zone, startSeconds, endSeconds) {
|
||||||
return new Promise(resolve => {
|
return new Promise(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,
|
||||||
stringifySeconds(startSeconds),
|
stringifySeconds(startSeconds),
|
||||||
stringifySeconds(endSeconds)
|
stringifySeconds(endSeconds)
|
||||||
|
@ -139,7 +123,7 @@ function ingestLogs(zone, startSeconds, endSeconds) {
|
||||||
const endFetchTime = Date.now()
|
const endFetchTime = Date.now()
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'info: Fetched %ds worth of logs for %s in %dms',
|
"info: Fetched %ds worth of logs for %s in %dms",
|
||||||
endSeconds - startSeconds,
|
endSeconds - startSeconds,
|
||||||
zone.name,
|
zone.name,
|
||||||
endFetchTime - startFetchTime
|
endFetchTime - startFetchTime
|
||||||
|
@ -151,7 +135,7 @@ function ingestLogs(zone, startSeconds, endSeconds) {
|
||||||
const endProcessTime = Date.now()
|
const endProcessTime = Date.now()
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'info: Processed %ds worth of logs for %s in %dms',
|
"info: Processed %ds worth of logs for %s in %dms",
|
||||||
endSeconds - startSeconds,
|
endSeconds - startSeconds,
|
||||||
zone.name,
|
zone.name,
|
||||||
endProcessTime - startProcessTime
|
endProcessTime - startProcessTime
|
||||||
|
@ -163,10 +147,7 @@ function ingestLogs(zone, startSeconds, endSeconds) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function startZone(zone) {
|
function startZone(zone) {
|
||||||
const startSecondsKey = `ingestLogsWorker-nextStartSeconds-${zone.name.replace(
|
const startSecondsKey = `ingestLogsWorker-nextStartSeconds-${zone.name.replace(".", "-")}`
|
||||||
'.',
|
|
||||||
'-'
|
|
||||||
)}`
|
|
||||||
|
|
||||||
function takeATurn() {
|
function takeATurn() {
|
||||||
db.get(startSecondsKey, function(error, value) {
|
db.get(startSecondsKey, function(error, value) {
|
||||||
|
@ -182,7 +163,7 @@ function startZone(zone) {
|
||||||
startSeconds = minSeconds
|
startSeconds = minSeconds
|
||||||
} else if (startSeconds < minSeconds) {
|
} else if (startSeconds < minSeconds) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'warning: Dropped logs for %s from %s to %s!',
|
"warning: Dropped logs for %s from %s to %s!",
|
||||||
zone.name,
|
zone.name,
|
||||||
stringifySeconds(startSeconds),
|
stringifySeconds(startSeconds),
|
||||||
stringifySeconds(minSeconds)
|
stringifySeconds(minSeconds)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const BlacklistAPI = require('../BlacklistAPI')
|
const BlacklistAPI = require("../BlacklistAPI")
|
||||||
|
|
||||||
function checkBlacklist(req, res, next) {
|
function checkBlacklist(req, res, next) {
|
||||||
BlacklistAPI.includesPackage(req.packageName).then(
|
BlacklistAPI.includesPackage(req.packageName).then(
|
||||||
|
@ -7,7 +7,7 @@ function checkBlacklist(req, res, next) {
|
||||||
if (blacklisted) {
|
if (blacklisted) {
|
||||||
res
|
res
|
||||||
.status(403)
|
.status(403)
|
||||||
.type('text')
|
.type("text")
|
||||||
.send(`Package "${req.packageName}" is blacklisted`)
|
.send(`Package "${req.packageName}" is blacklisted`)
|
||||||
} else {
|
} else {
|
||||||
next()
|
next()
|
||||||
|
@ -17,7 +17,7 @@ function checkBlacklist(req, res, next) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
||||||
res.status(500).send({
|
res.status(500).send({
|
||||||
error: 'Unable to fetch the blacklist'
|
error: "Unable to fetch the blacklist"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const React = require('react')
|
const React = require("react")
|
||||||
const prettyBytes = require('pretty-bytes')
|
const prettyBytes = require("pretty-bytes")
|
||||||
const getFileContentType = require('../utils/getFileContentType')
|
const getFileContentType = require("../utils/getFileContentType")
|
||||||
|
|
||||||
const e = React.createElement
|
const e = React.createElement
|
||||||
|
|
||||||
|
@ -9,46 +9,46 @@ const formatTime = time => 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 e(
|
return e(
|
||||||
'tr',
|
"tr",
|
||||||
{ key: file, className: index % 2 ? 'odd' : 'even' },
|
{ 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(
|
e(
|
||||||
'tr',
|
"tr",
|
||||||
{ key: '..', className: 'odd' },
|
{ 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, "-"),
|
||||||
e('td', null, '-')
|
e("td", null, "-")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return e(
|
return e(
|
||||||
'table',
|
"table",
|
||||||
null,
|
null,
|
||||||
e(
|
e(
|
||||||
'thead',
|
"thead",
|
||||||
null,
|
null,
|
||||||
e(
|
e(
|
||||||
'tr',
|
"tr",
|
||||||
null,
|
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"),
|
||||||
e('th', null, 'Last Modified')
|
e("th", null, "Last Modified")
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
e('tbody', null, rows)
|
e("tbody", null, rows)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
const React = require('react')
|
const React = require("react")
|
||||||
const semver = require('semver')
|
const semver = require("semver")
|
||||||
const DirectoryListing = require('./DirectoryListing')
|
const DirectoryListing = require("./DirectoryListing")
|
||||||
const readCSS = require('../utils/readCSS')
|
const readCSS = require("../utils/readCSS")
|
||||||
|
|
||||||
const e = React.createElement
|
const e = React.createElement
|
||||||
|
|
||||||
const IndexPageStyle = readCSS(__dirname, 'IndexPage.css')
|
const IndexPageStyle = readCSS(__dirname, "IndexPage.css")
|
||||||
const IndexPageScript = `
|
const IndexPageScript = `
|
||||||
var s = document.getElementById('version'), v = s.value
|
var s = document.getElementById('version'), v = s.value
|
||||||
s.onchange = function () {
|
s.onchange = function () {
|
||||||
|
@ -17,37 +17,35 @@ const byVersion = (a, b) => (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 e(
|
return e(
|
||||||
'html',
|
"html",
|
||||||
null,
|
null,
|
||||||
e(
|
e(
|
||||||
'head',
|
"head",
|
||||||
null,
|
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(
|
e(
|
||||||
'body',
|
"body",
|
||||||
null,
|
null,
|
||||||
e(
|
e(
|
||||||
'div',
|
"div",
|
||||||
{ className: 'content-wrapper' },
|
{ className: "content-wrapper" },
|
||||||
e(
|
e(
|
||||||
'div',
|
"div",
|
||||||
{ className: 'version-wrapper' },
|
{ 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}`),
|
||||||
e('script', { dangerouslySetInnerHTML: { __html: IndexPageScript } }),
|
e("script", { dangerouslySetInnerHTML: { __html: IndexPageScript } }),
|
||||||
e('hr'),
|
e("hr"),
|
||||||
e(DirectoryListing, { dir, entries }),
|
e(DirectoryListing, { dir, entries }),
|
||||||
e('hr'),
|
e("hr"),
|
||||||
e('address', null, `${packageInfo.name}@${version}`)
|
e("address", null, `${packageInfo.name}@${version}`)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
const fs = require('fs')
|
const fs = require("fs")
|
||||||
const path = require('path')
|
const path = require("path")
|
||||||
const semver = require('semver')
|
const semver = require("semver")
|
||||||
const createPackageURL = require('../utils/createPackageURL')
|
const createPackageURL = require("../utils/createPackageURL")
|
||||||
const createSearch = require('./utils/createSearch')
|
const createSearch = require("./utils/createSearch")
|
||||||
const getPackageInfo = require('./utils/getPackageInfo')
|
const getPackageInfo = require("./utils/getPackageInfo")
|
||||||
const getPackage = require('./utils/getPackage')
|
const getPackage = require("./utils/getPackage")
|
||||||
const incrementCounter = require('./utils/incrementCounter')
|
const incrementCounter = require("./utils/incrementCounter")
|
||||||
|
|
||||||
function getBasename(file) {
|
function getBasename(file) {
|
||||||
return path.basename(file, path.extname(file))
|
return path.basename(file, path.extname(file))
|
||||||
|
@ -14,7 +14,7 @@ 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"
|
||||||
|
@ -27,17 +27,13 @@ function findFile(base, useIndex, callback) {
|
||||||
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()
|
||||||
} else {
|
} else {
|
||||||
callback(error)
|
callback(error)
|
||||||
}
|
}
|
||||||
} else if (useIndex && stats.isDirectory()) {
|
} else if (useIndex && stats.isDirectory()) {
|
||||||
findFile(path.join(file, 'index'), false, function(
|
findFile(path.join(file, "index"), false, function(error, indexFile, indexStats) {
|
||||||
error,
|
|
||||||
indexFile,
|
|
||||||
indexStats
|
|
||||||
) {
|
|
||||||
if (error) {
|
if (error) {
|
||||||
callback(error)
|
callback(error)
|
||||||
} else if (indexFile) {
|
} else if (indexFile) {
|
||||||
|
@ -65,14 +61,14 @@ function fetchFile(req, res, next) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
return res
|
return res
|
||||||
.status(500)
|
.status(500)
|
||||||
.type('text')
|
.type("text")
|
||||||
.send(`Cannot get info for package "${req.packageName}"`)
|
.send(`Cannot get info for package "${req.packageName}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packageInfo == null || packageInfo.versions == null)
|
if (packageInfo == null || packageInfo.versions == null)
|
||||||
return res
|
return res
|
||||||
.status(404)
|
.status(404)
|
||||||
.type('text')
|
.type("text")
|
||||||
.send(`Cannot find package "${req.packageName}"`)
|
.send(`Cannot find package "${req.packageName}"`)
|
||||||
|
|
||||||
req.packageInfo = packageInfo
|
req.packageInfo = packageInfo
|
||||||
|
@ -86,7 +82,7 @@ function fetchFile(req, res, next) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
res
|
res
|
||||||
.status(500)
|
.status(500)
|
||||||
.type('text')
|
.type("text")
|
||||||
.send(`Cannot fetch package ${req.packageSpec}`)
|
.send(`Cannot fetch package ${req.packageSpec}`)
|
||||||
} else {
|
} else {
|
||||||
req.packageDir = outputDir
|
req.packageDir = outputDir
|
||||||
|
@ -98,77 +94,56 @@ 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 =
|
filename = req.packageConfig.module || req.packageConfig["jsnext:main"] || "/"
|
||||||
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 (
|
} else if (req.query.main && typeof req.packageConfig[req.query.main] === "string") {
|
||||||
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]
|
||||||
|
|
||||||
incrementCounter(
|
incrementCounter(
|
||||||
'package-json-custom-main',
|
"package-json-custom-main",
|
||||||
req.packageSpec + '?main=' + req.query.main,
|
req.packageSpec + "?main=" + req.query.main,
|
||||||
1
|
1
|
||||||
)
|
)
|
||||||
} else if (typeof req.packageConfig.unpkg === 'string') {
|
} else if (typeof req.packageConfig.unpkg === "string") {
|
||||||
// The "unpkg" field allows packages to explicitly declare the
|
// The "unpkg" field allows packages to explicitly declare the
|
||||||
// file to serve at the bare URL (see #59).
|
// file to serve at the bare URL (see #59).
|
||||||
filename = req.packageConfig.unpkg
|
filename = req.packageConfig.unpkg
|
||||||
} else if (typeof req.packageConfig.browser === 'string') {
|
} else if (typeof req.packageConfig.browser === "string") {
|
||||||
// Fall back to the "browser" field if declared (only support strings).
|
// Fall back to the "browser" field if declared (only support strings).
|
||||||
filename = req.packageConfig.browser
|
filename = req.packageConfig.browser
|
||||||
|
|
||||||
// Count which packages + versions are actually using this fallback
|
// Count which packages + versions are actually using this fallback
|
||||||
// so we can warn them when we deprecate this functionality.
|
// so we can warn them when we deprecate this functionality.
|
||||||
// See https://github.com/unpkg/unpkg/issues/63
|
// See https://github.com/unpkg/unpkg/issues/63
|
||||||
incrementCounter(
|
incrementCounter("package-json-browser-fallback", req.packageSpec, 1)
|
||||||
'package-json-browser-fallback',
|
|
||||||
req.packageSpec,
|
|
||||||
1
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
// Fall back to "main" or / (same as npm).
|
// Fall back to "main" or / (same as npm).
|
||||||
filename = req.packageConfig.main || '/'
|
filename = req.packageConfig.main || "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
findFile(path.join(req.packageDir, filename), useIndex, function(
|
findFile(path.join(req.packageDir, filename), useIndex, function(error, file, stats) {
|
||||||
error,
|
|
||||||
file,
|
|
||||||
stats
|
|
||||||
) {
|
|
||||||
if (error) console.error(error)
|
if (error) console.error(error)
|
||||||
|
|
||||||
if (file == null)
|
if (file == null)
|
||||||
return res
|
return res
|
||||||
.status(404)
|
.status(404)
|
||||||
.type('text')
|
.type("text")
|
||||||
.send(
|
.send(`Cannot find module "${filename}" in package ${req.packageSpec}`)
|
||||||
`Cannot find module "${filename}" in package ${
|
|
||||||
req.packageSpec
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
|
|
||||||
filename = file.replace(req.packageDir, '')
|
filename = file.replace(req.packageDir, "")
|
||||||
|
|
||||||
if (
|
if (req.query.main != null || getBasename(req.filename) !== getBasename(filename)) {
|
||||||
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
|
res
|
||||||
.set({
|
.set({
|
||||||
'Cache-Control': 'public, max-age=60',
|
"Cache-Control": "public, max-age=60",
|
||||||
'Cache-Tag': 'redirect,module-redirect'
|
"Cache-Tag": "redirect,module-redirect"
|
||||||
})
|
})
|
||||||
.redirect(
|
.redirect(
|
||||||
302,
|
302,
|
||||||
|
@ -187,18 +162,18 @@ 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
|
res
|
||||||
.set({
|
.set({
|
||||||
'Cache-Control': 'public, max-age=60',
|
"Cache-Control": "public, max-age=60",
|
||||||
'Cache-Tag': 'redirect,tag-redirect'
|
"Cache-Tag": "redirect,tag-redirect"
|
||||||
})
|
})
|
||||||
.redirect(
|
.redirect(
|
||||||
302,
|
302,
|
||||||
createPackageURL(
|
createPackageURL(
|
||||||
req.packageName,
|
req.packageName,
|
||||||
req.packageInfo['dist-tags'][req.packageVersion],
|
req.packageInfo["dist-tags"][req.packageVersion],
|
||||||
req.filename,
|
req.filename,
|
||||||
req.search
|
req.search
|
||||||
)
|
)
|
||||||
|
@ -213,22 +188,14 @@ function fetchFile(req, res, next) {
|
||||||
// Cache semver redirects for 1 minute.
|
// Cache semver redirects for 1 minute.
|
||||||
res
|
res
|
||||||
.set({
|
.set({
|
||||||
'Cache-Control': 'public, max-age=60',
|
"Cache-Control": "public, max-age=60",
|
||||||
'Cache-Tag': 'redirect,semver-redirect'
|
"Cache-Tag": "redirect,semver-redirect"
|
||||||
})
|
})
|
||||||
.redirect(
|
.redirect(302, createPackageURL(req.packageName, maxVersion, req.filename, req.search))
|
||||||
302,
|
|
||||||
createPackageURL(
|
|
||||||
req.packageName,
|
|
||||||
maxVersion,
|
|
||||||
req.filename,
|
|
||||||
req.search
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
res
|
res
|
||||||
.status(404)
|
.status(404)
|
||||||
.type('text')
|
.type("text")
|
||||||
.send(`Cannot find package ${req.packageSpec}`)
|
.send(`Cannot find package ${req.packageSpec}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const validateNpmPackageName = require('validate-npm-package-name')
|
const validateNpmPackageName = require("validate-npm-package-name")
|
||||||
const parsePackageURL = require('../utils/parsePackageURL')
|
const parsePackageURL = require("../utils/parsePackageURL")
|
||||||
const createSearch = require('./utils/createSearch')
|
const createSearch = require("./utils/createSearch")
|
||||||
|
|
||||||
const KnownQueryParams = {
|
const KnownQueryParams = {
|
||||||
main: true,
|
main: true,
|
||||||
|
@ -32,14 +32,14 @@ function sanitizeQuery(query) {
|
||||||
function parseURL(req, res, next) {
|
function parseURL(req, res, next) {
|
||||||
// Redirect /_meta/path to /path?meta.
|
// Redirect /_meta/path to /path?meta.
|
||||||
if (req.path.match(/^\/_meta\//)) {
|
if (req.path.match(/^\/_meta\//)) {
|
||||||
req.query.meta = ''
|
req.query.meta = ""
|
||||||
return res.redirect(302, req.path.substr(6) + createSearch(req.query))
|
return res.redirect(302, req.path.substr(6) + createSearch(req.query))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect /path?json => /path?meta
|
// Redirect /path?json => /path?meta
|
||||||
if (req.query.json != null) {
|
if (req.query.json != null) {
|
||||||
delete req.query.json
|
delete req.query.json
|
||||||
req.query.meta = ''
|
req.query.meta = ""
|
||||||
return res.redirect(302, req.path + createSearch(req.query))
|
return res.redirect(302, req.path + createSearch(req.query))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ function parseURL(req, res, next) {
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
return res
|
return res
|
||||||
.status(403)
|
.status(403)
|
||||||
.type('text')
|
.type("text")
|
||||||
.send(`Invalid URL: ${req.url}`)
|
.send(`Invalid URL: ${req.url}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,10 +64,10 @@ function parseURL(req, res, next) {
|
||||||
|
|
||||||
// Disallow invalid package names.
|
// Disallow invalid package names.
|
||||||
if (nameErrors) {
|
if (nameErrors) {
|
||||||
const reason = nameErrors.join(', ')
|
const reason = nameErrors.join(", ")
|
||||||
return res
|
return res
|
||||||
.status(403)
|
.status(403)
|
||||||
.type('text')
|
.type("text")
|
||||||
.send(`Invalid package name "${url.packageName}" (${reason})`)
|
.send(`Invalid package name "${url.packageName}" (${reason})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,9 @@
|
||||||
*/
|
*/
|
||||||
function requireAuth(scope) {
|
function requireAuth(scope) {
|
||||||
let checkScopes
|
let checkScopes
|
||||||
if (scope.includes('.')) {
|
if (scope.includes(".")) {
|
||||||
const parts = scope.split('.')
|
const parts = scope.split(".")
|
||||||
checkScopes = scopes =>
|
checkScopes = scopes => parts.reduce((memo, part) => memo && memo[part], scopes) != null
|
||||||
parts.reduce((memo, part) => memo && memo[part], scopes) != null
|
|
||||||
} else {
|
} else {
|
||||||
checkScopes = scopes => scopes[scope] != null
|
checkScopes = scopes => scopes[scope] != null
|
||||||
}
|
}
|
||||||
|
@ -20,11 +19,11 @@ function requireAuth(scope) {
|
||||||
const user = req.user
|
const user = req.user
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(403).send({ error: 'Missing auth token' })
|
return res.status(403).send({ error: "Missing auth token" })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.scopes || !checkScopes(user.scopes)) {
|
if (!user.scopes || !checkScopes(user.scopes)) {
|
||||||
return res.status(403).send({ error: 'Insufficient scopes' })
|
return res.status(403).send({ error: "Insufficient scopes" })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.auth) {
|
if (req.auth) {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
const fs = require('fs')
|
const fs = require("fs")
|
||||||
const path = require('path')
|
const path = require("path")
|
||||||
const etag = require('etag')
|
const etag = require("etag")
|
||||||
const babel = require('babel-core')
|
const babel = require("babel-core")
|
||||||
const unpkgRewrite = require('babel-plugin-unpkg-rewrite')
|
const unpkgRewrite = require("babel-plugin-unpkg-rewrite")
|
||||||
const getMetadata = require('./utils/getMetadata')
|
const getMetadata = require("./utils/getMetadata")
|
||||||
const getFileContentType = require('./utils/getFileContentType')
|
const getFileContentType = require("./utils/getFileContentType")
|
||||||
const getIndexHTML = require('./utils/getIndexHTML')
|
const getIndexHTML = require("./utils/getIndexHTML")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatically generate HTML pages that show package contents.
|
* Automatically generate HTML pages that show package contents.
|
||||||
|
@ -35,24 +35,19 @@ const FileTransforms = {
|
||||||
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(
|
getMetadata(req.packageDir, req.filename, req.stats, MaximumDepth, function(error, metadata) {
|
||||||
error,
|
|
||||||
metadata
|
|
||||||
) {
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
res
|
res
|
||||||
.status(500)
|
.status(500)
|
||||||
.type('text')
|
.type("text")
|
||||||
.send(
|
.send(`Cannot generate metadata for ${req.packageSpec}${req.filename}`)
|
||||||
`Cannot generate metadata for ${req.packageSpec}${req.filename}`
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
// Cache metadata for 1 year.
|
// Cache metadata for 1 year.
|
||||||
res
|
res
|
||||||
.set({
|
.set({
|
||||||
'Cache-Control': 'public, max-age=31536000',
|
"Cache-Control": "public, max-age=31536000",
|
||||||
'Cache-Tag': 'meta'
|
"Cache-Tag": "meta"
|
||||||
})
|
})
|
||||||
.send(metadata)
|
.send(metadata)
|
||||||
}
|
}
|
||||||
|
@ -63,9 +58,9 @@ function serveFile(req, res, next) {
|
||||||
|
|
||||||
let contentType = getFileContentType(file)
|
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) {
|
if (contentType === "application/javascript" && req.query.module != null) {
|
||||||
// Serve a JavaScript module.
|
// Serve a JavaScript module.
|
||||||
const dependencies = Object.assign(
|
const dependencies = Object.assign(
|
||||||
{},
|
{},
|
||||||
|
@ -78,33 +73,29 @@ function serveFile(req, res, next) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
const debugInfo =
|
const debugInfo =
|
||||||
error.constructor.name +
|
error.constructor.name +
|
||||||
': ' +
|
": " +
|
||||||
error.message.replace(/^.*?\/unpkg-.+?\//, `/${req.packageSpec}/`) +
|
error.message.replace(/^.*?\/unpkg-.+?\//, `/${req.packageSpec}/`) +
|
||||||
'\n\n' +
|
"\n\n" +
|
||||||
error.codeFrame
|
error.codeFrame
|
||||||
res
|
res
|
||||||
.status(500)
|
.status(500)
|
||||||
.type('text')
|
.type("text")
|
||||||
.send(
|
.send(`Cannot generate module for ${req.packageSpec}${req.filename}\n\n${debugInfo}`)
|
||||||
`Cannot generate module for ${req.packageSpec}${
|
|
||||||
req.filename
|
|
||||||
}\n\n${debugInfo}`
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
// Cache modules for 1 year.
|
// Cache modules for 1 year.
|
||||||
res
|
res
|
||||||
.set({
|
.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)
|
||||||
|
|
||||||
|
@ -112,17 +103,17 @@ function serveFile(req, res, next) {
|
||||||
|
|
||||||
// Cache files for 1 year.
|
// Cache files for 1 year.
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': contentType,
|
"Content-Type": contentType,
|
||||||
'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)
|
||||||
|
@ -132,35 +123,30 @@ 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(
|
getIndexHTML(req.packageInfo, req.packageVersion, req.packageDir, req.filename, function(
|
||||||
req.packageInfo,
|
error,
|
||||||
req.packageVersion,
|
html
|
||||||
req.packageDir,
|
) {
|
||||||
req.filename,
|
if (error) {
|
||||||
function(error, html) {
|
console.error(error)
|
||||||
if (error) {
|
res
|
||||||
console.error(error)
|
.status(500)
|
||||||
res
|
.type("text")
|
||||||
.status(500)
|
.send(`Cannot generate index page for ${req.packageSpec}${req.filename}`)
|
||||||
.type('text')
|
} else {
|
||||||
.send(
|
// Cache HTML directory listings for 1 minute.
|
||||||
`Cannot generate index page for ${req.packageSpec}${req.filename}`
|
res
|
||||||
)
|
.set({
|
||||||
} else {
|
"Cache-Control": "public, max-age=60",
|
||||||
// Cache HTML directory listings for 1 minute.
|
"Cache-Tag": "index"
|
||||||
res
|
})
|
||||||
.set({
|
.send(html)
|
||||||
'Cache-Control': 'public, max-age=60',
|
|
||||||
'Cache-Tag': 'index'
|
|
||||||
})
|
|
||||||
.send(html)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
} else {
|
} else {
|
||||||
res
|
res
|
||||||
.status(403)
|
.status(403)
|
||||||
.type('text')
|
.type("text")
|
||||||
.send(`Cannot serve ${req.packageSpec}${req.filename}; it's not a file`)
|
.send(`Cannot serve ${req.packageSpec}${req.filename}; it's not a file`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const AuthAPI = require('../AuthAPI')
|
const AuthAPI = require("../AuthAPI")
|
||||||
|
|
||||||
const ReadMethods = { GET: true, HEAD: true }
|
const ReadMethods = { GET: true, HEAD: true }
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ function userToken(req, res, next) {
|
||||||
next()
|
next()
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
if (error.name === 'JsonWebTokenError') {
|
if (error.name === "JsonWebTokenError") {
|
||||||
res.status(403).send({
|
res.status(403).send({
|
||||||
error: `Bad auth token: ${error.message}`
|
error: `Bad auth token: ${error.message}`
|
||||||
})
|
})
|
||||||
|
@ -31,7 +31,7 @@ function userToken(req, res, next) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
||||||
res.status(500).send({
|
res.status(500).send({
|
||||||
error: 'Unable to verify auth'
|
error: "Unable to verify auth"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,35 @@
|
||||||
const getFileContentType = require('../getFileContentType')
|
const getFileContentType = require("../getFileContentType")
|
||||||
|
|
||||||
it('gets a content type of text/plain for LICENSE|README|CHANGES|AUTHORS|Makefile', () => {
|
it("gets a content type of text/plain for LICENSE|README|CHANGES|AUTHORS|Makefile", () => {
|
||||||
expect(getFileContentType('AUTHORS')).toBe('text/plain')
|
expect(getFileContentType("AUTHORS")).toBe("text/plain")
|
||||||
expect(getFileContentType('CHANGES')).toBe('text/plain')
|
expect(getFileContentType("CHANGES")).toBe("text/plain")
|
||||||
expect(getFileContentType('LICENSE')).toBe('text/plain')
|
expect(getFileContentType("LICENSE")).toBe("text/plain")
|
||||||
expect(getFileContentType('Makefile')).toBe('text/plain')
|
expect(getFileContentType("Makefile")).toBe("text/plain")
|
||||||
expect(getFileContentType('PATENTS')).toBe('text/plain')
|
expect(getFileContentType("PATENTS")).toBe("text/plain")
|
||||||
expect(getFileContentType('README')).toBe('text/plain')
|
expect(getFileContentType("README")).toBe("text/plain")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('gets a content type of text/plain for .*rc files', () => {
|
it("gets a content type of text/plain for .*rc files", () => {
|
||||||
expect(getFileContentType('.eslintrc')).toBe('text/plain')
|
expect(getFileContentType(".eslintrc")).toBe("text/plain")
|
||||||
expect(getFileContentType('.babelrc')).toBe('text/plain')
|
expect(getFileContentType(".babelrc")).toBe("text/plain")
|
||||||
expect(getFileContentType('.anythingrc')).toBe('text/plain')
|
expect(getFileContentType(".anythingrc")).toBe("text/plain")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('gets a content type of text/plain for .git* files', () => {
|
it("gets a content type of text/plain for .git* files", () => {
|
||||||
expect(getFileContentType('.gitignore')).toBe('text/plain')
|
expect(getFileContentType(".gitignore")).toBe("text/plain")
|
||||||
expect(getFileContentType('.gitanything')).toBe('text/plain')
|
expect(getFileContentType(".gitanything")).toBe("text/plain")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('gets a content type of text/plain for .*ignore files', () => {
|
it("gets a content type of text/plain for .*ignore files", () => {
|
||||||
expect(getFileContentType('.eslintignore')).toBe('text/plain')
|
expect(getFileContentType(".eslintignore")).toBe("text/plain")
|
||||||
expect(getFileContentType('.anythingignore')).toBe('text/plain')
|
expect(getFileContentType(".anythingignore")).toBe("text/plain")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('gets a content type of text/plain for .ts files', () => {
|
it("gets a content type of text/plain for .ts files", () => {
|
||||||
expect(getFileContentType('app.ts')).toBe('text/plain')
|
expect(getFileContentType("app.ts")).toBe("text/plain")
|
||||||
expect(getFileContentType('app.d.ts')).toBe('text/plain')
|
expect(getFileContentType("app.d.ts")).toBe("text/plain")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('gets a content type of text/plain for .flow files', () => {
|
it("gets a content type of text/plain for .flow files", () => {
|
||||||
expect(getFileContentType('app.js.flow')).toBe('text/plain')
|
expect(getFileContentType("app.js.flow")).toBe("text/plain")
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
const db = require('../../RedisClient')
|
const db = require("../../RedisClient")
|
||||||
|
|
||||||
function createCache(keyPrefix) {
|
function createCache(keyPrefix) {
|
||||||
function createKey(key) {
|
function createKey(key) {
|
||||||
return keyPrefix + '-' + key
|
return keyPrefix + "-" + key
|
||||||
}
|
}
|
||||||
|
|
||||||
function set(key, value, expiry, callback) {
|
function set(key, value, expiry, callback) {
|
||||||
|
|
|
@ -2,16 +2,16 @@ function createSearch(query) {
|
||||||
const params = []
|
const params = []
|
||||||
|
|
||||||
Object.keys(query).forEach(param => {
|
Object.keys(query).forEach(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 {
|
||||||
params.push(`${param}=${encodeURIComponent(query[param])}`)
|
params.push(`${param}=${encodeURIComponent(query[param])}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const search = params.join('&')
|
const search = params.join("&")
|
||||||
|
|
||||||
return search ? `?${search}` : ''
|
return search ? `?${search}` : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = createSearch
|
module.exports = createSearch
|
||||||
|
|
|
@ -1,22 +1,13 @@
|
||||||
const mime = require('mime')
|
const mime = require("mime")
|
||||||
|
|
||||||
mime.define({
|
mime.define({
|
||||||
'text/plain': [
|
"text/plain": ["authors", "changes", "license", "makefile", "patents", "readme", "ts", "flow"]
|
||||||
'authors',
|
|
||||||
'changes',
|
|
||||||
'license',
|
|
||||||
'makefile',
|
|
||||||
'patents',
|
|
||||||
'readme',
|
|
||||||
'ts',
|
|
||||||
'flow'
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const TextFiles = /\/?(\.[a-z]*rc|\.git[a-z]*|\.[a-z]*ignore)$/i
|
const TextFiles = /\/?(\.[a-z]*rc|\.git[a-z]*|\.[a-z]*ignore)$/i
|
||||||
|
|
||||||
function getFileContentType(file) {
|
function getFileContentType(file) {
|
||||||
return TextFiles.test(file) ? 'text/plain' : mime.lookup(file)
|
return TextFiles.test(file) ? "text/plain" : mime.lookup(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = getFileContentType
|
module.exports = getFileContentType
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const fs = require('fs')
|
const fs = require("fs")
|
||||||
|
|
||||||
function getFileStats(file) {
|
function getFileStats(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
function getFileType(stats) {
|
function getFileType(stats) {
|
||||||
if (stats.isFile()) return 'file'
|
if (stats.isFile()) return "file"
|
||||||
if (stats.isDirectory()) return 'directory'
|
if (stats.isDirectory()) return "directory"
|
||||||
if (stats.isBlockDevice()) return 'blockDevice'
|
if (stats.isBlockDevice()) return "blockDevice"
|
||||||
if (stats.isCharacterDevice()) return 'characterDevice'
|
if (stats.isCharacterDevice()) return "characterDevice"
|
||||||
if (stats.isSymbolicLink()) return 'symlink'
|
if (stats.isSymbolicLink()) return "symlink"
|
||||||
if (stats.isSocket()) return 'socket'
|
if (stats.isSocket()) return "socket"
|
||||||
if (stats.isFIFO()) return 'fifo'
|
if (stats.isFIFO()) return "fifo"
|
||||||
return 'unknown'
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = getFileType
|
module.exports = getFileType
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
const fs = require('fs')
|
const fs = require("fs")
|
||||||
const path = require('path')
|
const path = require("path")
|
||||||
const React = require('react')
|
const React = require("react")
|
||||||
const ReactDOMServer = require('react-dom/server')
|
const ReactDOMServer = require("react-dom/server")
|
||||||
const getFileStats = require('./getFileStats')
|
const getFileStats = require("./getFileStats")
|
||||||
const IndexPage = require('../components/IndexPage')
|
const IndexPage = require("../components/IndexPage")
|
||||||
|
|
||||||
const e = React.createElement
|
const e = React.createElement
|
||||||
|
|
||||||
|
@ -14,9 +14,7 @@ function getEntries(dir) {
|
||||||
reject(error)
|
reject(error)
|
||||||
} else {
|
} else {
|
||||||
resolve(
|
resolve(
|
||||||
Promise.all(
|
Promise.all(files.map(file => getFileStats(path.join(dir, file)))).then(statsArray => {
|
||||||
files.map(file => getFileStats(path.join(dir, file)))
|
|
||||||
).then(statsArray => {
|
|
||||||
return statsArray.map((stats, index) => {
|
return statsArray.map((stats, index) => {
|
||||||
return { file: files[index], stats }
|
return { file: files[index], stats }
|
||||||
})
|
})
|
||||||
|
@ -27,7 +25,7 @@ function getEntries(dir) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const DOCTYPE = '<!DOCTYPE html>'
|
const DOCTYPE = "<!DOCTYPE html>"
|
||||||
|
|
||||||
function createHTML(props) {
|
function createHTML(props) {
|
||||||
return DOCTYPE + ReactDOMServer.renderToStaticMarkup(e(IndexPage, props))
|
return DOCTYPE + ReactDOMServer.renderToStaticMarkup(e(IndexPage, props))
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
const fs = require('fs')
|
const fs = require("fs")
|
||||||
const path = require('path')
|
const path = require("path")
|
||||||
const SRIToolbox = require('sri-toolbox')
|
const SRIToolbox = require("sri-toolbox")
|
||||||
const getFileContentType = require('./getFileContentType')
|
const getFileContentType = require("./getFileContentType")
|
||||||
const getFileStats = require('./getFileStats')
|
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((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -12,17 +12,10 @@ function getEntries(dir, file, maximumDepth) {
|
||||||
reject(error)
|
reject(error)
|
||||||
} else {
|
} else {
|
||||||
resolve(
|
resolve(
|
||||||
Promise.all(
|
Promise.all(files.map(f => getFileStats(path.join(dir, file, f)))).then(statsArray => {
|
||||||
files.map(f => getFileStats(path.join(dir, file, f)))
|
|
||||||
).then(statsArray => {
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
statsArray.map((stats, index) =>
|
statsArray.map((stats, index) =>
|
||||||
getMetadataRecursive(
|
getMetadataRecursive(dir, path.join(file, files[index]), stats, maximumDepth - 1)
|
||||||
dir,
|
|
||||||
path.join(file, files[index]),
|
|
||||||
stats,
|
|
||||||
maximumDepth - 1
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -42,7 +35,7 @@ function getIntegrity(file) {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error)
|
reject(error)
|
||||||
} else {
|
} else {
|
||||||
resolve(SRIToolbox.generate({ algorithms: ['sha384'] }, data))
|
resolve(SRIToolbox.generate({ algorithms: ["sha384"] }, data))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -64,8 +57,7 @@ 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(files => {
|
return getEntries(dir, file, maximumDepth).then(files => {
|
||||||
metadata.files = files
|
metadata.files = files
|
||||||
|
@ -74,12 +66,9 @@ function getMetadataRecursive(dir, file, stats, maximumDepth) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMetadata(baseDir, path, stats, maximumDepth, callback) {
|
function getMetadata(baseDir, path, stats, maximumDepth, callback) {
|
||||||
getMetadataRecursive(baseDir, path, stats, maximumDepth).then(function(
|
getMetadataRecursive(baseDir, path, stats, maximumDepth).then(function(metadata) {
|
||||||
metadata
|
|
||||||
) {
|
|
||||||
callback(null, metadata)
|
callback(null, metadata)
|
||||||
},
|
}, callback)
|
||||||
callback)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = getMetadata
|
module.exports = getMetadata
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
require('isomorphic-fetch')
|
require("isomorphic-fetch")
|
||||||
const fs = require('fs')
|
const fs = require("fs")
|
||||||
const path = require('path')
|
const path = require("path")
|
||||||
const tmpdir = require('os-tmpdir')
|
const tmpdir = require("os-tmpdir")
|
||||||
const gunzip = require('gunzip-maybe')
|
const gunzip = require("gunzip-maybe")
|
||||||
const mkdirp = require('mkdirp')
|
const mkdirp = require("mkdirp")
|
||||||
const tar = require('tar-fs')
|
const tar = require("tar-fs")
|
||||||
const createMutex = require('./createMutex')
|
const createMutex = require("./createMutex")
|
||||||
|
|
||||||
function createTempPath(name, version) {
|
function createTempPath(name, version) {
|
||||||
const normalName = name.replace(/\//g, '-')
|
const normalName = name.replace(/\//g, "-")
|
||||||
return path.join(tmpdir(), `unpkg-${normalName}-${version}`)
|
return path.join(tmpdir(), `unpkg-${normalName}-${version}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,12 +17,12 @@ function stripNamePrefix(headers) {
|
||||||
// so we shorten that to just "index.js" here. A few packages use a
|
// so we shorten that to just "index.js" here. A few packages use a
|
||||||
// prefix other than "package/". e.g. the firebase package uses the
|
// prefix other than "package/". e.g. the firebase package uses the
|
||||||
// "firebase_npm/" prefix. So we just strip the first dir name.
|
// "firebase_npm/" prefix. So we just strip the first dir name.
|
||||||
headers.name = headers.name.replace(/^[^\/]+\//, '')
|
headers.name = headers.name.replace(/^[^\/]+\//, "")
|
||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
function ignoreSymlinks(file, headers) {
|
function ignoreSymlinks(file, headers) {
|
||||||
return headers.type === 'link'
|
return headers.type === "link"
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractResponse(response, outputDir) {
|
function extractResponse(response, outputDir) {
|
||||||
|
@ -36,8 +36,8 @@ function extractResponse(response, outputDir) {
|
||||||
response.body
|
response.body
|
||||||
.pipe(gunzip())
|
.pipe(gunzip())
|
||||||
.pipe(extract)
|
.pipe(extract)
|
||||||
.on('finish', resolve)
|
.on("finish", resolve)
|
||||||
.on('error', reject)
|
.on("error", reject)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ const fetchMutex = createMutex((payload, callback) => {
|
||||||
|
|
||||||
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) {
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
require('isomorphic-fetch')
|
require("isomorphic-fetch")
|
||||||
const createCache = require('./createCache')
|
const createCache = require("./createCache")
|
||||||
const createMutex = require('./createMutex')
|
const createMutex = require("./createMutex")
|
||||||
|
|
||||||
const RegistryURL = process.env.NPM_REGISTRY_URL || 'https://registry.npmjs.org'
|
const RegistryURL = process.env.NPM_REGISTRY_URL || "https://registry.npmjs.org"
|
||||||
|
|
||||||
const PackageInfoCache = createCache('packageInfo')
|
const PackageInfoCache = createCache("packageInfo")
|
||||||
|
|
||||||
function fetchPackageInfo(packageName) {
|
function fetchPackageInfo(packageName) {
|
||||||
console.log(`info: Fetching package info for ${packageName}`)
|
console.log(`info: Fetching package info for ${packageName}`)
|
||||||
|
|
||||||
let encodedPackageName
|
let encodedPackageName
|
||||||
if (packageName.charAt(0) === '@') {
|
if (packageName.charAt(0) === "@") {
|
||||||
encodedPackageName = `@${encodeURIComponent(packageName.substring(1))}`
|
encodedPackageName = `@${encodeURIComponent(packageName.substring(1))}`
|
||||||
} else {
|
} else {
|
||||||
encodedPackageName = encodeURIComponent(packageName)
|
encodedPackageName = encodeURIComponent(packageName)
|
||||||
|
@ -20,14 +20,14 @@ function fetchPackageInfo(packageName) {
|
||||||
|
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json'
|
Accept: "application/json"
|
||||||
}
|
}
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
return res.status === 404 ? null : res.json()
|
return res.status === 404 ? null : res.json()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const PackageNotFound = 'PackageNotFound'
|
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.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const db = require('../../RedisClient')
|
const db = require("../../RedisClient")
|
||||||
|
|
||||||
function incrementCounter(counter, key, by) {
|
function incrementCounter(counter, key, by) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
const fs = require('fs')
|
const fs = require("fs")
|
||||||
const path = require('path')
|
const path = require("path")
|
||||||
const csso = require('csso')
|
const csso = require("csso")
|
||||||
|
|
||||||
function readCSS(...args) {
|
function readCSS(...args) {
|
||||||
return csso.minify(fs.readFileSync(path.resolve(...args), 'utf8')).css
|
return csso.minify(fs.readFileSync(path.resolve(...args), "utf8")).css
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = readCSS
|
module.exports = readCSS
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const parsePackageURL = require('../utils/parsePackageURL')
|
const parsePackageURL = require("../utils/parsePackageURL")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds various properties to the request object to do with the
|
* Adds various properties to the request object to do with the
|
||||||
|
|
|
@ -1,85 +1,85 @@
|
||||||
const parsePackageURL = require('../parsePackageURL')
|
const parsePackageURL = require("../parsePackageURL")
|
||||||
|
|
||||||
describe('parsePackageURL', () => {
|
describe("parsePackageURL", () => {
|
||||||
it('parses plain packages', () => {
|
it("parses plain packages", () => {
|
||||||
expect(parsePackageURL('/history@1.0.0/umd/history.min.js')).toEqual({
|
expect(parsePackageURL("/history@1.0.0/umd/history.min.js")).toEqual({
|
||||||
pathname: '/history@1.0.0/umd/history.min.js',
|
pathname: "/history@1.0.0/umd/history.min.js",
|
||||||
search: '',
|
search: "",
|
||||||
query: {},
|
query: {},
|
||||||
packageName: 'history',
|
packageName: "history",
|
||||||
packageVersion: '1.0.0',
|
packageVersion: "1.0.0",
|
||||||
filename: '/umd/history.min.js'
|
filename: "/umd/history.min.js"
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('parses plain packages with a hyphen in the name', () => {
|
it("parses plain packages with a hyphen in the name", () => {
|
||||||
expect(parsePackageURL('/query-string@5.0.0/index.js')).toEqual({
|
expect(parsePackageURL("/query-string@5.0.0/index.js")).toEqual({
|
||||||
pathname: '/query-string@5.0.0/index.js',
|
pathname: "/query-string@5.0.0/index.js",
|
||||||
search: '',
|
search: "",
|
||||||
query: {},
|
query: {},
|
||||||
packageName: 'query-string',
|
packageName: "query-string",
|
||||||
packageVersion: '5.0.0',
|
packageVersion: "5.0.0",
|
||||||
filename: '/index.js'
|
filename: "/index.js"
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('parses plain packages with no version specified', () => {
|
it("parses plain packages with no version specified", () => {
|
||||||
expect(parsePackageURL('/query-string/index.js')).toEqual({
|
expect(parsePackageURL("/query-string/index.js")).toEqual({
|
||||||
pathname: '/query-string/index.js',
|
pathname: "/query-string/index.js",
|
||||||
search: '',
|
search: "",
|
||||||
query: {},
|
query: {},
|
||||||
packageName: 'query-string',
|
packageName: "query-string",
|
||||||
packageVersion: 'latest',
|
packageVersion: "latest",
|
||||||
filename: '/index.js'
|
filename: "/index.js"
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('parses plain packages with version spec', () => {
|
it("parses plain packages with version spec", () => {
|
||||||
expect(parsePackageURL('/query-string@>=4.0.0/index.js')).toEqual({
|
expect(parsePackageURL("/query-string@>=4.0.0/index.js")).toEqual({
|
||||||
pathname: '/query-string@>=4.0.0/index.js',
|
pathname: "/query-string@>=4.0.0/index.js",
|
||||||
search: '',
|
search: "",
|
||||||
query: {},
|
query: {},
|
||||||
packageName: 'query-string',
|
packageName: "query-string",
|
||||||
packageVersion: '>=4.0.0',
|
packageVersion: ">=4.0.0",
|
||||||
filename: '/index.js'
|
filename: "/index.js"
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('parses scoped packages', () => {
|
it("parses scoped packages", () => {
|
||||||
expect(parsePackageURL('/@angular/router@4.3.3/src/index.d.ts')).toEqual({
|
expect(parsePackageURL("/@angular/router@4.3.3/src/index.d.ts")).toEqual({
|
||||||
pathname: '/@angular/router@4.3.3/src/index.d.ts',
|
pathname: "/@angular/router@4.3.3/src/index.d.ts",
|
||||||
search: '',
|
search: "",
|
||||||
query: {},
|
query: {},
|
||||||
packageName: '@angular/router',
|
packageName: "@angular/router",
|
||||||
packageVersion: '4.3.3',
|
packageVersion: "4.3.3",
|
||||||
filename: '/src/index.d.ts'
|
filename: "/src/index.d.ts"
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('parses package names with a period in them', () => {
|
it("parses package names with a period in them", () => {
|
||||||
expect(parsePackageURL('/index.js')).toEqual({
|
expect(parsePackageURL("/index.js")).toEqual({
|
||||||
pathname: '/index.js',
|
pathname: "/index.js",
|
||||||
search: '',
|
search: "",
|
||||||
query: {},
|
query: {},
|
||||||
packageName: 'index.js',
|
packageName: "index.js",
|
||||||
packageVersion: 'latest',
|
packageVersion: "latest",
|
||||||
filename: ''
|
filename: ""
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('parses valid query parameters', () => {
|
it("parses valid query parameters", () => {
|
||||||
expect(parsePackageURL('/history?main=browser')).toEqual({
|
expect(parsePackageURL("/history?main=browser")).toEqual({
|
||||||
pathname: '/history',
|
pathname: "/history",
|
||||||
search: '?main=browser',
|
search: "?main=browser",
|
||||||
query: { main: 'browser' },
|
query: { main: "browser" },
|
||||||
packageName: 'history',
|
packageName: "history",
|
||||||
packageVersion: 'latest',
|
packageVersion: "latest",
|
||||||
filename: ''
|
filename: ""
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns null for invalid pathnames', () => {
|
it("returns null for invalid pathnames", () => {
|
||||||
expect(parsePackageURL('history')).toBe(null)
|
expect(parsePackageURL("history")).toBe(null)
|
||||||
expect(parsePackageURL('/.invalid')).toBe(null)
|
expect(parsePackageURL("/.invalid")).toBe(null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const url = require('url')
|
const url = require("url")
|
||||||
const validatePackageName = require('./validatePackageName')
|
const validatePackageName = require("./validatePackageName")
|
||||||
|
|
||||||
const URLFormat = /^\/((?:@[^\/@]+\/)?[^\/@]+)(?:@([^\/]+))?(\/.*)?$/
|
const URLFormat = /^\/((?:@[^\/@]+\/)?[^\/@]+)(?:@([^\/]+))?(\/.*)?$/
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ function decodeParam(param) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ''
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePackageURL(packageURL) {
|
function parsePackageURL(packageURL) {
|
||||||
|
@ -28,7 +28,7 @@ function parsePackageURL(packageURL) {
|
||||||
// Disallow invalid npm package names.
|
// Disallow invalid npm package names.
|
||||||
if (!validatePackageName(packageName)) return null
|
if (!validatePackageName(packageName)) return null
|
||||||
|
|
||||||
const packageVersion = decodeParam(match[2]) || 'latest'
|
const packageVersion = decodeParam(match[2]) || "latest"
|
||||||
const filename = decodeParam(match[3])
|
const filename = decodeParam(match[3])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const validateNpmPackageName = require('validate-npm-package-name')
|
const validateNpmPackageName = require("validate-npm-package-name")
|
||||||
|
|
||||||
function validatePackageName(packageName) {
|
function validatePackageName(packageName) {
|
||||||
return validateNpmPackageName(packageName).errors == null
|
return validateNpmPackageName(packageName).errors == null
|
||||||
|
|
Loading…
Reference in New Issue