Merge branch 'master' into percent-40

This commit is contained in:
William Hilton 2019-02-01 12:17:42 -05:00 committed by GitHub
commit 1ee88d0166
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
270 changed files with 123523 additions and 11955 deletions

View File

@ -5,7 +5,8 @@
},
"extends": ["eslint:recommended", "plugin:import/errors"],
"rules": {
"no-console": 0
"no-console": 0,
"import/no-unresolved": 0
},
"globals": {
"fetch": true,
@ -13,7 +14,15 @@
},
"settings": {
"react": {
"version": "15"
"version": "16"
},
"import/resolver": {
"node": {
"moduleDirectory": [
"node_modules",
"functions/node_modules"
]
}
}
}
}

12
.gitignore vendored
View File

@ -1,12 +1,14 @@
.DS_Store
firebase-debug.log*
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/.env
/node_modules/
/public/_assets/
/dump.rdb
/stats.json
/public/_client/
/secrets.tar
/secret_key
/server.js
/service-account-staging.json
/service-account.json
/tokens/

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
8.14.0

3
.prettierrc Normal file
View File

@ -0,0 +1,3 @@
{
"singleQuote": true
}

View File

@ -1,17 +1,29 @@
language: node_js
node_js: node
cache: npm
services:
- redis-server
env:
global:
- secure: uKf/ZaQjLXOQE+h/HE2YhabzndG/gYtDU2hfo799hgyb/W1ojgW3NJZyS2H4SJ3ESaGSwc0L6x/CRGc6jn09rx4iDEzt5jeIQhA4IrEIlLEEarnn3amUqrQ4syd2GPpqsFROu7d5aQDK/dZHLk5Hpv1y9Atz392VCWnro5Pbc0tkRkMaCPZLEgs2A6wK0aYzX3X7FACyBXBc/xA+FfzevpAx7SVt10A6pxx8rYbqrmz0hi4NEH8wmynN35imKxRCVWblHv8ty7ZkyfTv7kUaEW0wBfTxJNh/Kio4Y71Jg/DI6lekF0oY2gAqZ+JnReC94po2HEaF15NuC6j35HHv56CtTUQBRl/nKrItPI7PMuPlyCHi5ChuSD3Oc5Hmu2UiRh5le+iNLjHrpXD25tAOPUfSmrgkaLIn0fdqeKnO3D4Be55KTnWhDlSWjyaQFR9POUvIQwWzjdTflIegRQIzNEC6z9fAp+YDpVyCgbpHbWk0xF8lrEqvBRoJ8vOTYTNUiYM/XPWrv552MyC90th8yCaWl+Dhf2Zso1/neIAejbwhGhFXUeXHxkUK+JDYvfDlq4yrg8h4MPuZ0SE3U5sqvFeNxxFbvNLxbbWLOYmaAXfYrdjT3Ns0TEQc49ZUkLz5feGsIkh+CpiqVRUoBVCizrnAU6mzW2E7IsHVES1ILVY=
- secure: 2glMrYXCJv0NAtO3MouSWhijwdUDaYPTWuNT+la0A4hxIF+N00j2+kgqjidJAmijpmPICEyeXe5RtdLVER8Wye8Nz6FocagC/bM3TKmGg689033HjUU1f4YDXkrhemywWV+Kdgnd+98bFWmUBUGy0c0m+C2GR+DocYchIVwzHH7NVUwRd+byE5CtW3xSMhrKVPhiQgeCBbMYLCHCrsppAsxRChRYcteYAXsR4DeT9BRyZ2q35FNNxh3NuVOBUoH1jUMEbJJ+te1UwxLM1mZRJ5GpJ1B6349myX0L5I5DxoYZqTMUSbYGFB5Ad1NZjEXxS6WtrlWRlsviX5ER3J8AkfPh4sjVt9IqAinBeMhW3SUeK2qazdWE/7zyb3n5sL4/74epGeLp6Sq4OHVDVTiDMVN1rW+9no0IKknvgzqDVkdp3/ShJnQj0TrZYjXyn2wnmYcfMIHjsqWr+uY0oFbyZDNeIGV9f/KW1Rx8XZt3pqiR2AuxLrvKTpPL/Zffu5GXryM53gpcWFoXFHjpBfFbRP250wLiCqNY+XZSA7okzv6vIsykXsHU6FEu7SYiZZhX2mfJQOSRa/+64wuhKwNllSuLIj1I9n/myaSUNQa5Lor6jzsDz4dbXP/tEnh93mXvdKe3DdS9LG7Ca9V8YfxOWccJhT7gXf7DVFMNI5VNMvw=
before_install:
- openssl aes-256-cbc -K $encrypted_a35d52d190dd_key -iv $encrypted_a35d52d190dd_iv
-in secrets.tar.enc -out secrets.tar -d
- tar xvf secrets.tar
script:
- npm run lint
- npm test
- npm run build
- NODE_ENV=$([ "$TRAVIS_BRANCH" == "master" ] && echo "production" || echo "staging") npm run build
deploy:
provider: heroku
- provider: gae
skip_cleanup: true
app: unpkg
api_key:
secure: qJTOiZE1hbghS0U7xpqU/38jm1Dga8bycsgDTW2wOSfAxqxtR4rAilW3ffBHY8/VSqt5GLkyPqFzb9R3I+xlcK6oakQyNMFGjQwMAqwdtHjucr45jWYNeqWf6Nbrrj+0LiQiH/uqMB4mKPquGqx2k2JIHW6jy5ndK14lI9a7SP44edyNugjhNW+OOYjwqo6maHvRoFNxL7kxrw7Js90d0gIKXpunBqHmxmr/hlYcGr9qJ+bLUXxmObxpoFWKHE0iTsTw3C3y558wZpHTfa3vXIvhseW2q7ATuRRVBv8StNjYxYSGpeZA+59d48z3EPK/mp7ybZO6cWyaBdcA9yF3thISEXlXcRcCck5KYk4gwzgrcfug59Z1VJUVXCywJTUu42V5ISoyUKJ+DelkkvMpXQH9T9fxm5eWEwb7sb+UTo3R8mlFliBogvXrWOKNXeRFsLQXH3/YmOUWONUUYRJS11K/p+sFOTfTfaH5lS0AFKCAKtvfLjdLUclsmmrITSFQa42/SRsFx88v3RxDL93r2838p51N7U/RqOqTNfQ8JR2iQOWlthMEvnVZMNs3VT9lW5eYIdTd5kpW/ARVtQf2drVVYR5BzNRJlGyw7M4x1riwNO7XhNqH76Iy/zMbeEDGaXqBCfzgq9vsRAjZm/TFKw8eOEy8+EGxLrBKD1U7EuI=
keyfile: service-account-staging.json
project: unpkg-staging
config: app-staging.yaml
on:
branch: staging
- provider: gae
skip_cleanup: true
keyfile: service-account.json
project: unpkg-gcp
config: app.yaml
on:
branch: master

View File

@ -1,2 +0,0 @@
web: node server.js
ingest_logs: node modules/ingestLogsEveryMinute.js

View File

@ -11,4 +11,4 @@ Please visit [the unpkg website](https://unpkg.com) to learn more about how to u
### Sponsors
The project is sponsored by [Cloudflare](https://cloudflare.com) and [Heroku](https://heroku.com).
unpkg is made possible by generous donations from [Cloudflare](https://cloudflare.com) and [Angular](https://angular.io).

5
app-staging.yaml Normal file
View File

@ -0,0 +1,5 @@
env: standard
instance_class: B4
runtime: nodejs10
basic_scaling:
max_instances: 1

9
app.yaml Normal file
View File

@ -0,0 +1,9 @@
runtime: nodejs10
env: standard
instance_class: F4
automatic_scaling:
min_instances: 2
max_instances: 12
max_concurrent_requests: 20
target_throughput_utilization: 0.6
max_idle_instances: 2

View File

@ -1,140 +0,0 @@
# Authentication
Some API methods require an authentication token. This token is a [JSON web token](https://en.wikipedia.org/wiki/JSON_Web_Token) that contains a list of "scopes" (i.e. permissions).
Once you obtain an API token ([see below](#post-api-auth)) you simply include it in the `Authorization` header of your request as a base-64 encoded string, i.e.
```
Authorization: base64(token)
```
### GET /api/publicKey
The [public key](https://en.wikipedia.org/wiki/Public-key_cryptography) unpkg uses to encrypt authentication tokens, as JSON. You can also find the key as plain text [on GitHub](https://github.com/unpkg/unpkg.com/blob/master/secret_key.pub).
This can be useful to verify a token was issued by unpkg.
Required scope: none
Query parameters: none
Example:
```log
> curl "https://unpkg.com/api/publicKey"
{
"publicKey": "..."
}
```
### POST /api/auth
Creates and returns a new auth token. By default, auth tokens have the following scopes:
```json
{
"blacklist": {
"read": true
}
}
```
Required scope: none
Body parameters: none
Example:
```log
> curl -X POST "https://unpkg.com/api/auth"
{
"token": "..."
}
```
Please reach out to @mjackson if you need a token with additional scopes.
### GET /api/auth
Verifies and returns the payload contained in the given auth token.
Required scope: none
Query parameters: none
Example:
```log
> curl -H "Authorization: $BASE_64_ENCODED_TOKEN" "https://unpkg.com/api/auth"
{
"jti": "...",
"iss": "https://unpkg.com",
"iat": ...,
"scopes": { ... }
}
```
# Blacklist
To protect unpkg users and prevent abuse, unpkg manages a blacklist of npm packages that are known to contain harmful code.
### GET /api/blacklist
Returns a list of all packages that are currently blacklisted.
Required scope: `blacklist.read`
Query parameters: none
Example:
```log
> curl -H "Authorization: $BASE_64_ENCODED_TOKEN" "https://unpkg.com/api/blacklist"
{
"blacklist": [ ... ]
}
```
### POST /api/blacklist
Adds a package to the blacklist.
Required scope: `blacklist.add`
Body parameters:
* `packageName` - The package to add to the blacklist (required)
Example:
```log
> curl -H "Authorization: $BASE_64_ENCODED_TOKEN" -d '{"packageName":"bad-package"}' "https://unpkg.com/api/blacklist"
{
"ok": true
}
```
### DELETE /api/blacklist
Removes a package from the blacklist.
Required scope: `blacklist.remove`
Body parameters:
* `packageName` - The package to remove from the blacklist (required)
Example:
```log
> curl -X DELETE -H "Authorization: $BASE_64_ENCODED_TOKEN" -d '{"packageName":"bad-package"}' "https://unpkg.com/api/blacklist"
{
"ok": true
}
```
# Stats
### GET /api/stats
TODO

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,48 +0,0 @@
unpkg is a fast, global [content delivery network](https://en.wikipedia.org/wiki/Content_delivery_network) for everything on [npm](https://www.npmjs.com/). Use it to quickly and easily load any file from any package using a URL like:
<div class="home-example">unpkg.com/:package@:version/:file</div>
### Examples
Using a fixed version:
* [unpkg.com/react@16.0.0/umd/react.production.min.js](/react@16.0.0/umd/react.production.min.js)
* [unpkg.com/react-dom@16.0.0/umd/react-dom.production.min.js](/react-dom@16.0.0/umd/react-dom.production.min.js)
You may also use a [semver range](https://docs.npmjs.com/misc/semver) or a [tag](https://docs.npmjs.com/cli/dist-tag) instead of a fixed version number, or omit the version/tag entirely to use the `latest` tag.
* [unpkg.com/react@^16/umd/react.production.min.js](/react@^16/umd/react.production.min.js)
* [unpkg.com/react/umd/react.production.min.js](/react/umd/react.production.min.js)
If you omit the file path (i.e. use a "bare" URL), unpkg will serve the file specified by the `unpkg` field in `package.json`, or fall back to `main`.
* [unpkg.com/d3](/d3)
* [unpkg.com/jquery](/jquery)
* [unpkg.com/three](/three)
Append a `/` at the end of a URL to view a listing of all the files in a package.
* [unpkg.com/react/](/react/)
* [unpkg.com/lodash/](/lodash/)
### Query Parameters
<dl>
<dt>`?meta`</dt>
<dd>Return metadata about any file in a package as JSON (e.g. `/any/file?meta`)</dd>
<dt>`?module`</dt>
<dd>Expands all ["bare" `import` specifiers](https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier) in JavaScript modules to unpkg URLs. This feature is *very experimental*</dd>
</dl>
### Workflow
For npm package authors, unpkg relieves the burden of publishing your code to a CDN in addition to the npm registry. All you need to do is include your [UMD](https://github.com/umdjs/umd) build in your npm package (not your repo, that's different!).
You can do this easily using the following setup:
* Add the `umd` (or `dist`) directory to your `.gitignore` file
* Add the `umd` directory to your [files array](https://docs.npmjs.com/files/package.json#files) in `package.json`
* Use a build script to generate your UMD build in the `umd` directory when you publish
That's it! Now when you `npm publish` you'll have a version available on unpkg as well.

View File

@ -1,71 +0,0 @@
const db = require('./utils/data');
const blacklistSet = 'blacklisted-packages';
function addPackage(packageName) {
return new Promise((resolve, reject) => {
db.sadd(blacklistSet, packageName, (error, value) => {
if (error) {
reject(error);
} else {
resolve(value === 1);
}
});
});
}
function removePackage(packageName) {
return new Promise((resolve, reject) => {
db.srem(blacklistSet, packageName, (error, value) => {
if (error) {
reject(error);
} else {
resolve(value === 1);
}
});
});
}
function removeAllPackages() {
return new Promise((resolve, reject) => {
db.del(blacklistSet, error => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
function getPackages() {
return new Promise((resolve, reject) => {
db.smembers(blacklistSet, (error, value) => {
if (error) {
reject(error);
} else {
resolve(value);
}
});
});
}
function includesPackage(packageName) {
return new Promise((resolve, reject) => {
db.sismember(blacklistSet, packageName, (error, value) => {
if (error) {
reject(error);
} else {
resolve(value === 1);
}
});
});
}
module.exports = {
addPackage,
removePackage,
removeAllPackages,
getPackages,
includesPackage
};

View File

@ -1,173 +0,0 @@
const db = require('./utils/data');
const CloudflareAPI = require('./CloudflareAPI');
const BlacklistAPI = require('./BlacklistAPI');
function prunePackages(packagesMap) {
return Promise.all(
Object.keys(packagesMap).map(packageName =>
BlacklistAPI.includesPackage(packageName).then(blacklisted => {
if (blacklisted) {
delete packagesMap[packageName];
}
})
)
).then(() => packagesMap);
}
function createDayKey(date) {
return `${date.getUTCFullYear()}-${date.getUTCMonth()}-${date.getUTCDate()}`;
}
function createHourKey(date) {
return `${createDayKey(date)}-${date.getUTCHours()}`;
}
function createMinuteKey(date) {
return `${createHourKey(date)}-${date.getUTCMinutes()}`;
}
function createScoresMap(array) {
const map = {};
for (let i = 0; i < array.length; i += 2) {
map[array[i]] = parseInt(array[i + 1], 10);
}
return map;
}
function getScoresMap(key, n = 100) {
return new Promise((resolve, reject) => {
db.zrevrange(key, 0, n, 'withscores', (error, value) => {
if (error) {
reject(error);
} else {
resolve(createScoresMap(value));
}
});
});
}
function getPackageRequests(date, n = 100) {
return getScoresMap(`stats-packageRequests-${createDayKey(date)}`, n).then(
prunePackages
);
}
function getPackageBandwidth(date, n = 100) {
return getScoresMap(`stats-packageBytes-${createDayKey(date)}`, n).then(
prunePackages
);
}
function getProtocolRequests(date) {
return getScoresMap(`stats-protocolRequests-${createDayKey(date)}`);
}
function addDailyMetricsToTimeseries(timeseries) {
const since = new Date(timeseries.since);
return Promise.all([
getPackageRequests(since),
getPackageBandwidth(since),
getProtocolRequests(since)
]).then(results => {
timeseries.requests.package = results[0];
timeseries.bandwidth.package = results[1];
timeseries.requests.protocol = results[2];
return timeseries;
});
}
function sumMaps(maps) {
return maps.reduce((memo, map) => {
Object.keys(map).forEach(key => {
memo[key] = (memo[key] || 0) + map[key];
});
return memo;
}, {});
}
function addDailyMetrics(result) {
return Promise.all(result.timeseries.map(addDailyMetricsToTimeseries)).then(
() => {
result.totals.requests.package = sumMaps(
result.timeseries.map(timeseries => {
return timeseries.requests.package;
})
);
result.totals.bandwidth.package = sumMaps(
result.timeseries.map(timeseries => timeseries.bandwidth.package)
);
result.totals.requests.protocol = sumMaps(
result.timeseries.map(timeseries => timeseries.requests.protocol)
);
return result;
}
);
}
function extractPublicInfo(data) {
return {
since: data.since,
until: data.until,
requests: {
all: data.requests.all,
cached: data.requests.cached,
country: data.requests.country,
status: data.requests.http_status
},
bandwidth: {
all: data.bandwidth.all,
cached: data.bandwidth.cached,
country: data.bandwidth.country
},
threats: {
all: data.threats.all,
country: data.threats.country
},
uniques: {
all: data.uniques.all
}
};
}
const DomainNames = ['unpkg.com', 'npmcdn.com'];
function fetchStats(since, until) {
return CloudflareAPI.getZones(DomainNames).then(zones => {
return CloudflareAPI.getZoneAnalyticsDashboard(zones, since, until).then(
dashboard => {
return {
timeseries: dashboard.timeseries.map(extractPublicInfo),
totals: extractPublicInfo(dashboard.totals)
};
}
);
});
}
const oneMinute = 1000 * 60;
const oneHour = oneMinute * 60;
const oneDay = oneHour * 24;
function getStats(since, until) {
const promise = fetchStats(since, until);
return until - since > oneDay ? promise.then(addDailyMetrics) : promise;
}
module.exports = {
createDayKey,
createHourKey,
createMinuteKey,
getStats
};

View File

@ -1 +1 @@
module.exports = {};
export default {};

View File

@ -1,24 +0,0 @@
const BlacklistAPI = require('../BlacklistAPI');
describe('Blacklist API', () => {
beforeEach(done => {
BlacklistAPI.removeAllPackages().then(() => done(), done);
});
it('adds and removes packages to/from the blacklist', done => {
const packageName = 'bad-package';
BlacklistAPI.addPackage(packageName).then(() => {
BlacklistAPI.getPackages().then(packageNames => {
expect(packageNames).toEqual([packageName]);
BlacklistAPI.removePackage(packageName).then(() => {
BlacklistAPI.getPackages().then(packageNames => {
expect(packageNames).toEqual([]);
done();
});
});
});
});
});
});

View File

@ -1,8 +1,8 @@
const request = require('supertest');
import request from 'supertest';
const createServer = require('../createServer');
const withRevokedToken = require('./utils/withRevokedToken');
const withToken = require('./utils/withToken');
import createServer from '../createServer';
import withRevokedToken from './utils/withRevokedToken';
import withToken from './utils/withToken';
describe('The /_auth endpoint', () => {
let server;

View File

@ -1,109 +0,0 @@
const request = require('supertest');
const createServer = require('../createServer');
const clearBlacklist = require('./utils/clearBlacklist');
const withToken = require('./utils/withToken');
describe('The /_blacklist endpoint', () => {
let server;
beforeEach(() => {
server = createServer();
});
describe('POST /_blacklist', () => {
afterEach(clearBlacklist);
describe('with no auth', () => {
it('is forbidden', done => {
request(server)
.post('/_blacklist')
.end((err, res) => {
expect(res.statusCode).toBe(403);
done();
});
});
});
describe('with the "blacklist.add" scope', () => {
it('can add to the blacklist', done => {
withToken({ blacklist: { add: true } }, token => {
request(server)
.post('/_blacklist')
.send({ token, packageName: 'bad-package' })
.end((err, res) => {
expect(res.statusCode).toBe(200);
expect(res.body.ok).toBe(true);
done();
});
});
});
});
});
describe('GET /_blacklist', () => {
describe('with no auth', () => {
it('is forbidden', done => {
request(server)
.get('/_blacklist')
.end((err, res) => {
expect(res.statusCode).toBe(403);
done();
});
});
});
describe('with the "blacklist.read" scope', () => {
it('can read the blacklist', done => {
withToken({ blacklist: { read: true } }, token => {
request(server)
.get('/_blacklist?token=' + token)
.end((err, res) => {
expect(res.statusCode).toBe(200);
done();
});
});
});
});
});
describe('DELETE /_blacklist/:packageName', () => {
describe('with no auth', () => {
it('is forbidden', done => {
request(server)
.delete('/_blacklist/bad-package')
.end((err, res) => {
expect(res.statusCode).toBe(403);
done();
});
});
});
describe('with the "blacklist.remove" scope', () => {
it('can remove a package from the blacklist', done => {
withToken({ blacklist: { remove: true } }, token => {
request(server)
.delete('/_blacklist/bad-package')
.send({ token })
.end((err, res) => {
expect(res.statusCode).toBe(200);
expect(res.body.ok).toBe(true);
done();
});
});
});
it('can remove a scoped package from the blacklist', done => {
withToken({ blacklist: { remove: true } }, token => {
request(server)
.delete('/_blacklist/@scope/bad-package')
.send({ token })
.end((err, res) => {
expect(res.statusCode).toBe(200);
expect(res.body.ok).toBe(true);
done();
});
});
});
});
});
});

View File

@ -1,6 +1,6 @@
const request = require('supertest');
import request from 'supertest';
const createServer = require('../createServer');
import createServer from '../createServer';
describe('The /_publicKey endpoint', () => {
let server;

View File

@ -1,9 +1,9 @@
const request = require('supertest');
import request from 'supertest';
const createServer = require('../createServer');
const withAuthHeader = require('./utils/withAuthHeader');
const withRevokedToken = require('./utils/withRevokedToken');
const withToken = require('./utils/withToken');
import createServer from '../createServer';
import withAuthHeader from './utils/withAuthHeader';
import withRevokedToken from './utils/withRevokedToken';
import withToken from './utils/withToken';
describe('The /api/auth endpoint', () => {
let server;

View File

@ -1,109 +0,0 @@
const request = require('supertest');
const createServer = require('../createServer');
const clearBlacklist = require('./utils/clearBlacklist');
const withToken = require('./utils/withToken');
describe('The /api/blacklist endpoint', () => {
let server;
beforeEach(() => {
server = createServer();
});
describe('POST /api/blacklist', () => {
afterEach(clearBlacklist);
describe('with no auth', () => {
it('is forbidden', done => {
request(server)
.post('/api/blacklist')
.end((err, res) => {
expect(res.statusCode).toBe(403);
done();
});
});
});
describe('with the "blacklist.add" scope', () => {
it('can add to the blacklist', done => {
withToken({ blacklist: { add: true } }, token => {
request(server)
.post('/api/blacklist')
.send({ token, packageName: 'bad-package' })
.end((err, res) => {
expect(res.statusCode).toBe(200);
expect(res.body.ok).toBe(true);
done();
});
});
});
});
});
describe('GET /api/blacklist', () => {
describe('with no auth', () => {
it('is forbidden', done => {
request(server)
.get('/api/blacklist')
.end((err, res) => {
expect(res.statusCode).toBe(403);
done();
});
});
});
describe('with the "blacklist.read" scope', () => {
it('can read the blacklist', done => {
withToken({ blacklist: { read: true } }, token => {
request(server)
.get('/api/blacklist?token=' + token)
.end((err, res) => {
expect(res.statusCode).toBe(200);
done();
});
});
});
});
});
describe('DELETE /api/blacklist', () => {
describe('with no auth', () => {
it('is forbidden', done => {
request(server)
.delete('/api/blacklist')
.end((err, res) => {
expect(res.statusCode).toBe(403);
done();
});
});
});
describe('with the "blacklist.remove" scope', () => {
it('can remove a package from the blacklist', done => {
withToken({ blacklist: { remove: true } }, token => {
request(server)
.delete('/api/blacklist')
.send({ token, packageName: 'bad-package' })
.end((err, res) => {
expect(res.statusCode).toBe(200);
expect(res.body.ok).toBe(true);
done();
});
});
});
it('can remove a scoped package from the blacklist', done => {
withToken({ blacklist: { remove: true } }, token => {
request(server)
.delete('/api/blacklist')
.send({ token, packageName: '@scope/bad-package' })
.end((err, res) => {
expect(res.statusCode).toBe(200);
expect(res.body.ok).toBe(true);
done();
});
});
});
});
});
});

View File

@ -1,6 +1,6 @@
const request = require('supertest');
import request from 'supertest';
const createServer = require('../createServer');
import createServer from '../createServer';
describe('The /api/publicKey endpoint', () => {
let server;

View File

@ -1,9 +1,6 @@
const request = require('supertest');
import request from 'supertest';
const createServer = require('../createServer');
const clearBlacklist = require('./utils/clearBlacklist');
const withBlacklist = require('./utils/withBlacklist');
import createServer from '../createServer';
describe('The server', () => {
let server;
@ -49,19 +46,4 @@ describe('The server', () => {
done();
});
});
describe('blacklisted packages', () => {
afterEach(clearBlacklist);
it('does not serve blacklisted packages', done => {
withBlacklist(['bad-package'], () => {
request(server)
.get('/bad-package/index.js')
.end((err, res) => {
expect(res.statusCode).toBe(403);
done();
});
});
});
});
});

View File

@ -1,3 +1,3 @@
const closeDatabase = require('./utils/closeDatabase');
import closeDatabase from './utils/closeDatabase';
afterAll(closeDatabase);

View File

@ -1,7 +0,0 @@
const BlacklistAPI = require('../../BlacklistAPI');
function clearBlacklist(done) {
BlacklistAPI.removeAllPackages().then(done, done);
}
module.exports = clearBlacklist;

View File

@ -1,7 +1,5 @@
const data = require('../../utils/data');
import data from '../../utils/data';
function closeDatabase() {
export default function closeDatabase() {
data.quit();
}
module.exports = closeDatabase;

View File

@ -1,13 +1,11 @@
const withToken = require('./withToken');
import withToken from './withToken';
function encodeBase64(token) {
return Buffer.from(token).toString('base64');
}
function withAuthHeader(scopes, done) {
export default function withAuthHeader(scopes, done) {
withToken(scopes, token => {
done(encodeBase64(token));
});
}
module.exports = withAuthHeader;

View File

@ -1,7 +0,0 @@
const BlacklistAPI = require('../../BlacklistAPI');
function withBlacklist(blacklist, done) {
Promise.all(blacklist.map(BlacklistAPI.addPackage)).then(done);
}
module.exports = withBlacklist;

View File

@ -1,12 +1,10 @@
const withToken = require('./withToken');
const AuthAPI = require('../../AuthAPI');
import { revokeToken } from '../../utils/auth';
import withToken from './withToken';
function withRevokedToken(scopes, done) {
export default function withRevokedToken(scopes, done) {
withToken(scopes, token => {
AuthAPI.revokeToken(token).then(() => {
revokeToken(token).then(() => {
done(token);
});
});
}
module.exports = withRevokedToken;

View File

@ -1,7 +1,5 @@
const AuthAPI = require('../../AuthAPI');
import { createToken } from '../../utils/auth';
function withToken(scopes, done) {
AuthAPI.createToken(scopes).then(done);
export default function withToken(scopes, done) {
createToken(scopes).then(done);
}
module.exports = withToken;

View File

@ -1,49 +0,0 @@
const validateNpmPackageName = require('validate-npm-package-name');
const BlacklistAPI = require('../BlacklistAPI');
function addToBlacklist(req, res) {
const packageName = req.body.packageName;
if (!packageName) {
return res
.status(403)
.send({ error: 'Missing "packageName" body parameter' });
}
const nameErrors = validateNpmPackageName(packageName).errors;
// Disallow invalid package names.
if (nameErrors) {
const reason = nameErrors.join(', ');
return res.status(403).send({
error: `Invalid package name "${packageName}" (${reason})`
});
}
BlacklistAPI.addPackage(packageName).then(
added => {
if (added) {
const userId = req.user.jti;
console.log(
`Package "${packageName}" was added to the blacklist by ${userId}`
);
}
res.send({
ok: true,
message: `Package "${packageName}" was ${
added ? 'added to' : 'already in'
} the blacklist`
});
},
error => {
console.error(error);
res.status(500).send({
error: `Unable to add "${packageName}" to the blacklist`
});
}
);
}
module.exports = addToBlacklist;

View File

@ -1,13 +1,9 @@
const AuthAPI = require('../AuthAPI');
import { createToken } from '../utils/auth';
const defaultScopes = {
blacklist: {
read: true
}
};
const defaultScopes = {};
function createAuth(req, res) {
AuthAPI.createToken(defaultScopes).then(
export default function createAuth(req, res) {
createToken(defaultScopes).then(
token => {
res.send({ token });
},
@ -20,5 +16,3 @@ function createAuth(req, res) {
}
);
}
module.exports = createAuth;

View File

@ -1,52 +0,0 @@
const validateNpmPackageName = require('validate-npm-package-name');
const BlacklistAPI = require('../BlacklistAPI');
function removeFromBlacklist(req, res) {
// TODO: Remove req.packageName when DELETE
// /_blacklist/:packageName API is removed
const packageName = req.body.packageName || req.packageName;
if (!packageName) {
return res
.status(403)
.send({ error: 'Missing "packageName" body parameter' });
}
const nameErrors = validateNpmPackageName(packageName).errors;
// Disallow invalid package names.
if (nameErrors) {
const reason = nameErrors.join(', ');
return res.status(403).send({
error: `Invalid package name "${packageName}" (${reason})`
});
}
BlacklistAPI.removePackage(packageName).then(
removed => {
if (removed) {
const userId = req.user.jti;
console.log(
`Package "${packageName}" was removed from the blacklist by ${userId}`
);
}
res.send({
ok: true,
message: `Package "${packageName}" was ${
removed ? 'removed from' : 'not in'
} the blacklist`
});
},
error => {
console.error(error);
res.status(500).send({
error: `Unable to remove "${packageName}" from the blacklist`
});
}
);
}
module.exports = removeFromBlacklist;

View File

@ -0,0 +1,3 @@
export default function serveAuth(req, res) {
res.send({ auth: req.user });
}

View File

@ -1,32 +1,35 @@
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const semver = require('semver');
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
import semver from 'semver';
const MainPage = require('../client/MainPage');
const AutoIndexApp = require('../client/autoIndex/App');
const createHTML = require('../client/utils/createHTML');
const renderPage = require('../utils/renderPage');
import AutoIndexApp from '../client/autoIndex/App';
const globalScripts =
process.env.NODE_ENV === 'production'
? [
'/react@16.4.1/umd/react.production.min.js',
'/react-dom@16.4.1/umd/react-dom.production.min.js'
]
: [
'/react@16.4.1/umd/react.development.js',
'/react-dom@16.4.1/umd/react-dom.development.js'
];
import createElement from './utils/createElement';
import createHTML from './utils/createHTML';
import createScript from './utils/createScript';
import getEntryPoint from './utils/getEntryPoint';
import getGlobalScripts from './utils/getGlobalScripts';
import MainTemplate from './utils/MainTemplate';
const doctype = '<!DOCTYPE html>';
const globalURLs =
process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'staging'
? {
'@emotion/core': '/@emotion/core@10.0.6/dist/core.umd.min.js',
react: '/react@16.7.0/umd/react.production.min.js',
'react-dom': '/react-dom@16.7.0/umd/react-dom.production.min.js'
}
: {
'@emotion/core': '/@emotion/core@10.0.6/dist/core.umd.min.js',
react: '/react@16.7.0/umd/react.development.js',
'react-dom': '/react-dom@16.7.0/umd/react-dom.development.js'
};
function byVersion(a, b) {
return semver.lt(a, b) ? -1 : semver.gt(a, b) ? 1 : 0;
}
function serveAutoIndexPage(req, res) {
const scripts = globalScripts.concat(req.assets.getScripts('autoIndex'));
const styles = req.assets.getStyles('autoIndex');
const props = {
export default function serveAutoIndexPage(req, res) {
const data = {
packageName: req.packageName,
packageVersion: req.packageVersion,
availableVersions: Object.keys(req.packageInfo.versions).sort(byVersion),
@ -34,25 +37,28 @@ function serveAutoIndexPage(req, res) {
entry: req.entry,
entries: req.entries
};
const content = createHTML(
ReactDOMServer.renderToString(React.createElement(AutoIndexApp, props))
const content = createHTML(renderToString(createElement(AutoIndexApp, data)));
const entryPoint = getEntryPoint('autoIndex', 'iife');
const elements = getGlobalScripts(entryPoint, globalURLs).concat(
createScript(entryPoint.code)
);
const html = renderPage(MainPage, {
title: `UNPKG - ${req.packageName}`,
description: `The CDN for ${req.packageName}`,
scripts: scripts,
styles: styles,
data: props,
content: content
});
const html =
doctype +
renderToStaticMarkup(
createElement(MainTemplate, {
title: `UNPKG - ${req.packageName}`,
description: `The CDN for ${req.packageName}`,
data,
content,
elements
})
);
res
.set({
'Cache-Control': 'public,max-age=60', // 1 minute
'Cache-Control': 'public, max-age=14400', // 4 hours
'Cache-Tag': 'auto-index'
})
.send(html);
}
module.exports = serveAutoIndexPage;

View File

@ -1,13 +1,13 @@
const serveAutoIndexPage = require('./serveAutoIndexPage');
const serveHTMLModule = require('./serveHTMLModule');
const serveJavaScriptModule = require('./serveJavaScriptModule');
const serveStaticFile = require('./serveStaticFile');
const serveMetadata = require('./serveMetadata');
import serveAutoIndexPage from './serveAutoIndexPage';
import serveHTMLModule from './serveHTMLModule';
import serveJavaScriptModule from './serveJavaScriptModule';
import serveMetadata from './serveMetadata';
import serveStaticFile from './serveStaticFile';
/**
* Send the file, JSON metadata, or HTML directory listing.
*/
function serveFile(req, res) {
export default function serveFile(req, res) {
if (req.query.meta != null) {
return serveMetadata(req, res);
}
@ -33,5 +33,3 @@ function serveFile(req, res) {
serveStaticFile(req, res);
}
module.exports = serveFile;

View File

@ -1,10 +1,10 @@
const etag = require('etag');
const cheerio = require('cheerio');
import etag from 'etag';
import cheerio from 'cheerio';
const getContentTypeHeader = require('../utils/getContentTypeHeader');
const rewriteBareModuleIdentifiers = require('../utils/rewriteBareModuleIdentifiers');
import getContentTypeHeader from '../utils/getContentTypeHeader';
import rewriteBareModuleIdentifiers from '../utils/rewriteBareModuleIdentifiers';
function serveHTMLModule(req, res) {
export default function serveHTMLModule(req, res) {
try {
const $ = cheerio.load(req.entry.content.toString('utf8'));
@ -20,7 +20,7 @@ function serveHTMLModule(req, res) {
.set({
'Content-Length': Buffer.byteLength(code),
'Content-Type': getContentTypeHeader(req.entry.contentType),
'Cache-Control': 'public, max-age=31536000, immutable', // 1 year
'Cache-Control': 'public, max-age=31536000', // 1 year
ETag: etag(code),
'Cache-Tag': 'file, html-file, html-module'
})
@ -46,5 +46,3 @@ function serveHTMLModule(req, res) {
);
}
}
module.exports = serveHTMLModule;

View File

@ -1,9 +1,9 @@
const etag = require('etag');
import etag from 'etag';
const getContentTypeHeader = require('../utils/getContentTypeHeader');
const rewriteBareModuleIdentifiers = require('../utils/rewriteBareModuleIdentifiers');
import getContentTypeHeader from '../utils/getContentTypeHeader';
import rewriteBareModuleIdentifiers from '../utils/rewriteBareModuleIdentifiers';
function serveJavaScriptModule(req, res) {
export default function serveJavaScriptModule(req, res) {
try {
const code = rewriteBareModuleIdentifiers(
req.entry.content.toString('utf8'),
@ -14,7 +14,7 @@ function serveJavaScriptModule(req, res) {
.set({
'Content-Length': Buffer.byteLength(code),
'Content-Type': getContentTypeHeader(req.entry.contentType),
'Cache-Control': 'public, max-age=31536000, immutable', // 1 year
'Cache-Control': 'public, max-age=31536000', // 1 year
ETag: etag(code),
'Cache-Tag': 'file, js-file, js-module'
})
@ -40,5 +40,3 @@ function serveJavaScriptModule(req, res) {
);
}
}
module.exports = serveJavaScriptModule;

View File

@ -0,0 +1,43 @@
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
import MainApp from '../client/main/App';
import createElement from './utils/createElement';
import createHTML from './utils/createHTML';
import createScript from './utils/createScript';
import getEntryPoint from './utils/getEntryPoint';
import getGlobalScripts from './utils/getGlobalScripts';
import MainTemplate from './utils/MainTemplate';
const doctype = '<!DOCTYPE html>';
const globalURLs =
process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'staging'
? {
'@emotion/core': '/@emotion/core@10.0.6/dist/core.umd.min.js',
react: '/react@16.7.0/umd/react.production.min.js',
'react-dom': '/react-dom@16.7.0/umd/react-dom.production.min.js'
}
: {
'@emotion/core': '/@emotion/core@10.0.6/dist/core.umd.min.js',
react: '/react@16.7.0/umd/react.development.js',
'react-dom': '/react-dom@16.7.0/umd/react-dom.development.js'
};
export default function serveMainPage(req, res) {
const content = createHTML(renderToString(createElement(MainApp)));
const entryPoint = getEntryPoint('main', 'iife');
const elements = getGlobalScripts(entryPoint, globalURLs).concat(
createScript(entryPoint.code)
);
const html =
doctype +
renderToStaticMarkup(createElement(MainTemplate, { content, elements }));
res
.set({
'Cache-Control': 'public, max-age=14400', // 4 hours
'Cache-Tag': 'main'
})
.send(html);
}

View File

@ -1,6 +1,6 @@
const path = require('path');
import path from 'path';
const addLeadingSlash = require('../utils/addLeadingSlash');
import addLeadingSlash from '../utils/addLeadingSlash';
function getMatchingEntries(entry, entries) {
const dirname = entry.name || '.';
@ -30,15 +30,13 @@ function getMetadata(entry, entries) {
return metadata;
}
function serveMetadata(req, res) {
export default function serveMetadata(req, res) {
const metadata = getMetadata(req.entry, req.entries);
res
.set({
'Cache-Control': 'public, max-age=31536000, immutable', // 1 year
'Cache-Control': 'public, max-age=31536000', // 1 year
'Cache-Tag': 'meta'
})
.send(metadata);
}
module.exports = serveMetadata;

View File

@ -0,0 +1,5 @@
import { publicKey } from '../utils/secret';
export default function servePublicKey(req, res) {
res.send({ publicKey });
}

View File

@ -1,29 +0,0 @@
const MainPage = require('../client/MainPage');
const renderPage = require('../utils/renderPage');
const globalScripts =
process.env.NODE_ENV === 'production'
? [
'/react@16.4.1/umd/react.production.min.js',
'/react-dom@16.4.1/umd/react-dom.production.min.js',
'/react-router-dom@4.3.1/umd/react-router-dom.min.js'
]
: [
'/react@16.4.1/umd/react.development.js',
'/react-dom@16.4.1/umd/react-dom.development.js',
'/react-router-dom@4.3.1/umd/react-router-dom.js'
];
function serveRootPage(req, res) {
const scripts = globalScripts.concat(req.assets.getScripts('main'));
const styles = req.assets.getStyles('main');
const html = renderPage(MainPage, {
scripts: scripts,
styles: styles
});
res.send(html);
}
module.exports = serveRootPage;

View File

@ -1,9 +1,9 @@
const path = require('path');
const etag = require('etag');
import path from 'path';
import etag from 'etag';
const getContentTypeHeader = require('../utils/getContentTypeHeader');
import getContentTypeHeader from '../utils/getContentTypeHeader';
function serveStaticFile(req, res) {
export default function serveStaticFile(req, res) {
const tags = ['file'];
const ext = path.extname(req.entry.name).substr(1);
@ -15,12 +15,10 @@ function serveStaticFile(req, res) {
.set({
'Content-Length': req.entry.size,
'Content-Type': getContentTypeHeader(req.entry.contentType),
'Cache-Control': 'public, max-age=31536000, immutable', // 1 year
'Cache-Control': 'public, max-age=31536000', // 1 year
'Last-Modified': req.entry.lastModified,
ETag: etag(req.entry.content),
'Cache-Tag': tags.join(', ')
})
.send(req.entry.content);
}
module.exports = serveStaticFile;

View File

@ -0,0 +1,69 @@
import { subDays, startOfDay, startOfSecond } from 'date-fns';
import { getStats } from '../utils/stats';
export default function serveStats(req, res) {
let since, until;
if (req.query.period) {
switch (req.query.period) {
case 'last-day':
until = startOfDay(new Date());
since = subDays(until, 1);
break;
case 'last-week':
until = startOfDay(new Date());
since = subDays(until, 7);
break;
case 'last-month':
default:
until = startOfDay(new Date());
since = subDays(until, 30);
}
} else {
if (!req.query.since) {
return res.status(403).send({ error: 'Missing ?since query parameter' });
}
if (!req.query.until) {
return res.status(403).send({ error: 'Missing ?until query parameter' });
}
since = new Date(req.query.since);
until = req.query.until
? new Date(req.query.until)
: startOfSecond(new Date());
}
if (isNaN(since.getTime())) {
return res.status(403).send({ error: '?since is not a valid date' });
}
if (isNaN(until.getTime())) {
return res.status(403).send({ error: '?until is not a valid date' });
}
if (until <= since) {
return res
.status(403)
.send({ error: '?until date must come after ?since date' });
}
if (until >= new Date()) {
return res.status(403).send({ error: '?until must be a date in the past' });
}
getStats(since, until).then(
stats => {
res
.set({
'Cache-Control': 'public, max-age=3600', // 1 hour
'Cache-Tag': 'stats'
})
.send(stats);
},
error => {
console.error(error);
res.status(500).send({ error: 'Unable to fetch stats' });
}
);
}

View File

@ -1,5 +0,0 @@
function showAuth(req, res) {
res.send({ auth: req.user });
}
module.exports = showAuth;

View File

@ -1,17 +0,0 @@
const BlacklistAPI = require('../BlacklistAPI');
function showBlacklist(req, res) {
BlacklistAPI.getPackages().then(
blacklist => {
res.send({ blacklist });
},
error => {
console.error(error);
res.status(500).send({
error: 'Unable to fetch blacklist'
});
}
);
}
module.exports = showBlacklist;

View File

@ -1,7 +0,0 @@
const secretKey = require('../secretKey');
function showPublicKey(req, res) {
res.send({ publicKey: secretKey.public });
}
module.exports = showPublicKey;

View File

@ -1,63 +0,0 @@
const subDays = require('date-fns/sub_days');
const startOfDay = require('date-fns/start_of_day');
const startOfSecond = require('date-fns/start_of_second');
const StatsAPI = require('../StatsAPI');
function showStats(req, res) {
let since, until;
switch (req.query.period) {
case 'last-day':
until = startOfDay(new Date());
since = subDays(until, 1);
break;
case 'last-week':
until = startOfDay(new Date());
since = subDays(until, 7);
break;
case 'last-month':
until = startOfDay(new Date());
since = subDays(until, 30);
break;
default:
until = req.query.until
? new Date(req.query.until)
: startOfSecond(new Date());
since = new Date(req.query.since);
}
if (isNaN(since.getTime())) {
return res.status(403).send({ error: '?since is not a valid date' });
}
if (isNaN(until.getTime())) {
return res.status(403).send({ error: '?until is not a valid date' });
}
if (until <= since) {
return res
.status(403)
.send({ error: '?until date must come after ?since date' });
}
if (until >= new Date()) {
return res.status(403).send({ error: '?until must be a date in the past' });
}
StatsAPI.getStats(since, until).then(
stats => {
res
.set({
'Cache-Control': 'public, max-age=60',
'Cache-Tag': 'stats'
})
.send(stats);
},
error => {
console.error(error);
res.status(500).send({ error: 'Unable to fetch stats' });
}
);
}
module.exports = showStats;

View File

@ -0,0 +1,71 @@
import PropTypes from 'prop-types';
import e from './createElement';
import h from './createHTML';
import x from './createScript';
const promiseShim =
'window.Promise || document.write(\'\\x3Cscript src="/es6-promise@4.2.5/dist/es6-promise.min.js">\\x3C/script>\\x3Cscript>ES6Promise.polyfill()\\x3C/script>\')';
const fetchShim =
'window.fetch || document.write(\'\\x3Cscript src="/whatwg-fetch@3.0.0/dist/fetch.umd.js">\\x3C/script>\')';
export default function MainTemplate({
title,
description,
favicon,
data,
content,
elements
}) {
return e(
'html',
{ lang: 'en' },
e(
'head',
null,
e('meta', { charSet: 'utf-8' }),
e('meta', { httpEquiv: 'X-UA-Compatible', content: 'IE=edge,chrome=1' }),
description && e('meta', { name: 'description', content: description }),
e('meta', {
name: 'viewport',
content: 'width=device-width,initial-scale=1,maximum-scale=1'
}),
e('meta', { name: 'timestamp', content: new Date().toISOString() }),
favicon && e('link', { rel: 'shortcut icon', href: favicon }),
e('title', null, title),
x(promiseShim),
x(fetchShim),
data && x(`window.__DATA__ = ${JSON.stringify(data)}`)
),
e(
'body',
null,
e('div', { id: 'root', dangerouslySetInnerHTML: content }),
...elements
)
);
}
MainTemplate.defaultProps = {
title: 'UNPKG',
description: 'The CDN for everything on npm',
favicon: '/favicon.ico',
content: h(''),
elements: []
};
if (process.env.NODE_ENV !== 'production') {
const htmlType = PropTypes.shape({
__html: PropTypes.string
});
MainTemplate.propTypes = {
title: PropTypes.string,
description: PropTypes.string,
favicon: PropTypes.string,
data: PropTypes.any,
content: htmlType,
elements: PropTypes.arrayOf(PropTypes.node)
};
}

View File

@ -0,0 +1 @@
export { createElement as default } from 'react';

View File

@ -0,0 +1,3 @@
export default function createHTML(code) {
return { __html: code };
}

View File

@ -0,0 +1,8 @@
import createElement from './createElement';
import createHTML from './createHTML';
export default function createScript(script) {
return createElement('script', {
dangerouslySetInnerHTML: createHTML(script)
});
}

View File

@ -0,0 +1,17 @@
// Virtual module id; see rollup.config.js
import entryManifest from 'entry-manifest';
export default function getEntryPoint(name, format) {
let entryPoints;
entryManifest.forEach(manifest => {
if (name in manifest) {
entryPoints = manifest[name];
}
});
if (entryPoints) {
return entryPoints.find(e => e.format === format);
}
return null;
}

View File

@ -0,0 +1,10 @@
import invariant from 'invariant';
import createElement from './createElement';
export default function getGlobalScripts(entryPoint, globalURLs) {
return entryPoint.globalImports.map(id => {
invariant(globalURLs[id], 'Missing global URL for id "%s"', id);
return createElement('script', { src: globalURLs[id] });
});
}

View File

@ -1,4 +1,9 @@
{
"presets": [["@babel/env", { "loose": true }], "@babel/react"],
"plugins": [["@babel/plugin-proposal-class-properties", { "loose": true }]]
"presets": [
["@babel/preset-env", { "loose": true }],
"@babel/preset-react"
],
"plugins": [
["@babel/plugin-proposal-class-properties", { "loose": true }]
]
}

View File

@ -9,6 +9,8 @@
"plugin:react/recommended"
],
"rules": {
"react/no-children-prop": 0
"import/no-unresolved": 0,
"react/no-children-prop": 0,
"react/prop-types": 0
}
}

View File

@ -1,64 +0,0 @@
const React = require('react');
const PropTypes = require('prop-types');
const createHTML = require('./utils/createHTML');
const x = require('./utils/execScript');
function MainPage({ title, description, scripts, styles, data, content }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="description" content={description} />
<meta httpEquiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1"
/>
<meta name="timestamp" content={new Date().toISOString()} />
<link rel="shortcut icon" href="/favicon.ico" />
{styles.map(s => (
<link key={s} rel="stylesheet" href={s} />
))}
{x(
'window.Promise || document.write(\'\\x3Cscript src="/_polyfills/es6-promise.min.js">\\x3C/script>\\x3Cscript>ES6Promise.polyfill()\\x3C/script>\')'
)}
{x(
'window.fetch || document.write(\'\\x3Cscript src="/_polyfills/fetch.min.js">\\x3C/script>\')'
)}
{x(`window.__DATA__ = ${JSON.stringify(data)}`)}
<title>{title}</title>
</head>
<body>
<div id="root" dangerouslySetInnerHTML={content} />
{scripts.map(s => (
<script key={s} src={s} />
))}
</body>
</html>
);
}
const htmlType = PropTypes.shape({
__html: PropTypes.string
});
MainPage.propTypes = {
title: PropTypes.string,
description: PropTypes.string,
scripts: PropTypes.arrayOf(PropTypes.string),
styles: PropTypes.arrayOf(PropTypes.string),
data: PropTypes.any,
content: htmlType
};
MainPage.defaultProps = {
title: 'UNPKG',
description: 'The CDN for everything on npm',
scripts: [],
styles: [],
data: {},
content: createHTML('')
};
module.exports = MainPage;

View File

@ -1,8 +0,0 @@
body {
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif;
line-height: 1.7;
padding: 0px 10px 5px;
color: #000000;
}

View File

@ -1,9 +1,7 @@
require('./autoIndex.css');
import React from 'react';
import ReactDOM from 'react-dom';
const React = require('react');
const ReactDOM = require('react-dom');
const App = require('./autoIndex/App');
import App from './autoIndex/App';
const props = window.__DATA__ || {};

View File

@ -1,23 +0,0 @@
.app {
max-width: 900px;
margin: 0 auto;
}
.app-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.app-version-selector {
line-height: 2.25em;
float: right;
}
.app-version-selector select {
font-size: 1em;
}
.app-address {
text-align: right;
}

View File

@ -1,10 +1,22 @@
require('./App.css');
/** @jsx jsx */
import React from 'react';
import PropTypes from 'prop-types';
import { Global, css, jsx } from '@emotion/core';
const React = require('react');
import DirectoryListing from './DirectoryListing';
const DirectoryListing = require('./DirectoryListing');
const globalStyles = css`
body {
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Helvetica, Arial, sans-serif;
line-height: 1.7;
padding: 0px 10px 5px;
color: #000000;
}
`;
class App extends React.Component {
export default class App extends React.Component {
static defaultProps = {
availableVersions: []
};
@ -18,19 +30,29 @@ class App extends React.Component {
render() {
return (
<div className="app">
<header className="app-header">
<div css={{ maxWidth: 900, margin: '0 auto' }}>
<Global styles={globalStyles} />
<header
css={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<h1>
Index of /{this.props.packageName}@{this.props.packageVersion}
{this.props.filename}
</h1>
<div className="app-version-selector">
<div css={{ float: 'right', lineHeight: '2.25em' }}>
Version:{' '}
<select
id="version"
defaultValue={this.props.packageVersion}
onChange={this.handleChange}
css={{ fontSize: '1em' }}
>
{this.props.availableVersions.map(v => (
<option key={v} value={v}>
@ -51,7 +73,7 @@ class App extends React.Component {
<hr />
<address className="app-address">
<address css={{ textAlign: 'right' }}>
{this.props.packageName}@{this.props.packageVersion}
</address>
</div>
@ -59,9 +81,7 @@ class App extends React.Component {
}
}
if (process.env.NODE_ENV === 'development') {
const PropTypes = require('prop-types');
if (process.env.NODE_ENV !== 'production') {
const entryType = PropTypes.object;
App.propTypes = {
@ -73,5 +93,3 @@ if (process.env.NODE_ENV === 'development') {
entries: PropTypes.objectOf(entryType).isRequired
};
}
module.exports = App;

View File

@ -1,17 +0,0 @@
.directory-listing table {
width: 100%;
border-collapse: collapse;
font: 0.85em Monaco, monospace;
}
.directory-listing tr.even {
background-color: #eee;
}
.directory-listing th {
text-align: left;
}
.directory-listing th,
.directory-listing td {
padding: 0.5em 1em;
}

View File

@ -1,8 +1,9 @@
require('./DirectoryListing.css');
const React = require('react');
const formatBytes = require('pretty-bytes');
const sortBy = require('sort-by');
/** @jsx jsx */
import React from 'react';
import PropTypes from 'prop-types';
import { jsx } from '@emotion/core';
import formatBytes from 'pretty-bytes';
import sortBy from 'sort-by';
function getDirname(name) {
return (
@ -25,20 +26,33 @@ function getRelativeName(base, name) {
return base.length ? name.substr(base.length + 1) : name;
}
function DirectoryListing({ filename, entry, entries }) {
const styles = {
tableHead: {
textAlign: 'left',
padding: '0.5em 1em'
},
tableCell: {
padding: '0.5em 1em'
},
evenRow: {
backgroundColor: '#eee'
}
};
export default function DirectoryListing({ filename, entry, entries }) {
const rows = [];
if (filename !== '/') {
rows.push(
<tr key="..">
<td>
<td css={styles.tableCell}>
<a title="Parent directory" href="../">
..
</a>
</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td css={styles.tableCell}>-</td>
<td css={styles.tableCell}>-</td>
<td css={styles.tableCell}>-</td>
</tr>
);
}
@ -54,14 +68,14 @@ function DirectoryListing({ filename, entry, entries }) {
rows.push(
<tr key={name}>
<td>
<td css={styles.tableCell}>
<a title={relName} href={href}>
{href}
</a>
</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td css={styles.tableCell}>-</td>
<td css={styles.tableCell}>-</td>
<td css={styles.tableCell}>-</td>
</tr>
);
});
@ -74,33 +88,39 @@ function DirectoryListing({ filename, entry, entries }) {
rows.push(
<tr key={name}>
<td>
<td css={styles.tableCell}>
<a title={relName} href={relName}>
{relName}
</a>
</td>
<td>{contentType}</td>
<td>{formatBytes(size)}</td>
<td>{lastModified}</td>
<td css={styles.tableCell}>{contentType}</td>
<td css={styles.tableCell}>{formatBytes(size)}</td>
<td css={styles.tableCell}>{lastModified}</td>
</tr>
);
});
return (
<div className="directory-listing">
<table>
<div>
<table
css={{
width: '100%',
borderCollapse: 'collapse',
font: '0.85em Monaco, monospace'
}}
>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Size</th>
<th>Last Modified</th>
<th css={styles.tableHead}>Name</th>
<th css={styles.tableHead}>Type</th>
<th css={styles.tableHead}>Size</th>
<th css={styles.tableHead}>Last Modified</th>
</tr>
</thead>
<tbody>
{rows.map((row, index) =>
React.cloneElement(row, {
className: index % 2 ? 'odd' : 'even'
style: index % 2 ? undefined : styles.evenRow
})
)}
</tbody>
@ -109,9 +129,7 @@ function DirectoryListing({ filename, entry, entries }) {
);
}
if (process.env.NODE_ENV === 'development') {
const PropTypes = require('prop-types');
if (process.env.NODE_ENV !== 'production') {
const entryType = PropTypes.shape({
name: PropTypes.string.isRequired
});
@ -122,5 +140,3 @@ if (process.env.NODE_ENV === 'development') {
entries: PropTypes.objectOf(entryType).isRequired
};
}
module.exports = DirectoryListing;

View File

@ -1,63 +0,0 @@
body {
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif;
line-height: 1.7;
padding: 5px 20px;
color: #000000;
}
@media (min-width: 800px) {
body {
padding: 40px 20px 120px;
}
}
a:link {
color: blue;
}
a:visited {
color: rebeccapurple;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.8em;
}
h3 {
font-size: 1.6em;
}
ul {
padding-left: 25px;
}
dd {
margin-left: 25px;
}
table {
border: 1px solid black;
border: 0;
}
th {
text-align: left;
background-color: #eee;
}
th,
td {
padding: 5px;
}
th {
vertical-align: bottom;
}
td {
vertical-align: top;
}
.wrapper {
max-width: 700px;
margin: 0 auto;
}

View File

@ -1,8 +1,6 @@
require('./main.css');
import React from 'react';
import ReactDOM from 'react-dom';
const React = require('react');
const ReactDOM = require('react-dom');
const App = require('./main/App');
import App from './main/App';
ReactDOM.render(<App />, document.getElementById('root'));

View File

@ -1,13 +0,0 @@
.about-logos {
margin: 2em 0;
display: flex;
justify-content: center;
}
.about-logo {
text-align: center;
flex: 1;
max-width: 80%;
}
.about-logo img {
max-width: 60%;
}

View File

@ -1,12 +0,0 @@
require('./About.css');
const React = require('react');
const h = require('../utils/createHTML');
const markup = require('./About.md');
function About() {
return <div className="wrapper" dangerouslySetInnerHTML={h(markup)} />;
}
module.exports = About;

View File

@ -1,30 +0,0 @@
unpkg is an [open source](https://github.com/unpkg) project built and maintained by [Michael Jackson](https://twitter.com/mjackson).
### Sponsors
The fast, global infrastructure that powers unpkg is generously donated by [Cloudflare](https://www.cloudflare.com) and [Heroku](https://www.heroku.com).
<div class="about-logos">
<div class="about-logo">
<a href="https://www.cloudflare.com"><img src="CloudflareLogo.png"></a>
</div>
<div class="about-logo">
<a href="https://www.heroku.com"><img src="HerokuLogo.png"></a>
</div>
</div>
### Cache Behavior
The CDN caches files based on their permanent URL, which includes the npm package version. This works because npm does not allow package authors to overwrite a package that has already been published with a different one at the same version number.
URLs that do not specify a package version number redirect to one that does. This is the `latest` version when no version is specified, or the `maxSatisfying` version when a [semver version](https://github.com/npm/node-semver) is given. Redirects are cached for 5 minutes.
Browsers are instructed (via the `Cache-Control` header) to cache assets for 1 year.
### Abuse
unpkg maintains a list of packages that are known to be malicious. If you find such a package on npm, please let us know!
### Support
unpkg is not affiliated with or supported by npm, Inc. in any way. Please do not contact npm for help with unpkg. Instead, please reach out to [@unpkg](https://twitter.com/unpkg) with any questions or concerns.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,14 +1,397 @@
const React = require('react');
const { HashRouter } = require('react-router-dom');
/** @jsx jsx */
import React from 'react';
import PropTypes from 'prop-types';
import { Global, css, jsx } from '@emotion/core';
import formatBytes from 'pretty-bytes';
import formatDate from 'date-fns/format';
import parseDate from 'date-fns/parse';
const Layout = require('./Layout');
import formatNumber from '../utils/formatNumber';
import formatPercent from '../utils/formatPercent';
import cloudflareLogo from './CloudflareLogo.png';
import angularLogo from './AngularLogo.png';
import googleCloudLogo from './GoogleCloudLogo.png';
const globalStyles = css`
body {
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Helvetica, Arial, sans-serif;
line-height: 1.7;
padding: 5px 20px;
color: black;
}
@media (min-width: 800px) {
body {
padding: 40px 20px 120px;
}
}
a:link {
color: blue;
text-decoration: none;
}
a:visited {
color: rebeccapurple;
}
dd,
ul {
margin-left: 0;
padding-left: 25px;
}
`;
const styles = {
heading: {
margin: '0.8em 0',
textTransform: 'uppercase',
textAlign: 'center',
fontSize: '5em'
},
subheading: {
fontSize: '1.6em'
},
example: {
textAlign: 'center',
backgroundColor: '#eee',
margin: '2em 0',
padding: '5px 0'
}
};
function AboutLogo({ children }) {
return <div css={{ textAlign: 'center', flex: '1' }}>{children}</div>;
}
function AboutLogoImage(props) {
return <img {...props} css={{ maxWidth: '90%' }} />;
}
function Stats({ data }) {
const totals = data.totals;
const since = parseDate(totals.since);
const until = parseDate(totals.until);
function App() {
return (
<HashRouter>
<Layout />
</HashRouter>
<p>
From <strong>{formatDate(since, 'MMM D')}</strong> to{' '}
<strong>{formatDate(until, 'MMM D')}</strong> unpkg served{' '}
<strong>{formatNumber(totals.requests.all)}</strong> requests and a total
of <strong>{formatBytes(totals.bandwidth.all)}</strong> of data to{' '}
<strong>{formatNumber(totals.uniques.all)}</strong> unique visitors,{' '}
<strong>
{formatPercent(totals.requests.cached / totals.requests.all, 0)}%
</strong>{' '}
of which were served from the cache.
</p>
);
}
module.exports = App;
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = { stats: null };
if (typeof window === 'object' && window.localStorage) {
const savedStats = window.localStorage.savedStats;
if (savedStats) {
this.state.stats = JSON.parse(savedStats);
}
window.onbeforeunload = () => {
window.localStorage.savedStats = JSON.stringify(this.state.stats);
};
}
}
componentDidMount() {
// Refresh latest stats.
fetch('/api/stats?period=last-month')
.then(res => res.json())
.then(stats => this.setState({ stats }));
}
render() {
const { stats } = this.state;
const hasStats = !!(stats && !stats.error);
return (
<div css={{ maxWidth: 700, margin: '0 auto' }}>
<Global styles={globalStyles} />
<header>
<h1 css={styles.heading}>unpkg</h1>
<p>
unpkg is a fast, global{' '}
<a href="https://en.wikipedia.org/wiki/Content_delivery_network">
content delivery network
</a>{' '}
for everything on <a href="https://www.npmjs.com/">npm</a>. Use it
to quickly and easily load any file from any package using a URL
like:
</p>
<div css={styles.example}>unpkg.com/:package@:version/:file</div>
{hasStats && <Stats data={stats} />}
</header>
<h3 css={styles.subheading} id="examples">
Examples
</h3>
<p>Using a fixed version:</p>
<ul>
<li>
<a href="/react@16.7.0/umd/react.production.min.js">
unpkg.com/react@16.7.0/umd/react.production.min.js
</a>
</li>
<li>
<a href="/react-dom@16.7.0/umd/react-dom.production.min.js">
unpkg.com/react-dom@16.7.0/umd/react-dom.production.min.js
</a>
</li>
</ul>
<p>
You may also use a{' '}
<a href="https://docs.npmjs.com/misc/semver">semver range</a> or a{' '}
<a href="https://docs.npmjs.com/cli/dist-tag">tag</a> instead of a
fixed version number, or omit the version/tag entirely to use the{' '}
<code>latest</code> tag.
</p>
<ul>
<li>
<a href="/react@^16/umd/react.production.min.js">
unpkg.com/react@^16/umd/react.production.min.js
</a>
</li>
<li>
<a href="/react/umd/react.production.min.js">
unpkg.com/react/umd/react.production.min.js
</a>
</li>
</ul>
<p>
If you omit the file path (i.e. use a &ldquo;bare&rdquo; URL), unpkg
will serve the file specified by the <code>unpkg</code> field in{' '}
<code>package.json</code>, or fall back to
<code>main</code>.
</p>
<ul>
<li>
<a href="/jquery">unpkg.com/jquery</a>
</li>
<li>
<a href="/three">unpkg.com/three</a>
</li>
</ul>
<p>
Append a <code>/</code> at the end of a URL to view a listing of all
the files in a package.
</p>
<ul>
<li>
<a href="/react/">unpkg.com/react/</a>
</li>
<li>
<a href="/lodash/">unpkg.com/lodash/</a>
</li>
</ul>
<h3 css={styles.subheading} id="query-params">
Query Parameters
</h3>
<dl>
<dt>
<code>?meta</code>
</dt>
<dd>
Return metadata about any file in a package as JSON (e.g.
<code>/any/file?meta</code>)
</dd>
<dt>
<code>?module</code>
</dt>
<dd>
Expands all{' '}
<a href="https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier">
&ldquo;bare&rdquo; <code>import</code> specifiers
</a>{' '}
in JavaScript modules to unpkg URLs. This feature is{' '}
<em>very experimental</em>
</dd>
</dl>
<h3 css={styles.subheading} id="cache-behavior">
Cache Behavior
</h3>
<p>
The CDN caches files based on their permanent URL, which includes the
npm package version. This works because npm does not allow package
authors to overwrite a package that has already been published with a
different one at the same version number.
</p>
<p>
URLs that do not specify a package version number redirect to one that
does. This is the <code>latest</code> version when no version is
specified, or the <code>maxSatisfying</code> version when a{' '}
<a href="https://github.com/npm/node-semver">semver version</a> is
given. Redirects are cached for 5 minutes.
</p>
<p>
Browsers are instructed (via the <code>Cache-Control</code> header) to
cache assets for 1 year.
</p>
<h3 css={styles.subheading} id="workflow">
Workflow
</h3>
<p>
For npm package authors, unpkg relieves the burden of publishing your
code to a CDN in addition to the npm registry. All you need to do is
include your <a href="https://github.com/umdjs/umd">UMD</a> build in
your npm package (not your repo, that&apos;s different!).
</p>
<p>You can do this easily using the following setup:</p>
<ul>
<li>
Add the <code>umd</code> (or <code>dist</code>) directory to your{' '}
<code>.gitignore</code> file
</li>
<li>
Add the <code>umd</code> directory to your{' '}
<a href="https://docs.npmjs.com/files/package.json#files">
files array
</a>{' '}
in
<code>package.json</code>
</li>
<li>
Use a build script to generate your UMD build in the{' '}
<code>umd</code> directory when you publish
</li>
</ul>
<p>
That&apos;s it! Now when you <code>npm publish</code> you&apos;ll have
a version available on unpkg as well.
</p>
<h3 css={styles.subheading} id="about">
About
</h3>
<p>
unpkg is an <a href="https://github.com/unpkg">open source</a> project
built and maintained by{' '}
<a href="https://twitter.com/mjackson">Michael Jackson</a>. unpkg is
not affiliated with or supported by npm, Inc. in any way. Please do
not contact npm for help with unpkg. Instead, please reach out to{' '}
<a href="https://twitter.com/unpkg">@unpkg</a> with any questions or
concerns.
</p>
<p>
The unpkg CDN is powered by{' '}
<a href="https://www.cloudflare.com">Cloudflare</a>, one of the
world&apos;s largest and fastest cloud network platforms.{' '}
{hasStats && (
<span>
In the past month, Cloudflare served over{' '}
<strong>{formatBytes(stats.totals.bandwidth.all)}</strong> to{' '}
<strong>{formatNumber(stats.totals.uniques.all)}</strong> unique
unpkg users all over the world.
</span>
)}
</p>
<div
css={{
margin: '4em 0',
display: 'flex',
justifyContent: 'center'
}}
>
<AboutLogo>
<a href="https://www.cloudflare.com">
<AboutLogoImage src={cloudflareLogo} height="100" />
</a>
</AboutLogo>
</div>
<p>
The origin servers for unpkg are powered by{' '}
<a href="https://cloud.google.com/">Google Cloud</a> and made possible
by a generous donation from the{' '}
<a href="https://angular.io">Angular web framework</a>, one of the
world&apos;s most popular libraries for building incredible user
experiences on both desktop and mobile.
</p>
<div
css={{
margin: '4em 0 0',
display: 'flex',
justifyContent: 'center'
}}
>
<AboutLogo>
<a href="https://angular.io">
<AboutLogoImage src={angularLogo} width="200" />
</a>
</AboutLogo>
</div>
<footer
css={{
marginTop: '10em',
color: '#aaa'
}}
>
<p css={{ textAlign: 'center' }}>
&copy; {new Date().getFullYear()} unpkg &nbsp;&mdash;&nbsp; powered
by{' '}
<a href="https://cloud.google.com/">
<img
src={googleCloudLogo}
height="32"
css={{
verticalAlign: 'middle',
marginTop: -2,
marginLeft: -10
}}
/>
</a>
</p>
</footer>
</div>
);
}
}
if (process.env.NODE_ENV !== 'production') {
App.propTypes = {
location: PropTypes.object,
children: PropTypes.node
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,6 +0,0 @@
.home-example {
text-align: center;
background-color: #eee;
margin: 2em 0;
padding: 5px 0;
}

View File

@ -1,12 +0,0 @@
require('./Home.css');
const React = require('react');
const h = require('../utils/createHTML');
const markup = require('./Home.md');
function Home() {
return <div className="wrapper" dangerouslySetInnerHTML={h(markup)} />;
}
module.exports = Home;

View File

@ -1,48 +0,0 @@
unpkg is a fast, global [content delivery network](https://en.wikipedia.org/wiki/Content_delivery_network) for everything on [npm](https://www.npmjs.com/). Use it to quickly and easily load any file from any package using a URL like:
<div class="home-example">unpkg.com/:package@:version/:file</div>
### Examples
Using a fixed version:
* [unpkg.com/react@16.0.0/umd/react.production.min.js](/react@16.0.0/umd/react.production.min.js)
* [unpkg.com/react-dom@16.0.0/umd/react-dom.production.min.js](/react-dom@16.0.0/umd/react-dom.production.min.js)
You may also use a [semver range](https://docs.npmjs.com/misc/semver) or a [tag](https://docs.npmjs.com/cli/dist-tag) instead of a fixed version number, or omit the version/tag entirely to use the `latest` tag.
* [unpkg.com/react@^16/umd/react.production.min.js](/react@^16/umd/react.production.min.js)
* [unpkg.com/react/umd/react.production.min.js](/react/umd/react.production.min.js)
If you omit the file path (i.e. use a "bare" URL), unpkg will serve the file specified by the `unpkg` field in `package.json`, or fall back to `main`.
* [unpkg.com/d3](/d3)
* [unpkg.com/jquery](/jquery)
* [unpkg.com/three](/three)
Append a `/` at the end of a URL to view a listing of all the files in a package.
* [unpkg.com/react/](/react/)
* [unpkg.com/lodash/](/lodash/)
### Query Parameters
<dl>
<dt>`?meta`</dt>
<dd>Return metadata about any file in a package as JSON (e.g. `/any/file?meta`)</dd>
<dt>`?module`</dt>
<dd>Expands all ["bare" `import` specifiers](https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier) in JavaScript modules to unpkg URLs. This feature is *very experimental*</dd>
</dl>
### Workflow
For npm package authors, unpkg relieves the burden of publishing your code to a CDN in addition to the npm registry. All you need to do is include your [UMD](https://github.com/umdjs/umd) build in your npm package (not your repo, that's different!).
You can do this easily using the following setup:
* Add the `umd` (or `dist`) directory to your `.gitignore` file
* Add the `umd` directory to your [files array](https://docs.npmjs.com/files/package.json#files) in `package.json`
* Use a build script to generate your UMD build in the `umd` directory when you publish
That's it! Now when you `npm publish` you'll have a version available on unpkg as well.

View File

@ -1,38 +0,0 @@
.layout-title {
margin: 0;
text-transform: uppercase;
text-align: center;
font-size: 5em;
}
.layout-nav {
margin: 0 0 3em;
}
.layout-navList {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
}
.layout-navList li {
flex-basis: auto;
list-style-type: none;
display: inline-block;
font-size: 1.1em;
margin: 0 10px;
}
.layout-navList li a:link {
text-decoration: none;
}
.layout-navList li a:link,
.layout-navList li a:visited {
color: black;
}
.layout-navUnderline {
height: 4px;
background-color: black;
position: absolute;
left: 0;
}

View File

@ -1,135 +0,0 @@
require('./Layout.css');
const React = require('react');
const PropTypes = require('prop-types');
const { Switch, Route, Link, withRouter } = require('react-router-dom');
const { Motion, spring } = require('react-motion');
const WindowSize = require('./WindowSize');
const About = require('./About');
const Stats = require('./Stats');
const Home = require('./Home');
class Layout extends React.Component {
static propTypes = {
location: PropTypes.object,
children: PropTypes.node
};
state = {
underlineLeft: 0,
underlineWidth: 0,
useSpring: false,
stats: null
};
adjustUnderline = (useSpring = false) => {
let itemIndex;
switch (this.props.location.pathname) {
case '/stats':
itemIndex = 1;
break;
case '/about':
itemIndex = 2;
break;
case '/':
default:
itemIndex = 0;
}
const itemNodes = this.listNode.querySelectorAll('li');
const currentNode = itemNodes[itemIndex];
this.setState({
underlineLeft: currentNode.offsetLeft,
underlineWidth: currentNode.offsetWidth,
useSpring
});
};
componentDidMount() {
this.adjustUnderline();
fetch('/api/stats?period=last-month')
.then(res => res.json())
.then(stats => this.setState({ stats }));
if (window.localStorage) {
const savedStats = window.localStorage.savedStats;
if (savedStats) this.setState({ stats: JSON.parse(savedStats) });
window.onbeforeunload = () => {
localStorage.savedStats = JSON.stringify(this.state.stats);
};
}
}
componentDidUpdate(prevProps) {
if (prevProps.location.pathname !== this.props.location.pathname)
this.adjustUnderline(true);
}
render() {
const { underlineLeft, underlineWidth, useSpring } = this.state;
const style = {
left: useSpring
? spring(underlineLeft, { stiffness: 220 })
: underlineLeft,
width: useSpring ? spring(underlineWidth) : underlineWidth
};
return (
<div className="layout">
<WindowSize onChange={this.adjustUnderline} />
<div className="wrapper">
<header>
<h1 className="layout-title">unpkg</h1>
<nav className="layout-nav">
<ol
className="layout-navList"
ref={node => (this.listNode = node)}
>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/stats">Stats</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
</ol>
<Motion
defaultStyle={{ left: underlineLeft, width: underlineWidth }}
style={style}
children={style => (
<div
className="layout-navUnderline"
style={{
WebkitTransform: `translate3d(${style.left}px,0,0)`,
transform: `translate3d(${style.left}px,0,0)`,
width: style.width
}}
/>
)}
/>
</nav>
</header>
</div>
<Switch>
<Route
path="/stats"
render={() => <Stats data={this.state.stats} />}
/>
<Route path="/about" component={About} />
<Route path="/" component={Home} />
</Switch>
</div>
);
}
}
module.exports = withRouter(Layout);

View File

@ -1,8 +0,0 @@
.table-filter {
font-size: 0.8em;
text-align: right;
}
.regions-table .country-row td.country-name {
padding-left: 20px;
}

View File

@ -1,323 +0,0 @@
require('./Stats.css');
const React = require('react');
const PropTypes = require('prop-types');
const formatBytes = require('pretty-bytes');
const formatDate = require('date-fns/format');
const parseDate = require('date-fns/parse');
const { continents, countries } = require('countries-list');
const formatNumber = require('../utils/formatNumber');
const formatPercent = require('../utils/formatPercent');
function getCountriesByContinent(continent) {
return Object.keys(countries).filter(
country => countries[country].continent === continent
);
}
function sumKeyValues(hash, keys) {
return keys.reduce((n, key) => n + (hash[key] || 0), 0);
}
function sumValues(hash) {
return Object.keys(hash).reduce((memo, key) => memo + hash[key], 0);
}
class Stats extends React.Component {
static propTypes = {
data: PropTypes.object
};
state = {
minPackageRequests: 1000000,
minCountryRequests: 1000000
};
render() {
const { data } = this.props;
if (data == null) return null;
const totals = data.totals;
// Summary data
const since = parseDate(totals.since);
const until = parseDate(totals.until);
// Packages
const packageRows = [];
Object.keys(totals.requests.package)
.sort((a, b) => {
return totals.requests.package[b] - totals.requests.package[a];
})
.forEach(packageName => {
const requests = totals.requests.package[packageName];
const bandwidth = totals.bandwidth.package[packageName];
if (requests >= this.state.minPackageRequests) {
packageRows.push(
<tr key={packageName}>
<td>
<a
href={`https://npmjs.org/package/${packageName}`}
title={`${packageName} on npm`}
>
{packageName}
</a>
</td>
<td>
{formatNumber(requests)} (
{formatPercent(requests / totals.requests.all)}
%)
</td>
{bandwidth ? (
<td>
{formatBytes(bandwidth)} (
{formatPercent(bandwidth / totals.bandwidth.all)}
%)
</td>
) : (
<td>-</td>
)}
</tr>
);
}
});
// Regions
const regionRows = [];
const continentsData = Object.keys(continents).reduce((memo, continent) => {
const localCountries = getCountriesByContinent(continent);
memo[continent] = {
countries: localCountries,
requests: sumKeyValues(totals.requests.country, localCountries),
bandwidth: sumKeyValues(totals.bandwidth.country, localCountries)
};
return memo;
}, {});
const topContinents = Object.keys(continentsData).sort((a, b) => {
return continentsData[b].requests - continentsData[a].requests;
});
topContinents.forEach(continent => {
const continentName = continents[continent];
const continentData = continentsData[continent];
if (
continentData.requests > this.state.minCountryRequests &&
continentData.bandwidth !== 0
) {
regionRows.push(
<tr key={continent} className="continent-row">
<td>
<strong>{continentName}</strong>
</td>
<td>
<strong>
{formatNumber(continentData.requests)} (
{formatPercent(continentData.requests / totals.requests.all)}
%)
</strong>
</td>
<td>
<strong>
{formatBytes(continentData.bandwidth)} (
{formatPercent(continentData.bandwidth / totals.bandwidth.all)}
%)
</strong>
</td>
</tr>
);
const topCountries = continentData.countries.sort((a, b) => {
return totals.requests.country[b] - totals.requests.country[a];
});
topCountries.forEach(country => {
const countryRequests = totals.requests.country[country];
const countryBandwidth = totals.bandwidth.country[country];
if (countryRequests > this.state.minCountryRequests) {
regionRows.push(
<tr key={continent + country} className="country-row">
<td className="country-name">{countries[country].name}</td>
<td>
{formatNumber(countryRequests)} (
{formatPercent(countryRequests / totals.requests.all)}
%)
</td>
<td>
{formatBytes(countryBandwidth)} (
{formatPercent(countryBandwidth / totals.bandwidth.all)}
%)
</td>
</tr>
);
}
});
}
});
// Protocols
const protocolRows = Object.keys(totals.requests.protocol)
.sort((a, b) => {
return totals.requests.protocol[b] - totals.requests.protocol[a];
})
.map(protocol => {
const requests = totals.requests.protocol[protocol];
return (
<tr key={protocol}>
<td>{protocol}</td>
<td>
{formatNumber(requests)} (
{formatPercent(requests / sumValues(totals.requests.protocol))}
%)
</td>
</tr>
);
});
return (
<div className="wrapper">
<p>
From <strong>{formatDate(since, 'MMM D')}</strong> to{' '}
<strong>{formatDate(until, 'MMM D')}</strong> unpkg served{' '}
<strong>{formatNumber(totals.requests.all)}</strong> requests and a
total of <strong>{formatBytes(totals.bandwidth.all)}</strong> of data
to <strong>{formatNumber(totals.uniques.all)}</strong> unique
visitors,{' '}
<strong>
{formatPercent(totals.requests.cached / totals.requests.all, 0)}%
</strong>{' '}
of which were served from the cache.
</p>
<h3>Packages</h3>
<p>
The table below shows the most popular packages served by unpkg from{' '}
<strong>{formatDate(since, 'MMM D')}</strong> to{' '}
<strong>{formatDate(until, 'MMM D')}</strong>. Only the top{' '}
{Object.keys(totals.requests.package).length} packages are shown.
</p>
<p className="table-filter">
Include only packages that received at least{' '}
<select
value={this.state.minPackageRequests}
onChange={event =>
this.setState({
minPackageRequests: parseInt(event.target.value, 10)
})
}
>
<option value="0">0</option>
<option value="1000">1,000</option>
<option value="10000">10,000</option>
<option value="100000">100,000</option>
<option value="1000000">1,000,000</option>
<option value="10000000">10,000,000</option>
</select>{' '}
requests.
</p>
<table cellSpacing="0" cellPadding="0" style={{ width: '100%' }}>
<thead>
<tr>
<th>
<strong>Package</strong>
</th>
<th>
<strong>Requests (% of total)</strong>
</th>
<th>
<strong>Bandwidth (% of total)</strong>
</th>
</tr>
</thead>
<tbody>{packageRows}</tbody>
</table>
<h3>Regions</h3>
<p>
The table below breaks down requests to unpkg from{' '}
<strong>{formatDate(since, 'MMM D')}</strong> to{' '}
<strong>{formatDate(until, 'MMM D')}</strong> by geographic region.
</p>
<p className="table-filter">
Include only countries that made at least{' '}
<select
value={this.state.minCountryRequests}
onChange={event =>
this.setState({
minCountryRequests: parseInt(event.target.value, 10)
})
}
>
<option value="0">0</option>
<option value="100000">100,000</option>
<option value="1000000">1,000,000</option>
<option value="10000000">10,000,000</option>
<option value="100000000">100,000,000</option>
</select>{' '}
requests.
</p>
<table
cellSpacing="0"
cellPadding="0"
style={{ width: '100%' }}
className="regions-table"
>
<thead>
<tr>
<th>
<strong>Region</strong>
</th>
<th>
<strong>Requests (% of total)</strong>
</th>
<th>
<strong>Bandwidth (% of total)</strong>
</th>
</tr>
</thead>
<tbody>{regionRows}</tbody>
</table>
<h3>Protocols</h3>
<p>
The table below breaks down requests to unpkg from{' '}
<strong>{formatDate(since, 'MMM D')}</strong> to{' '}
<strong>{formatDate(until, 'MMM D')}</strong> by HTTP protocol.
</p>
<table cellSpacing="0" cellPadding="0" style={{ width: '100%' }}>
<thead>
<tr>
<th>
<strong>Protocol</strong>
</th>
<th>
<strong>Requests (% of total)</strong>
</th>
</tr>
</thead>
<tbody>{protocolRows}</tbody>
</table>
</div>
);
}
}
module.exports = Stats;

View File

@ -1,35 +0,0 @@
const React = require('react');
const PropTypes = require('prop-types');
const addEvent = require('../utils/addEvent');
const removeEvent = require('../utils/removeEvent');
const resizeEvent = 'resize';
class WindowSize extends React.Component {
static propTypes = {
onChange: PropTypes.func
};
handleWindowResize = () => {
if (this.props.onChange)
this.props.onChange({
width: window.innerWidth,
height: window.innerHeight
});
};
componentDidMount() {
addEvent(window, resizeEvent, this.handleWindowResize);
}
componentWillUnmount() {
removeEvent(window, resizeEvent, this.handleWindowResize);
}
render() {
return null;
}
}
module.exports = WindowSize;

View File

@ -1,9 +0,0 @@
function addEvent(node, type, handler) {
if (node.addEventListener) {
node.addEventListener(type, handler, false);
} else if (node.attachEvent) {
node.attachEvent('on' + type, handler);
}
}
module.exports = addEvent;

View File

@ -1,5 +0,0 @@
function createHTML(code) {
return { __html: code };
}
module.exports = createHTML;

View File

@ -1,9 +0,0 @@
const React = require('react');
const h = require('./createHTML');
function execScript(code) {
return <script dangerouslySetInnerHTML={h(code)} />;
}
module.exports = execScript;

View File

@ -1,4 +1,4 @@
function formatNumber(n) {
export default function formatNumber(n) {
const digits = String(n).split('');
const groups = [];
@ -8,5 +8,3 @@ function formatNumber(n) {
return groups.join(',');
}
module.exports = formatNumber;

View File

@ -1,5 +1,3 @@
function formatPercent(n, fixed = 1) {
export default function formatPercent(n, fixed = 1) {
return String((n.toPrecision(2) * 100).toFixed(fixed));
}
module.exports = formatPercent;

View File

@ -1,5 +0,0 @@
function parseNumber(s) {
return parseInt(s.replace(/,/g, ''), 10) || 0;
}
module.exports = parseNumber;

View File

@ -1,9 +0,0 @@
function removeEvent(node, type, handler) {
if (node.removeEventListener) {
node.removeEventListener(type, handler, false);
} else if (node.detachEvent) {
node.detachEvent('on' + type, handler);
}
}
module.exports = removeEvent;

View File

@ -1,9 +0,0 @@
// Use babel to compile JSX on the fly.
require('@babel/register')({
only: [/modules\/client/]
});
// Ignore require("*.css") calls.
require.extensions['.css'] = function() {
return {};
};

View File

@ -1,42 +0,0 @@
const webpack = require('webpack');
/**
* Returns a modified copy of the given webpackEntry object with
* the moduleId in front of all other assets.
*/
function prependModuleId(webpackEntry, moduleId) {
if (typeof webpackEntry === 'string') {
return [moduleId, webpackEntry];
}
if (Array.isArray(webpackEntry)) {
return [moduleId, ...webpackEntry];
}
if (webpackEntry && typeof webpackEntry === 'object') {
const entry = { ...webpackEntry };
for (const chunkName in entry) {
if (entry.hasOwnProperty(chunkName)) {
entry[chunkName] = prependModuleId(entry[chunkName], moduleId);
}
}
return entry;
}
throw new Error('Invalid webpack entry object');
}
/**
* Creates a webpack compiler that automatically inlines the
* webpack dev runtime in all entry points.
*/
function createDevCompiler(webpackConfig, webpackRuntimeModuleId) {
return webpack({
...webpackConfig,
entry: prependModuleId(webpackConfig.entry, webpackRuntimeModuleId)
});
}
module.exports = createDevCompiler;

View File

@ -1,54 +0,0 @@
const express = require('express');
const morgan = require('morgan');
const WebpackDevServer = require('webpack-dev-server');
const devErrorHandler = require('errorhandler');
const devAssets = require('./middleware/devAssets');
const createDevCompiler = require('./createDevCompiler');
const createRouter = require('./createRouter');
function createDevServer(publicDir, webpackConfig, devOrigin) {
const compiler = createDevCompiler(
webpackConfig,
`webpack-dev-server/client?${devOrigin}`
);
const server = new WebpackDevServer(compiler, {
// webpack-dev-middleware options
publicPath: webpackConfig.output.publicPath,
quiet: false,
noInfo: false,
stats: {
// https://webpack.js.org/configuration/stats/
assets: true,
colors: true,
version: true,
hash: true,
timings: true,
chunks: false
},
// webpack-dev-server options
contentBase: false,
disableHostCheck: true,
before(app) {
// This runs before webpack-dev-middleware
app.disable('x-powered-by');
app.use(morgan('dev'));
}
});
// This runs after webpack-dev-middleware
server.use(devErrorHandler());
if (publicDir) {
server.use(express.static(publicDir));
}
server.use(devAssets(compiler));
server.use(createRouter());
return server;
}
module.exports = createDevServer;

View File

@ -1,103 +0,0 @@
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
function route(setup) {
const app = express.Router();
setup(app);
return app;
}
function createRouter() {
return route(app => {
app.get('/', require('./actions/serveRootPage'));
app.use(cors());
app.use(bodyParser.json());
app.use(require('./middleware/userToken'));
app.use(
'/api',
route(app => {
app.get('/publicKey', require('./actions/showPublicKey'));
app.post('/auth', require('./actions/createAuth'));
app.get('/auth', require('./actions/showAuth'));
app.post(
'/blacklist',
require('./middleware/requireAuth')('blacklist.add'),
require('./actions/addToBlacklist')
);
app.get(
'/blacklist',
require('./middleware/requireAuth')('blacklist.read'),
require('./actions/showBlacklist')
);
app.delete(
'/blacklist',
require('./middleware/requireAuth')('blacklist.remove'),
require('./actions/removeFromBlacklist')
);
if (process.env.NODE_ENV !== 'test') {
app.get('/stats', require('./actions/showStats'));
}
})
);
// TODO: Remove
app.get('/_publicKey', require('./actions/showPublicKey'));
// TODO: Remove
app.use(
'/_auth',
route(app => {
app.post('/', require('./actions/createAuth'));
app.get('/', require('./actions/showAuth'));
})
);
// TODO: Remove
app.use(
'/_blacklist',
route(app => {
app.post(
'/',
require('./middleware/requireAuth')('blacklist.add'),
require('./actions/addToBlacklist')
);
app.get(
'/',
require('./middleware/requireAuth')('blacklist.read'),
require('./actions/showBlacklist')
);
app.delete(
'*',
require('./middleware/requireAuth')('blacklist.remove'),
require('./middleware/validatePackageURL'),
require('./actions/removeFromBlacklist')
);
})
);
// TODO: Remove
if (process.env.NODE_ENV !== 'test') {
app.get('/_stats', require('./actions/showStats'));
}
app.get(
'*',
require('./middleware/redirectLegacyURLs'),
require('./middleware/validatePackageURL'),
require('./middleware/validatePackageName'),
require('./middleware/validateQuery'),
require('./middleware/checkBlacklist'),
require('./middleware/fetchPackage'),
require('./middleware/findFile'),
require('./actions/serveFile')
);
});
}
module.exports = createRouter;

View File

@ -1,93 +0,0 @@
const http = require('http');
const express = require('express');
const morgan = require('morgan');
const raven = require('raven');
const staticAssets = require('./middleware/staticAssets');
const createRouter = require('./createRouter');
morgan.token('fwd', req => {
const fwd = req.get('x-forwarded-for');
return fwd ? fwd.replace(/\s/g, '') : '-';
});
if (process.env.SENTRY_DSN) {
raven
.config(process.env.SENTRY_DSN, {
release: process.env.HEROKU_RELEASE_VERSION,
autoBreadcrumbs: true
})
.install();
}
// function errorHandler(err, req, res, next) {
// console.error(err.stack);
// res
// .status(500)
// .type("text")
// .send("Internal Server Error");
// next(err);
// }
function createServer(publicDir, statsFile) {
const app = express();
app.disable('x-powered-by');
if (process.env.SENTRY_DSN) {
app.use(raven.requestHandler());
}
if (process.env.NODE_ENV !== 'test') {
app.use(
morgan(
// Modified version of Heroku's 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]'
)
);
}
// app.use(errorHandler);
if (publicDir) {
app.use(express.static(publicDir, { maxAge: '365d' }));
}
if (statsFile) {
app.use(staticAssets(statsFile));
}
app.use(createRouter());
if (process.env.SENTRY_DSN) {
app.use(raven.errorHandler());
}
const server = http.createServer(app);
// Heroku dynos automatically timeout after 30s. Set our
// own timeout here to force sockets to close before that.
// https://devcenter.heroku.com/articles/request-timeout
server.setTimeout(25000, socket => {
const message = `Timeout of 25 seconds exceeded`;
socket.end(
[
'HTTP/1.1 503 Service Unavailable',
'Date: ' + new Date().toGMTString(),
'Content-Length: ' + Buffer.byteLength(message),
'Content-Type: text/plain',
'Connection: close',
'',
message
].join('\r\n')
);
});
return server;
}
module.exports = createServer;

View File

@ -1,43 +0,0 @@
const addMinutes = require('date-fns/add_minutes');
const startOfMinute = require('date-fns/start_of_minute');
const ingestLogs = require('./ingestLogs');
const oneSecond = 1000;
const oneMinute = oneSecond * 60;
let currentWorkload, timer;
function work() {
const now = Date.now();
// The log for a request is typically available within thirty (30) minutes
// of the request taking place under normal conditions. We deliver logs
// ordered by the time that the logs were created, i.e. the timestamp of
// the request when it was received by the edge. Given the order of
// delivery, we recommend waiting a full thirty minutes to ingest a full
// set of logs. This will help ensure that any congestion in the log
// pipeline has passed and a full set of logs can be ingested.
// https://support.cloudflare.com/hc/en-us/articles/216672448-Enterprise-Log-Share-REST-API
const start = startOfMinute(now - oneMinute * 31);
const end = addMinutes(start, 1);
currentWorkload = ingestLogs(start, end);
}
function shutdown() {
console.log('Shutting down...');
clearInterval(timer);
currentWorkload.then(() => {
console.log('Goodbye!');
process.exit();
});
}
work();
process.on('SIGINT', shutdown).on('SIGTERM', shutdown);
timer = setInterval(work, oneMinute);

View File

@ -1,25 +0,0 @@
const BlacklistAPI = require('../BlacklistAPI');
function checkBlacklist(req, res, next) {
BlacklistAPI.includesPackage(req.packageName).then(
blacklisted => {
// Disallow packages that have been blacklisted.
if (blacklisted) {
res
.status(403)
.type('text')
.send(`Package "${req.packageName}" is blacklisted`);
} else {
next();
}
},
error => {
console.error('Unable to fetch the blacklist: %s', error);
// Continue anyway.
next();
}
);
}
module.exports = checkBlacklist;

View File

@ -0,0 +1,5 @@
import corsMiddleware from 'cors';
const cors = corsMiddleware();
export default cors;

View File

@ -1,29 +0,0 @@
const invariant = require('invariant');
const createAssets = require('./utils/createAssets');
/**
* An express middleware that sets req.assets from the
* latest result from a running webpack compiler (i.e. using
* webpack-dev-middleware). Should only be used in dev.
*/
function devAssets(webpackCompiler) {
let assets;
webpackCompiler.plugin('done', stats => {
assets = createAssets(stats.toJson());
});
return (req, res, next) => {
invariant(
assets != null,
'devAssets middleware needs a running compiler; ' +
'use webpack-dev-middleware in front of devAssets'
);
req.assets = assets;
next();
};
}
module.exports = devAssets;

View File

@ -1,18 +1,16 @@
const semver = require('semver');
import semver from 'semver';
const addLeadingSlash = require('../utils/addLeadingSlash');
const createPackageURL = require('../utils/createPackageURL');
const createSearch = require('../utils/createSearch');
const getNpmPackageInfo = require('../utils/getNpmPackageInfo');
const incrementCounter = require('../utils/incrementCounter');
import addLeadingSlash from '../utils/addLeadingSlash';
import createPackageURL from '../utils/createPackageURL';
import createSearch from '../utils/createSearch';
import getNpmPackageInfo from '../utils/getNpmPackageInfo';
function tagRedirect(req, res) {
const version = req.packageInfo['dist-tags'][req.packageVersion];
// Cache tag redirects for 1 minute.
res
.set({
'Cache-Control': 'public, max-age=60',
'Cache-Control': 'public, s-maxage=14400, max-age=3600', // 4 hours on CDN, 1 hour on clients
'Cache-Tag': 'redirect, tag-redirect'
})
.redirect(
@ -28,10 +26,9 @@ function semverRedirect(req, res) {
);
if (maxVersion) {
// Cache semver redirects for 1 minute.
res
.set({
'Cache-Control': 'public, max-age=60',
'Cache-Control': 'public, s-maxage=14400, max-age=3600', // 4 hours on CDN, 1 hour on clients
'Cache-Tag': 'redirect, semver-redirect'
})
.redirect(
@ -61,14 +58,6 @@ function filenameRedirect(req, res) {
) {
// Deprecated, see #63
filename = req.packageConfig[req.query.main];
// Count which packages are using this so we can warn them when we
// remove this functionality.
incrementCounter(
'package-json-custom-main',
req.packageSpec + '?main=' + req.query.main,
1
);
} else if (
req.packageConfig.unpkg &&
typeof req.packageConfig.unpkg === 'string'
@ -80,10 +69,6 @@ function filenameRedirect(req, res) {
) {
// Deprecated, see #63
filename = req.packageConfig.browser;
// Count which packages are using this so we can warn them when we
// remove this functionality.
incrementCounter('package-json-browser-fallback', req.packageSpec, 1);
} else {
filename = req.packageConfig.main || '/index.js';
}
@ -92,7 +77,7 @@ function filenameRedirect(req, res) {
// and URLs resolve correctly.
res
.set({
'Cache-Control': 'public, max-age=31536000, immutable', // 1 year
'Cache-Control': 'public, max-age=31536000', // 1 year
'Cache-Tag': 'redirect, filename-redirect'
})
.redirect(
@ -111,7 +96,7 @@ function filenameRedirect(req, res) {
* version if the request targets a tag or uses a semver version, or to the
* exact filename if the request omits the filename.
*/
function fetchPackage(req, res, next) {
export default function fetchPackage(req, res, next) {
getNpmPackageInfo(req.packageName).then(
packageInfo => {
if (packageInfo == null || packageInfo.versions == null) {
@ -149,5 +134,3 @@ function fetchPackage(req, res, next) {
}
);
}
module.exports = fetchPackage;

View File

@ -1,18 +1,18 @@
const path = require('path');
import path from 'path';
const addLeadingSlash = require('../utils/addLeadingSlash');
const createPackageURL = require('../utils/createPackageURL');
const createSearch = require('../utils/createSearch');
const fetchNpmPackage = require('../utils/fetchNpmPackage');
const getIntegrity = require('../utils/getIntegrity');
const getContentType = require('../utils/getContentType');
import addLeadingSlash from '../utils/addLeadingSlash';
import createPackageURL from '../utils/createPackageURL';
import createSearch from '../utils/createSearch';
import fetchNpmPackage from '../utils/fetchNpmPackage';
import getIntegrity from '../utils/getIntegrity';
import getContentType from '../utils/getContentType';
function indexRedirect(req, res, entry) {
// Redirect to the index file so relative imports
// resolve correctly.
res
.set({
'Cache-Control': 'public, max-age=31536000, immutable', // 1 year
'Cache-Control': 'public, max-age=31536000', // 1 year
'Cache-Tag': 'redirect, index-redirect'
})
.redirect(
@ -124,7 +124,7 @@ const multipleSlash = /\/\/+/;
* Fetch and search the archive to try and find the requested file.
* Redirect to the "index" file if a directory was requested.
*/
function findFile(req, res, next) {
export default function findFile(req, res, next) {
fetchNpmPackage(req.packageConfig).then(tarballStream => {
const entryName = req.filename
.replace(multipleSlash, '/')
@ -137,6 +137,10 @@ function findFile(req, res, next) {
if (!foundEntry) {
return res
.status(404)
.set({
'Cache-Control': 'public, max-age=31536000', // 1 year
'Cache-Tag': 'missing, missing-entry'
})
.type('text')
.send(`Cannot find "${req.filename}" in ${req.packageSpec}`);
}
@ -156,6 +160,10 @@ function findFile(req, res, next) {
} else {
return res
.status(404)
.set({
'Cache-Control': 'public, max-age=31536000', // 1 year
'Cache-Tag': 'missing, missing-index'
})
.type('text')
.send(
`Cannot find an index in "${req.filename}" in ${
@ -173,5 +181,3 @@ function findFile(req, res, next) {
);
});
}
module.exports = findFile;

View File

@ -0,0 +1,15 @@
import morgan from 'morgan';
const logger = morgan(
process.env.NODE_ENV === 'development'
? 'dev'
: ':date[clf] - :method :url :status :res[content-length] - :response-time ms',
{
skip:
process.env.NODE_ENV === 'production'
? (req, res) => res.statusCode < 400 // Log only errors in production
: () => process.env.NODE_ENV === 'test' // Skip logging in test env
}
);
export default logger;

Some files were not shown because too many files have changed in this diff Show More