Merge branch 'master' into percent-40
This commit is contained in:
commit
1ee88d0166
13
.eslintrc
13
.eslintrc
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"singleQuote": true
|
||||
}
|
28
.travis.yml
28
.travis.yml
|
@ -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
|
||||
|
|
2
Procfile
2
Procfile
|
@ -1,2 +0,0 @@
|
|||
web: node server.js
|
||||
ingest_logs: node modules/ingestLogsEveryMinute.js
|
|
@ -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).
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
env: standard
|
||||
instance_class: B4
|
||||
runtime: nodejs10
|
||||
basic_scaling:
|
||||
max_instances: 1
|
|
@ -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
|
140
docs/api.md
140
docs/api.md
|
@ -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 |
48
docs/home.md
48
docs/home.md
|
@ -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.
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -1 +1 @@
|
|||
module.exports = {};
|
||||
export default {};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
const closeDatabase = require('./utils/closeDatabase');
|
||||
import closeDatabase from './utils/closeDatabase';
|
||||
|
||||
afterAll(closeDatabase);
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
const BlacklistAPI = require('../../BlacklistAPI');
|
||||
|
||||
function clearBlacklist(done) {
|
||||
BlacklistAPI.removeAllPackages().then(done, done);
|
||||
}
|
||||
|
||||
module.exports = clearBlacklist;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
const BlacklistAPI = require('../../BlacklistAPI');
|
||||
|
||||
function withBlacklist(blacklist, done) {
|
||||
Promise.all(blacklist.map(BlacklistAPI.addPackage)).then(done);
|
||||
}
|
||||
|
||||
module.exports = withBlacklist;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,3 @@
|
|||
export default function serveAuth(req, res) {
|
||||
res.send({ auth: req.user });
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { publicKey } from '../utils/secret';
|
||||
|
||||
export default function servePublicKey(req, res) {
|
||||
res.send({ publicKey });
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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' });
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
function showAuth(req, res) {
|
||||
res.send({ auth: req.user });
|
||||
}
|
||||
|
||||
module.exports = showAuth;
|
|
@ -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;
|
|
@ -1,7 +0,0 @@
|
|||
const secretKey = require('../secretKey');
|
||||
|
||||
function showPublicKey(req, res) {
|
||||
res.send({ publicKey: secretKey.public });
|
||||
}
|
||||
|
||||
module.exports = showPublicKey;
|
|
@ -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;
|
|
@ -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)
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { createElement as default } from 'react';
|
|
@ -0,0 +1,3 @@
|
|||
export default function createHTML(code) {
|
||||
return { __html: code };
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import createElement from './createElement';
|
||||
import createHTML from './createHTML';
|
||||
|
||||
export default function createScript(script) {
|
||||
return createElement('script', {
|
||||
dangerouslySetInnerHTML: createHTML(script)
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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] });
|
||||
});
|
||||
}
|
|
@ -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 }]
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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__ || {};
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -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%;
|
||||
}
|
|
@ -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;
|
|
@ -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 |
|
@ -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 “bare” 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">
|
||||
“bare” <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'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's it! Now when you <code>npm publish</code> you'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'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'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' }}>
|
||||
© {new Date().getFullYear()} unpkg — 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 |
|
@ -1,6 +0,0 @@
|
|||
.home-example {
|
||||
text-align: center;
|
||||
background-color: #eee;
|
||||
margin: 2em 0;
|
||||
padding: 5px 0;
|
||||
}
|
|
@ -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;
|
|
@ -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.
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
|
@ -1,8 +0,0 @@
|
|||
.table-filter {
|
||||
font-size: 0.8em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.regions-table .country-row td.country-name {
|
||||
padding-left: 20px;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,5 +0,0 @@
|
|||
function createHTML(code) {
|
||||
return { __html: code };
|
||||
}
|
||||
|
||||
module.exports = createHTML;
|
|
@ -1,9 +0,0 @@
|
|||
const React = require('react');
|
||||
|
||||
const h = require('./createHTML');
|
||||
|
||||
function execScript(code) {
|
||||
return <script dangerouslySetInnerHTML={h(code)} />;
|
||||
}
|
||||
|
||||
module.exports = execScript;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
function parseNumber(s) {
|
||||
return parseInt(s.replace(/,/g, ''), 10) || 0;
|
||||
}
|
||||
|
||||
module.exports = parseNumber;
|
|
@ -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;
|
|
@ -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 {};
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -0,0 +1,5 @@
|
|||
import corsMiddleware from 'cors';
|
||||
|
||||
const cors = corsMiddleware();
|
||||
|
||||
export default cors;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue