This commit is contained in:
MICHAEL JACKSON 2017-11-25 13:25:01 -08:00
parent f3974b5e2d
commit 3a309241da
64 changed files with 635 additions and 801 deletions

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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}
/>
)
} }
} }

View File

@ -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>

View File

@ -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 = {

View File

@ -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"))

View File

@ -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)
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
} }
} }

View File

@ -122,7 +122,7 @@
"extends": "react-app" "extends": "react-app"
}, },
"prettier": { "prettier": {
"semi": false, "printWidth": 100,
"singleQuote": true "semi": false
} }
} }

View File

@ -1,2 +1,2 @@
exports.isBareModuleIdentifier = require('./utils/isBareModuleIdentifier') exports.isBareModuleIdentifier = require("./utils/isBareModuleIdentifier")
exports.parseModuleIdentifier = require('./utils/parseModuleIdentifier') exports.parseModuleIdentifier = require("./utils/parseModuleIdentifier")

View File

@ -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
)
}) })
}) })

View File

@ -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"
}) })
}) })
}) })

View File

@ -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

View File

@ -5,7 +5,7 @@ function parseBareModuleIdentifier(id) {
return { return {
packageName: match[1], packageName: match[1],
file: match[2] || '' file: match[2] || ""
} }
} }

View File

@ -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"
]
} }

View File

@ -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) {

View File

@ -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) => {

View File

@ -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 = {

View File

@ -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)

View File

@ -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)

View File

@ -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 => {

View File

@ -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 => {

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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 => {

View File

@ -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)

View File

@ -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 => {

View File

@ -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"
}) })
} }
) )

View File

@ -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 => {

View File

@ -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"
}) })
} }
) )

View File

@ -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() })

View File

@ -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" })
} }
) )
} }

View File

@ -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
} }

View File

@ -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)

View File

@ -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"
}) })
} }
) )

View File

@ -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)
) )
} }

View File

@ -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}`)
) )
) )
) )

View File

@ -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}`)
} }
} }

View File

@ -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})`)
} }

View File

@ -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) {

View File

@ -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`)
} }
} }

View 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"
}) })
} }
} }

View File

@ -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")
}) })

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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) => {

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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) {

View File

@ -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.

View File

@ -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) => {

View File

@ -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

View File

@ -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

View File

@ -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)
}) })
}) })

View File

@ -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 {

View File

@ -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