Experimental port to Firebase hosting

This commit is contained in:
Michael Jackson 2019-01-05 16:50:05 -08:00
parent e4d6df255e
commit 31e7d3865a
300 changed files with 129300 additions and 5817 deletions

View File

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

5
.firebaserc Normal file
View File

@ -0,0 +1,5 @@
{
"projects": {
"default": "unpkg-gcp"
}
}

9
.gitignore vendored
View File

@ -1,12 +1,13 @@
.DS_Store .DS_Store
firebase-debug.log*
npm-debug.log* npm-debug.log*
yarn-debug.log*
yarn-error.log*
/.env /.env
/functions/*.js
/functions/node_modules/
/node_modules/ /node_modules/
/public/_assets/ /public/_assets/
/dump.rdb /manifest.json
/stats.json
/secret_key /secret_key
/tokens/ /tokens/

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
8.14.0

3
.prettierrc Normal file
View File

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

12
.size-snapshot.json Normal file
View File

@ -0,0 +1,12 @@
{
"public/_assets/main.js": {
"bundled": 106742,
"minified": 41138,
"gzipped": 12988
},
"public/_assets/autoIndex.js": {
"bundled": 44018,
"minified": 15077,
"gzipped": 5142
}
}

View File

@ -1,17 +1,20 @@
language: node_js language: node_js
node_js: node node_js: node
cache: npm cache: npm
services: env:
- redis-server secure: Z/uibhTgn2S4fy12WQs1wopytecb0Eo2O1qkT4FyEuu4xjSM0ZQ5zKP3eRSRAfr7tTZbTro1zRGWjNCQjJSNvm/6Ftyzp7aN8tFd7zVlZSGIJWGGItB1vqz3ls5ynt6EGLk6SRPtsHCiunaIzYUCLl5c1kFyjyqW3Aab77TQL6sNeTEOb2nS9wDi4xEfS1yDcJIT3swupHf4+tHtTHBMqwtvTpqdyRwkoauAaP94qWP2Glz3YnEpgJFLfvI9MHlOG+dMs6iroJ6UTzNfYOeWKq7xXrmtH2DY0u93fa9JOFIe4CrcSEt88fSO4o1BcdnPitLoB5GfgC8G4IanmtPTMl/3lrnjA2LkhqNs4w4ad9Xhd0fMLRKDofKSRarhMmfMxRY8o14K/2AmUWoS6DBjiOoHap7UTwdPxh2RmEhctG9ufvaQOXD6LAT2pewx9I9Jdg/FrEF7pCnuJxV8jFn8CryQw9QFYAqvgesjJNuHhjfoQdtuqNAQZl6RjoD0lg5iTnr/iwxfE6ayPilKPw7bJNw+yPUgNC//0rCj/KGcOS+9Ho2eD48Qh0GWpOOYBHhxVoo9oF2M1evlMpuuMXfM5KT++XZbxUbDZj1L22eYQDKF0Cwaf4NLCJ0/K5Wwcjphshmk7iy2t5c+JL76T5lFTt/aYQMGNdMdqB/Ato8aow8=
script: script:
- npm run lint
- npm test
- npm run build - npm run build
before_deploy:
- npm install -g firebase-tools
deploy: deploy:
provider: heroku - provider: script
skip_cleanup: true skip_cleanup: true
app: unpkg script: $(npm bin -g)/firebase deploy
api_key: --project unpkg-gcp
secure: qJTOiZE1hbghS0U7xpqU/38jm1Dga8bycsgDTW2wOSfAxqxtR4rAilW3ffBHY8/VSqt5GLkyPqFzb9R3I+xlcK6oakQyNMFGjQwMAqwdtHjucr45jWYNeqWf6Nbrrj+0LiQiH/uqMB4mKPquGqx2k2JIHW6jy5ndK14lI9a7SP44edyNugjhNW+OOYjwqo6maHvRoFNxL7kxrw7Js90d0gIKXpunBqHmxmr/hlYcGr9qJ+bLUXxmObxpoFWKHE0iTsTw3C3y558wZpHTfa3vXIvhseW2q7ATuRRVBv8StNjYxYSGpeZA+59d48z3EPK/mp7ybZO6cWyaBdcA9yF3thISEXlXcRcCck5KYk4gwzgrcfug59Z1VJUVXCywJTUu42V5ISoyUKJ+DelkkvMpXQH9T9fxm5eWEwb7sb+UTo3R8mlFliBogvXrWOKNXeRFsLQXH3/YmOUWONUUYRJS11K/p+sFOTfTfaH5lS0AFKCAKtvfLjdLUclsmmrITSFQa42/SRsFx88v3RxDL93r2838p51N7U/RqOqTNfQ8JR2iQOWlthMEvnVZMNs3VT9lW5eYIdTd5kpW/ARVtQf2drVVYR5BzNRJlGyw7M4x1riwNO7XhNqH76Iy/zMbeEDGaXqBCfzgq9vsRAjZm/TFKw8eOEy8+EGxLrBKD1U7EuI= --message "https://travis-ci.com/$TRAVIS_REPO_SLUG/builds/$TRAVIS_BUILD_ID"
--token $FIREBASE_TOKEN
--non-interactive
--force
on: on:
branch: master branch: firebase-hosting

View File

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

13
firebase.json Normal file
View File

@ -0,0 +1,13 @@
{
"hosting": {
"public": "public",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{ "source": "/api/auth", "function": "serveAuth" },
{ "source": "/api/public-key", "function": "servePublicKey" },
{ "source": "/api/stats", "function": "serveStats" },
{ "source": "**/", "function": "serveAutoIndexPage" },
{ "source": "**", "function": "serveNpmPackageFile" }
]
}
}

4744
functions/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
functions/package.json Normal file
View File

@ -0,0 +1,50 @@
{
"private": true,
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"serve": "firebase serve --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"dependencies": {
"@babel/core": "^7.2.2",
"@babel/plugin-proposal-class-properties": "^7.2.3",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-syntax-export-default-from": "^7.2.0",
"@babel/plugin-syntax-export-namespace-from": "^7.2.0",
"@babel/plugin-syntax-import-meta": "^7.2.0",
"cheerio": "^1.0.0-rc.2",
"cors": "^2.8.5",
"date-fns": "^1.30.1",
"etag": "^1.8.1",
"express": "^4.16.4",
"firebase-admin": "^6.0.0",
"firebase-functions": "^2.1.0",
"gunzip-maybe": "^1.4.1",
"invariant": "^2.2.4",
"isomorphic-fetch": "^2.2.1",
"jsonwebtoken": "^8.4.0",
"lru-cache": "^5.1.1",
"mime": "^2.4.0",
"ndjson": "^1.5.0",
"node-forge": "^0.7.6",
"pretty-bytes": "^5.1.0",
"prop-types": "^15.6.2",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"semver": "^5.6.0",
"sort-by": "^1.2.0",
"sri-toolbox": "^0.2.0",
"tar-stream": "^1.6.2",
"validate-npm-package-name": "^3.0.0",
"warning": "^4.0.2",
"whatwg-url": "^7.0.0"
},
"devDependencies": {},
"engines": {
"node": "8"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,8 @@
const request = require('supertest'); import request from 'supertest';
const createServer = require('../createServer'); import createServer from '../createServer';
import clearBlacklist from './utils/clearBlacklist';
const clearBlacklist = require('./utils/clearBlacklist'); import withBlacklist from './utils/withBlacklist';
const withBlacklist = require('./utils/withBlacklist');
describe('The server', () => { describe('The server', () => {
let server; let server;

View File

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

View File

@ -1,7 +1,5 @@
const BlacklistAPI = require('../../BlacklistAPI'); import { removeAllPackages } from '../../utils/blacklist';
function clearBlacklist(done) { export default function clearBlacklist(done) {
BlacklistAPI.removeAllPackages().then(done, done); removeAllPackages().then(done, done);
} }
module.exports = clearBlacklist;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
const validateNpmPackageName = require('validate-npm-package-name'); import validateNpmPackageName from 'validate-npm-package-name';
const BlacklistAPI = require('../BlacklistAPI'); import { addPackage } from '../utils/blacklist';
function addToBlacklist(req, res) { export default function addToBlacklist(req, res) {
const packageName = req.body.packageName; const packageName = req.body.packageName;
if (!packageName) { if (!packageName) {
@ -21,7 +21,7 @@ function addToBlacklist(req, res) {
}); });
} }
BlacklistAPI.addPackage(packageName).then( addPackage(packageName).then(
added => { added => {
if (added) { if (added) {
const userId = req.user.jti; const userId = req.user.jti;
@ -45,5 +45,3 @@ function addToBlacklist(req, res) {
} }
); );
} }
module.exports = addToBlacklist;

View File

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

View File

@ -1,8 +1,8 @@
const validateNpmPackageName = require('validate-npm-package-name'); import validateNpmPackageName from 'validate-npm-package-name';
const BlacklistAPI = require('../BlacklistAPI'); import { removePackage } from '../utils/blacklist';
function removeFromBlacklist(req, res) { export default function removeFromBlacklist(req, res) {
// TODO: Remove req.packageName when DELETE // TODO: Remove req.packageName when DELETE
// /_blacklist/:packageName API is removed // /_blacklist/:packageName API is removed
const packageName = req.body.packageName || req.packageName; const packageName = req.body.packageName || req.packageName;
@ -23,7 +23,7 @@ function removeFromBlacklist(req, res) {
}); });
} }
BlacklistAPI.removePackage(packageName).then( removePackage(packageName).then(
removed => { removed => {
if (removed) { if (removed) {
const userId = req.user.jti; const userId = req.user.jti;
@ -48,5 +48,3 @@ function removeFromBlacklist(req, res) {
} }
); );
} }
module.exports = removeFromBlacklist;

View File

@ -1,30 +1,30 @@
const React = require('react'); import React from 'react';
const ReactDOMServer = require('react-dom/server'); import ReactDOMServer from 'react-dom/server';
const semver = require('semver'); import semver from 'semver';
const MainPage = require('../client/MainPage'); import MainPage from '../client/MainPage';
const AutoIndexApp = require('../client/autoIndex/App'); import AutoIndexApp from '../client/autoIndex/App';
const createHTML = require('../client/utils/createHTML'); import createHTML from '../client/utils/createHTML';
const renderPage = require('../utils/renderPage'); import renderPage from '../utils/renderPage';
const globalScripts = const globalScripts =
process.env.NODE_ENV === 'production' process.env.NODE_ENV === 'production'
? [ ? [
'/react@16.4.1/umd/react.production.min.js', '/react@16.7.0/umd/react.production.min.js',
'/react-dom@16.4.1/umd/react-dom.production.min.js' '/react-dom@16.7.0/umd/react-dom.production.min.js'
] ]
: [ : [
'/react@16.4.1/umd/react.development.js', '/react@16.7.0/umd/react.development.js',
'/react-dom@16.4.1/umd/react-dom.development.js' '/react-dom@16.7.0/umd/react-dom.development.js'
]; ];
function byVersion(a, b) { function byVersion(a, b) {
return semver.lt(a, b) ? -1 : semver.gt(a, b) ? 1 : 0; return semver.lt(a, b) ? -1 : semver.gt(a, b) ? 1 : 0;
} }
function serveAutoIndexPage(req, res) { export default function serveAutoIndexPage(req, res) {
const scripts = globalScripts.concat(req.assets.getScripts('autoIndex')); const scripts = globalScripts.concat('/_assets/autoIndex.js');
const styles = req.assets.getStyles('autoIndex'); const styles = ['/autoIndex.css'];
const props = { const props = {
packageName: req.packageName, packageName: req.packageName,
@ -54,5 +54,3 @@ function serveAutoIndexPage(req, res) {
}) })
.send(html); .send(html);
} }
module.exports = serveAutoIndexPage;

View File

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

View File

@ -1,10 +1,10 @@
const etag = require('etag'); import etag from 'etag';
const cheerio = require('cheerio'); import cheerio from 'cheerio';
const getContentTypeHeader = require('../utils/getContentTypeHeader'); import getContentTypeHeader from '../utils/getContentTypeHeader';
const rewriteBareModuleIdentifiers = require('../utils/rewriteBareModuleIdentifiers'); import rewriteBareModuleIdentifiers from '../utils/rewriteBareModuleIdentifiers';
function serveHTMLModule(req, res) { export default function serveHTMLModule(req, res) {
try { try {
const $ = cheerio.load(req.entry.content.toString('utf8')); const $ = cheerio.load(req.entry.content.toString('utf8'));
@ -46,5 +46,3 @@ function serveHTMLModule(req, res) {
); );
} }
} }
module.exports = serveHTMLModule;

View File

@ -1,9 +1,9 @@
const etag = require('etag'); import etag from 'etag';
const getContentTypeHeader = require('../utils/getContentTypeHeader'); import getContentTypeHeader from '../utils/getContentTypeHeader';
const rewriteBareModuleIdentifiers = require('../utils/rewriteBareModuleIdentifiers'); import rewriteBareModuleIdentifiers from '../utils/rewriteBareModuleIdentifiers';
function serveJavaScriptModule(req, res) { export default function serveJavaScriptModule(req, res) {
try { try {
const code = rewriteBareModuleIdentifiers( const code = rewriteBareModuleIdentifiers(
req.entry.content.toString('utf8'), req.entry.content.toString('utf8'),
@ -40,5 +40,3 @@ function serveJavaScriptModule(req, res) {
); );
} }
} }
module.exports = serveJavaScriptModule;

View File

@ -1,6 +1,6 @@
const path = require('path'); import path from 'path';
const addLeadingSlash = require('../utils/addLeadingSlash'); import addLeadingSlash from '../utils/addLeadingSlash';
function getMatchingEntries(entry, entries) { function getMatchingEntries(entry, entries) {
const dirname = entry.name || '.'; const dirname = entry.name || '.';
@ -30,7 +30,7 @@ function getMetadata(entry, entries) {
return metadata; return metadata;
} }
function serveMetadata(req, res) { export default function serveMetadata(req, res) {
const metadata = getMetadata(req.entry, req.entries); const metadata = getMetadata(req.entry, req.entries);
res res
@ -40,5 +40,3 @@ function serveMetadata(req, res) {
}) })
.send(metadata); .send(metadata);
} }
module.exports = serveMetadata;

View File

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

View File

@ -1,9 +1,9 @@
const path = require('path'); import path from 'path';
const etag = require('etag'); 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 tags = ['file'];
const ext = path.extname(req.entry.name).substr(1); const ext = path.extname(req.entry.name).substr(1);
@ -22,5 +22,3 @@ function serveStaticFile(req, res) {
}) })
.send(req.entry.content); .send(req.entry.content);
} }
module.exports = serveStaticFile;

View File

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

View File

@ -1,7 +1,7 @@
const BlacklistAPI = require('../BlacklistAPI'); import { getPackages } from '../utils/blacklist';
function showBlacklist(req, res) { export default function showBlacklist(req, res) {
BlacklistAPI.getPackages().then( getPackages().then(
blacklist => { blacklist => {
res.send({ blacklist }); res.send({ blacklist });
}, },
@ -13,5 +13,3 @@ function showBlacklist(req, res) {
} }
); );
} }
module.exports = showBlacklist;

View File

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

View File

@ -1,29 +1,37 @@
const subDays = require('date-fns/sub_days'); import { subDays, startOfDay, startOfSecond } from 'date-fns';
const startOfDay = require('date-fns/start_of_day');
const startOfSecond = require('date-fns/start_of_second');
const StatsAPI = require('../StatsAPI'); import { getStats } from '../utils/stats';
function showStats(req, res) { export default function showStats(req, res) {
let since, until; let since, until;
switch (req.query.period) { if (req.query.period) {
case 'last-day': switch (req.query.period) {
until = startOfDay(new Date()); case 'last-day':
since = subDays(until, 1); until = startOfDay(new Date());
break; since = subDays(until, 1);
case 'last-week': break;
until = startOfDay(new Date()); case 'last-week':
since = subDays(until, 7); until = startOfDay(new Date());
break; since = subDays(until, 7);
case 'last-month': break;
until = startOfDay(new Date()); case 'last-month':
since = subDays(until, 30); default:
break; until = startOfDay(new Date());
default: since = subDays(until, 30);
until = req.query.until }
? new Date(req.query.until) } else {
: startOfSecond(new Date()); if (!req.query.since) {
since = new Date(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())) { if (isNaN(since.getTime())) {
@ -44,7 +52,7 @@ function showStats(req, res) {
return res.status(403).send({ error: '?until must be a date in the past' }); return res.status(403).send({ error: '?until must be a date in the past' });
} }
StatsAPI.getStats(since, until).then( getStats(since, until).then(
stats => { stats => {
res res
.set({ .set({
@ -59,5 +67,3 @@ function showStats(req, res) {
} }
); );
} }
module.exports = showStats;

View File

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

View File

@ -1,22 +1,30 @@
const React = require('react'); import React from 'react';
const PropTypes = require('prop-types'); import PropTypes from 'prop-types';
const createHTML = require('./utils/createHTML'); import createHTML from './utils/createHTML';
const x = require('./utils/execScript'); import x from './utils/execScript';
function MainPage({ title, description, scripts, styles, data, content }) { export default function MainPage({
title,
description,
favicon,
scripts,
styles,
data,
content
}) {
return ( return (
<html lang="en"> <html lang="en">
<head> <head>
<meta charSet="utf-8" /> <meta charSet="utf-8" />
<meta name="description" content={description} />
<meta httpEquiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta httpEquiv="X-UA-Compatible" content="IE=edge,chrome=1" />
{description && <meta name="description" content={description} />}
<meta <meta
name="viewport" name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1" content="width=device-width,initial-scale=1,maximum-scale=1"
/> />
<meta name="timestamp" content={new Date().toISOString()} /> <meta name="timestamp" content={new Date().toISOString()} />
<link rel="shortcut icon" href="/favicon.ico" /> {favicon && <link rel="shortcut icon" href={favicon} />}
{styles.map(s => ( {styles.map(s => (
<link key={s} rel="stylesheet" href={s} /> <link key={s} rel="stylesheet" href={s} />
))} ))}
@ -39,6 +47,16 @@ function MainPage({ title, description, scripts, styles, data, content }) {
); );
} }
MainPage.defaultProps = {
title: 'UNPKG',
description: 'The CDN for everything on npm',
favicon: '/favicon.ico',
scripts: [],
styles: [],
data: {},
content: createHTML('')
};
const htmlType = PropTypes.shape({ const htmlType = PropTypes.shape({
__html: PropTypes.string __html: PropTypes.string
}); });
@ -46,19 +64,9 @@ const htmlType = PropTypes.shape({
MainPage.propTypes = { MainPage.propTypes = {
title: PropTypes.string, title: PropTypes.string,
description: PropTypes.string, description: PropTypes.string,
favicon: PropTypes.string,
scripts: PropTypes.arrayOf(PropTypes.string), scripts: PropTypes.arrayOf(PropTypes.string),
styles: PropTypes.arrayOf(PropTypes.string), styles: PropTypes.arrayOf(PropTypes.string),
data: PropTypes.any, data: PropTypes.any,
content: htmlType content: htmlType
}; };
MainPage.defaultProps = {
title: 'UNPKG',
description: 'The CDN for everything on npm',
scripts: [],
styles: [],
data: {},
content: createHTML('')
};
module.exports = MainPage;

View File

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

View File

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

View File

@ -1,10 +1,43 @@
require('./App.css'); import React from 'react';
import PropTypes from 'prop-types';
const React = require('react'); import DirectoryListing from './DirectoryListing';
const DirectoryListing = require('./DirectoryListing'); const styles = {
wrapper: {
maxWidth: 900,
margin: '0 auto'
},
header: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between'
},
versionSelector: {
float: 'right',
lineHeight: '2.25em'
},
versionDropdown: {
fontSize: '1em'
},
address: {
textAlign: 'right'
}
};
const entryType = PropTypes.object;
export default class App extends React.Component {
static propTypes = {
packageName: PropTypes.string.isRequired,
packageVersion: PropTypes.string.isRequired,
availableVersions: PropTypes.arrayOf(PropTypes.string),
filename: PropTypes.string.isRequired,
entry: entryType.isRequired,
entries: PropTypes.objectOf(entryType).isRequired
};
class App extends React.Component {
static defaultProps = { static defaultProps = {
availableVersions: [] availableVersions: []
}; };
@ -18,19 +51,20 @@ class App extends React.Component {
render() { render() {
return ( return (
<div className="app"> <div style={styles.wrapper}>
<header className="app-header"> <header style={styles.header}>
<h1> <h1>
Index of /{this.props.packageName}@{this.props.packageVersion} Index of /{this.props.packageName}@{this.props.packageVersion}
{this.props.filename} {this.props.filename}
</h1> </h1>
<div className="app-version-selector"> <div style={styles.versionSelector}>
Version:{' '} Version:{' '}
<select <select
id="version" id="version"
defaultValue={this.props.packageVersion} defaultValue={this.props.packageVersion}
onChange={this.handleChange} onChange={this.handleChange}
style={styles.versionDropdown}
> >
{this.props.availableVersions.map(v => ( {this.props.availableVersions.map(v => (
<option key={v} value={v}> <option key={v} value={v}>
@ -51,27 +85,10 @@ class App extends React.Component {
<hr /> <hr />
<address className="app-address"> <address style={styles.address}>
{this.props.packageName}@{this.props.packageVersion} {this.props.packageName}@{this.props.packageVersion}
</address> </address>
</div> </div>
); );
} }
} }
if (process.env.NODE_ENV === 'development') {
const PropTypes = require('prop-types');
const entryType = PropTypes.object;
App.propTypes = {
packageName: PropTypes.string.isRequired,
packageVersion: PropTypes.string.isRequired,
availableVersions: PropTypes.arrayOf(PropTypes.string),
filename: PropTypes.string.isRequired,
entry: entryType.isRequired,
entries: PropTypes.objectOf(entryType).isRequired
};
}
module.exports = App;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,95 @@
require('./About.css'); import React from 'react';
const React = require('react'); import Wrapper from './Wrapper';
const h = require('../utils/createHTML'); import cloudflareLogo from './CloudflareLogo.png';
const markup = require('./About.md'); import herokuLogo from './HerokuLogo.png';
function About() { const styles = {
return <div className="wrapper" dangerouslySetInnerHTML={h(markup)} />; logoList: {
margin: '2em 0',
display: 'flex',
justifyContent: 'center'
},
logo: {
textAlign: 'center',
flex: '1',
maxWidth: '80%'
},
logoImage: {
maxWidth: '60%'
}
};
function AboutLogo({ children }) {
return <div style={styles.logo}>{children}</div>;
} }
module.exports = About; function AboutLogoImage(props) {
return <img {...props} style={styles.logoImage} />;
}
export default function About() {
return (
<Wrapper>
<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>.
</p>
<h3 id="sponsors">Sponsors</h3>
<p>
The fast, global infrastructure that powers unpkg is generously donated
by <a href="https://www.cloudflare.com">Cloudflare</a> and{' '}
<a href="https://www.heroku.com">Heroku</a>.
</p>
<div style={styles.logoList}>
<AboutLogo>
<a href="https://www.cloudflare.com">
<AboutLogoImage src={cloudflareLogo} />
</a>
</AboutLogo>
<AboutLogo>
<a href="https://www.heroku.com">
<AboutLogoImage src={herokuLogo} />
</a>
</AboutLogo>
</div>
<h3 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 id="abuse">Abuse</h3>
<p>
unpkg maintains a list of packages that are known to be malicious. If
you find such a package on npm, please let us know!
</p>
<h3 id="support">Support</h3>
<p>
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>
</Wrapper>
);
}

View File

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

View File

@ -1,7 +1,7 @@
const React = require('react'); import React from 'react';
const { HashRouter } = require('react-router-dom'); import { HashRouter } from 'react-router-dom';
const Layout = require('./Layout'); import Layout from './Layout';
function App() { function App() {
return ( return (
@ -11,4 +11,4 @@ function App() {
); );
} }
module.exports = App; export default App;

View File

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

View File

@ -1,12 +1,159 @@
require('./Home.css'); import React from 'react';
const React = require('react'); import Wrapper from './Wrapper';
const h = require('../utils/createHTML'); const styles = {
const markup = require('./Home.md'); homeExample: {
textAlign: 'center',
backgroundColor: '#eee',
margin: '2em 0',
padding: '5px 0'
}
};
function Home() { export default function Home() {
return <div className="wrapper" dangerouslySetInnerHTML={h(markup)} />; return (
<Wrapper>
<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 style={styles.homeExample}>unpkg.com/:package@:version/:file</div>
<h3>Examples</h3>
<p>Using a fixed version:</p>
<ul>
<li>
<a href="/react@16.0.0/umd/react.production.min.js">
unpkg.com/react@16.0.0/umd/react.production.min.js
</a>
</li>
<li>
<a href="/react-dom@16.0.0/umd/react-dom.production.min.js">
unpkg.com/react-dom@16.0.0/umd/react-dom.production.min.js
</a>
</li>
</ul>
<p>
You may also use a{' '}
<a href="https://docs.npmjs.com/misc/semver">semver range</a> or a{' '}
<a href="https://docs.npmjs.com/cli/dist-tag">tag</a> instead of a fixed
version number, or omit the version/tag entirely to use the{' '}
<code>latest</code> tag.
</p>
<ul>
<li>
<a href="/react@^16/umd/react.production.min.js">
unpkg.com/react@^16/umd/react.production.min.js
</a>
</li>
<li>
<a href="/react/umd/react.production.min.js">
unpkg.com/react/umd/react.production.min.js
</a>
</li>
</ul>
<p>
If you omit the file path (i.e. use a &ldquo;bare&rdquo; URL), unpkg
will serve the file specified by the <code>unpkg</code> field in{' '}
<code>package.json</code>, or fall back to
<code>main</code>.
</p>
<ul>
<li>
<a href="/d3">unpkg.com/d3</a>
</li>
<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>Query Parameters</h3>
<dl>
<dt>
<code>?meta</code>
</dt>
<dd>
Return metadata about any file in a package as JSON (e.g.
<code>/any/file?meta</code>)
</dd>
<dt>
<code>?module</code>
</dt>
<dd>
Expands all{' '}
<a href="https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier">
&ldquo;bare&rdquo; <code>import</code> specifiers
</a>
in JavaScript modules to unpkg URLs. This feature is{' '}
<em>very experimental</em>
</dd>
</dl>
<h3>Workflow</h3>
<p>
For npm package authors, unpkg relieves the burden of publishing your
code to a CDN in addition to the npm registry. All you need to do is
include your <a href="https://github.com/umdjs/umd">UMD</a> build in
your npm package (not your repo, that&apos;s different!).
</p>
<p>You can do this easily using the following setup:</p>
<ul>
<li>
Add the <code>umd</code> (or <code>dist</code>) directory to your{' '}
<code>.gitignore</code> file
</li>
<li>
Add the <code>umd</code> directory to your{' '}
<a href="https://docs.npmjs.com/files/package.json#files">
files array
</a>{' '}
in
<code>package.json</code>
</li>
<li>
Use a build script to generate your UMD build in the <code>umd</code>{' '}
directory when you publish
</li>
</ul>
<p>
That&apos;s it! Now when you <code>npm publish</code> you&apos;ll have a
version available on unpkg as well.
</p>
</Wrapper>
);
} }
module.exports = Home;

View File

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

View File

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

View File

@ -1,14 +1,47 @@
require('./Layout.css'); import React from 'react';
import PropTypes from 'prop-types';
import { Switch, Route, Link, withRouter } from 'react-router-dom';
import { Motion, spring } from 'react-motion';
const React = require('react'); import WindowSize from './WindowSize';
const PropTypes = require('prop-types'); import About from './About';
const { Switch, Route, Link, withRouter } = require('react-router-dom'); import Stats from './Stats';
const { Motion, spring } = require('react-motion'); import Home from './Home';
const WindowSize = require('./WindowSize'); const styles = {
const About = require('./About'); title: {
const Stats = require('./Stats'); margin: 0,
const Home = require('./Home'); textTransform: 'uppercase',
textAlign: 'center',
fontSize: '5em'
},
nav: {
margin: '0 0 3em'
},
navList: {
margin: 0,
padding: 0,
display: 'flex',
justifyContent: 'center'
},
navListItem: {
flexBasis: 'auto',
listStyleType: 'none',
display: 'inline-block',
fontSize: '1.1em',
margin: '0 10px'
},
navLink: {
textDecoration: 'none',
color: 'black'
},
navUnderline: {
height: 4,
backgroundColor: 'black',
position: 'absolute',
left: 0
}
};
class Layout extends React.Component { class Layout extends React.Component {
static propTypes = { static propTypes = {
@ -66,8 +99,9 @@ class Layout extends React.Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (prevProps.location.pathname !== this.props.location.pathname) if (prevProps.location.pathname !== this.props.location.pathname) {
this.adjustUnderline(true); this.adjustUnderline(true);
}
} }
render() { render() {
@ -85,20 +119,23 @@ class Layout extends React.Component {
<WindowSize onChange={this.adjustUnderline} /> <WindowSize onChange={this.adjustUnderline} />
<div className="wrapper"> <div className="wrapper">
<header> <header>
<h1 className="layout-title">unpkg</h1> <h1 style={styles.title}>unpkg</h1>
<nav className="layout-nav"> <nav style={styles.nav}>
<ol <ol style={styles.navList} ref={node => (this.listNode = node)}>
className="layout-navList" <li style={styles.navListItem}>
ref={node => (this.listNode = node)} <Link to="/" style={styles.navLink}>
> Home
<li> </Link>
<Link to="/">Home</Link>
</li> </li>
<li> <li style={styles.navListItem}>
<Link to="/stats">Stats</Link> <Link to="/stats" style={styles.navLink}>
Stats
</Link>
</li> </li>
<li> <li style={styles.navListItem}>
<Link to="/about">About</Link> <Link to="/about" style={styles.navLink}>
About
</Link>
</li> </li>
</ol> </ol>
<Motion <Motion
@ -106,8 +143,8 @@ class Layout extends React.Component {
style={style} style={style}
children={style => ( children={style => (
<div <div
className="layout-navUnderline"
style={{ style={{
...styles.navUnderline,
WebkitTransform: `translate3d(${style.left}px,0,0)`, WebkitTransform: `translate3d(${style.left}px,0,0)`,
transform: `translate3d(${style.left}px,0,0)`, transform: `translate3d(${style.left}px,0,0)`,
width: style.width width: style.width
@ -132,4 +169,4 @@ class Layout extends React.Component {
} }
} }
module.exports = withRouter(Layout); export default withRouter(Layout);

View File

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

View File

@ -1,14 +1,24 @@
require('./Stats.css'); import React from 'react';
import PropTypes from 'prop-types';
import formatBytes from 'pretty-bytes';
import formatDate from 'date-fns/format';
import parseDate from 'date-fns/parse';
const React = require('react'); import { continents, countries } from './countries.json';
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'); import Wrapper from './Wrapper';
const formatPercent = require('../utils/formatPercent'); import formatNumber from '../utils/formatNumber';
import formatPercent from '../utils/formatPercent';
const styles = {
tableFilter: {
fontSize: '0.8em',
textAlign: 'right'
},
countryName: {
paddingLeft: 20
}
};
function getCountriesByContinent(continent) { function getCountriesByContinent(continent) {
return Object.keys(countries).filter( return Object.keys(countries).filter(
@ -20,17 +30,17 @@ function sumKeyValues(hash, keys) {
return keys.reduce((n, key) => n + (hash[key] || 0), 0); return keys.reduce((n, key) => n + (hash[key] || 0), 0);
} }
function sumValues(hash) { // function sumValues(hash) {
return Object.keys(hash).reduce((memo, key) => memo + hash[key], 0); // return Object.keys(hash).reduce((memo, key) => memo + hash[key], 0);
} // }
class Stats extends React.Component { export default class Stats extends React.Component {
static propTypes = { static propTypes = {
data: PropTypes.object data: PropTypes.object
}; };
state = { state = {
minPackageRequests: 1000000, // minPackageRequests: 1000000,
minCountryRequests: 1000000 minCountryRequests: 1000000
}; };
@ -46,45 +56,45 @@ class Stats extends React.Component {
const until = parseDate(totals.until); const until = parseDate(totals.until);
// Packages // Packages
const packageRows = []; // const packageRows = [];
Object.keys(totals.requests.package) // Object.keys(totals.requests.package)
.sort((a, b) => { // .sort((a, b) => {
return totals.requests.package[b] - totals.requests.package[a]; // return totals.requests.package[b] - totals.requests.package[a];
}) // })
.forEach(packageName => { // .forEach(packageName => {
const requests = totals.requests.package[packageName]; // const requests = totals.requests.package[packageName];
const bandwidth = totals.bandwidth.package[packageName]; // const bandwidth = totals.bandwidth.package[packageName];
if (requests >= this.state.minPackageRequests) { // if (requests >= this.state.minPackageRequests) {
packageRows.push( // packageRows.push(
<tr key={packageName}> // <tr key={packageName}>
<td> // <td>
<a // <a
href={`https://npmjs.org/package/${packageName}`} // href={`https://npmjs.org/package/${packageName}`}
title={`${packageName} on npm`} // title={`${packageName} on npm`}
> // >
{packageName} // {packageName}
</a> // </a>
</td> // </td>
<td> // <td>
{formatNumber(requests)} ( // {formatNumber(requests)} (
{formatPercent(requests / totals.requests.all)} // {formatPercent(requests / totals.requests.all)}
%) // %)
</td> // </td>
{bandwidth ? ( // {bandwidth ? (
<td> // <td>
{formatBytes(bandwidth)} ( // {formatBytes(bandwidth)} (
{formatPercent(bandwidth / totals.bandwidth.all)} // {formatPercent(bandwidth / totals.bandwidth.all)}
%) // %)
</td> // </td>
) : ( // ) : (
<td>-</td> // <td>-</td>
)} // )}
</tr> // </tr>
); // );
} // }
}); // });
// Regions // Regions
const regionRows = []; const regionRows = [];
@ -114,7 +124,7 @@ class Stats extends React.Component {
continentData.bandwidth !== 0 continentData.bandwidth !== 0
) { ) {
regionRows.push( regionRows.push(
<tr key={continent} className="continent-row"> <tr key={continent}>
<td> <td>
<strong>{continentName}</strong> <strong>{continentName}</strong>
</td> </td>
@ -145,8 +155,8 @@ class Stats extends React.Component {
if (countryRequests > this.state.minCountryRequests) { if (countryRequests > this.state.minCountryRequests) {
regionRows.push( regionRows.push(
<tr key={continent + country} className="country-row"> <tr key={continent + country}>
<td className="country-name">{countries[country].name}</td> <td style={styles.countryName}>{countries[country].name}</td>
<td> <td>
{formatNumber(countryRequests)} ( {formatNumber(countryRequests)} (
{formatPercent(countryRequests / totals.requests.all)} {formatPercent(countryRequests / totals.requests.all)}
@ -165,27 +175,27 @@ class Stats extends React.Component {
}); });
// Protocols // Protocols
const protocolRows = Object.keys(totals.requests.protocol) // const protocolRows = Object.keys(totals.requests.protocol)
.sort((a, b) => { // .sort((a, b) => {
return totals.requests.protocol[b] - totals.requests.protocol[a]; // return totals.requests.protocol[b] - totals.requests.protocol[a];
}) // })
.map(protocol => { // .map(protocol => {
const requests = totals.requests.protocol[protocol]; // const requests = totals.requests.protocol[protocol];
return ( // return (
<tr key={protocol}> // <tr key={protocol}>
<td>{protocol}</td> // <td>{protocol}</td>
<td> // <td>
{formatNumber(requests)} ( // {formatNumber(requests)} (
{formatPercent(requests / sumValues(totals.requests.protocol))} // {formatPercent(requests / sumValues(totals.requests.protocol))}
%) // %)
</td> // </td>
</tr> // </tr>
); // );
}); // });
return ( return (
<div className="wrapper"> <Wrapper>
<p> <p>
From <strong>{formatDate(since, 'MMM D')}</strong> to{' '} From <strong>{formatDate(since, 'MMM D')}</strong> to{' '}
<strong>{formatDate(until, 'MMM D')}</strong> unpkg served{' '} <strong>{formatDate(until, 'MMM D')}</strong> unpkg served{' '}
@ -201,6 +211,12 @@ class Stats extends React.Component {
<h3>Packages</h3> <h3>Packages</h3>
<p>
We recently migrated unpkg to a new backend and are working on getting
package-specific data back on the site.
</p>
{/*
<p> <p>
The table below shows the most popular packages served by unpkg from{' '} The table below shows the most popular packages served by unpkg from{' '}
<strong>{formatDate(since, 'MMM D')}</strong> to{' '} <strong>{formatDate(since, 'MMM D')}</strong> to{' '}
@ -208,7 +224,7 @@ class Stats extends React.Component {
{Object.keys(totals.requests.package).length} packages are shown. {Object.keys(totals.requests.package).length} packages are shown.
</p> </p>
<p className="table-filter"> <p style={styles.tableFilter}>
Include only packages that received at least{' '} Include only packages that received at least{' '}
<select <select
value={this.state.minPackageRequests} value={this.state.minPackageRequests}
@ -244,6 +260,7 @@ class Stats extends React.Component {
</thead> </thead>
<tbody>{packageRows}</tbody> <tbody>{packageRows}</tbody>
</table> </table>
*/}
<h3>Regions</h3> <h3>Regions</h3>
@ -253,7 +270,7 @@ class Stats extends React.Component {
<strong>{formatDate(until, 'MMM D')}</strong> by geographic region. <strong>{formatDate(until, 'MMM D')}</strong> by geographic region.
</p> </p>
<p className="table-filter"> <p style={styles.tableFilter}>
Include only countries that made at least{' '} Include only countries that made at least{' '}
<select <select
value={this.state.minCountryRequests} value={this.state.minCountryRequests}
@ -272,12 +289,7 @@ class Stats extends React.Component {
requests. requests.
</p> </p>
<table <table cellSpacing="0" cellPadding="0" style={{ width: '100%' }}>
cellSpacing="0"
cellPadding="0"
style={{ width: '100%' }}
className="regions-table"
>
<thead> <thead>
<tr> <tr>
<th> <th>
@ -294,6 +306,7 @@ class Stats extends React.Component {
<tbody>{regionRows}</tbody> <tbody>{regionRows}</tbody>
</table> </table>
{/*
<h3>Protocols</h3> <h3>Protocols</h3>
<p> <p>
@ -315,9 +328,8 @@ class Stats extends React.Component {
</thead> </thead>
<tbody>{protocolRows}</tbody> <tbody>{protocolRows}</tbody>
</table> </table>
</div> */}
</Wrapper>
); );
} }
} }
module.exports = Stats;

View File

@ -1,22 +1,22 @@
const React = require('react'); import React from 'react';
const PropTypes = require('prop-types'); import PropTypes from 'prop-types';
const addEvent = require('../utils/addEvent'); import { addEvent, removeEvent } from '../utils/dom';
const removeEvent = require('../utils/removeEvent');
const resizeEvent = 'resize'; const resizeEvent = 'resize';
class WindowSize extends React.Component { export default class WindowSize extends React.Component {
static propTypes = { static propTypes = {
onChange: PropTypes.func onChange: PropTypes.func
}; };
handleWindowResize = () => { handleWindowResize = () => {
if (this.props.onChange) if (this.props.onChange) {
this.props.onChange({ this.props.onChange({
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight height: window.innerHeight
}); });
}
}; };
componentDidMount() { componentDidMount() {
@ -31,5 +31,3 @@ class WindowSize extends React.Component {
return null; return null;
} }
} }
module.exports = WindowSize;

View File

@ -0,0 +1,5 @@
import React from 'react';
export default function Wrapper({ children }) {
return <div style={{ maxWidth: 700, margin: '0 auto' }}>{children}</div>;
}

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -0,0 +1,15 @@
export function addEvent(node, type, handler) {
if (node.addEventListener) {
node.addEventListener(type, handler, false);
} else if (node.attachEvent) {
node.attachEvent('on' + type, handler);
}
}
export function removeEvent(node, type, handler) {
if (node.removeEventListener) {
node.removeEventListener(type, handler, false);
} else if (node.detachEvent) {
node.detachEvent('on' + type, handler);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

4
modules/config.js Normal file
View File

@ -0,0 +1,4 @@
export const npmRegistryURL =
process.env.NPM_REGISTRY_URL || 'https://registry.npmjs.org';
export const origin = process.env.ORIGIN || 'http://localhost:5000';

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,15 @@
import { https } from 'firebase-functions';
// import serveAuth from './serveAuth';
import serveAutoIndexPage from './serveAutoIndexPage';
import serveNpmPackageFile from './serveNpmPackageFile';
import servePublicKey from './servePublicKey';
import serveStats from './serveStats';
export default {
// serveAuth: https.onRequest(serveAuth),
serveAutoIndexPage: https.onRequest(serveAutoIndexPage),
serveNpmPackageFile: https.onRequest(serveNpmPackageFile),
servePublicKey: https.onRequest(servePublicKey),
serveStats: https.onRequest(serveStats)
};

View File

@ -1,14 +1,11 @@
const parseURL = require('url').parse; import url from 'url';
const startOfDay = require('date-fns/start_of_day'); import { startOfDay, addDays } from 'date-fns';
const addDays = require('date-fns/add_days');
const db = require('./utils/data'); import data from '../utils/data';
const isValidPackageName = require('./utils/isValidPackageName'); import isValidPackageName from '../utils/isValidPackageName';
const parsePackageURL = require('./utils/parsePackageURL'); import parsePackageURL from '../utils/parsePackageURL';
const logging = require('./utils/logging'); import * as cloudflare from '../utils/cloudflare';
import * as stats from '../utils/stats';
const CloudflareAPI = require('./CloudflareAPI');
const StatsAPI = require('./StatsAPI');
/** /**
* Domains we want to analyze. * Domains we want to analyze.
@ -54,13 +51,13 @@ function computeCounters(stream) {
const nextDay = startOfDay(addDays(date, 1)); const nextDay = startOfDay(addDays(date, 1));
const sevenDaysLater = getSeconds(addDays(nextDay, 7)); const sevenDaysLater = getSeconds(addDays(nextDay, 7));
const thirtyDaysLater = getSeconds(addDays(nextDay, 30)); const thirtyDaysLater = getSeconds(addDays(nextDay, 30));
const dayKey = StatsAPI.createDayKey(date); const dayKey = stats.createDayKey(date);
if (entry.EdgeResponseStatus === 200) { if (entry.EdgeResponseStatus === 200) {
// Q: How many requests do we serve for a package per day? // Q: How many requests do we serve for a package per day?
// Q: How many bytes do we serve for a package per day? // Q: How many bytes do we serve for a package per day?
const url = parsePackageURL(entry.ClientRequestURI); const parsed = parsePackageURL(entry.ClientRequestURI);
const packageName = url && url.packageName; const packageName = parsed && parsed.packageName;
if (packageName && isValidPackageName(packageName)) { if (packageName && isValidPackageName(packageName)) {
incr( incr(
@ -93,7 +90,7 @@ function computeCounters(stream) {
// Q: How many requests do we receive from a hostname per day? // Q: How many requests do we receive from a hostname per day?
// Q: How many bytes do we serve to a hostname per day? // Q: How many bytes do we serve to a hostname per day?
const referer = entry.ClientRequestReferer; const referer = entry.ClientRequestReferer;
const hostname = referer && parseURL(referer).hostname; const hostname = referer && url.parse(referer).hostname;
if (hostname) { if (hostname) {
incr(`stats-hostnameRequests-${dayKey}`, hostname, 1, sevenDaysLater); incr(`stats-hostnameRequests-${dayKey}`, hostname, 1, sevenDaysLater);
@ -118,11 +115,11 @@ function processLogs(stream) {
const values = counters[key]; const values = counters[key];
Object.keys(values).forEach(member => { Object.keys(values).forEach(member => {
db.zincrby(key, values[member], member); data.zincrby(key, values[member], member);
}); });
if (expireat[key]) { if (expireat[key]) {
db.expireat(key, expireat[key]); data.expireat(key, expireat[key]);
} }
}); });
@ -145,44 +142,46 @@ function ingestLogsForZone(zone, startDate, endDate) {
'ClientRequestReferer' 'ClientRequestReferer'
]; ];
return CloudflareAPI.getLogs( return cloudflare
zone.id, .getLogs(
stringifySeconds(startSeconds), zone.id,
stringifySeconds(endSeconds),
fields
).then(stream => {
const endFetchTime = Date.now();
logging.info(
'Fetched logs for %s from %s to %s (%dms)',
zone.name,
stringifySeconds(startSeconds), stringifySeconds(startSeconds),
stringifySeconds(endSeconds), stringifySeconds(endSeconds),
endFetchTime - startFetchTime fields
); )
.then(stream => {
const endFetchTime = Date.now();
const startProcessTime = Date.now(); console.log(
'Fetched logs for %s from %s to %s (%dms)',
return processLogs(stream).then(totalEntries => {
const endProcessTime = Date.now();
logging.info(
'Processed %d log entries for %s (%dms)',
totalEntries,
zone.name, zone.name,
endProcessTime - startProcessTime stringifySeconds(startSeconds),
stringifySeconds(endSeconds),
endFetchTime - startFetchTime
); );
const startProcessTime = Date.now();
return processLogs(stream).then(totalEntries => {
const endProcessTime = Date.now();
console.log(
'Processed %d log entries for %s (%dms)',
totalEntries,
zone.name,
endProcessTime - startProcessTime
);
});
}); });
});
} }
function getZones(domainNames) { function getZones(domainNames) {
return Promise.all(domainNames.map(CloudflareAPI.getZones)).then(results => return Promise.all(domainNames.map(cloudflare.getZones)).then(results =>
results.reduce((memo, zones) => memo.concat(zones)) results.reduce((memo, zones) => memo.concat(zones))
); );
} }
function ingestLogs(startDate, endDate) { export default function ingestLogs(startDate, endDate) {
return Promise.resolve(cachedZones || getZones(domainNames)).then(zones => { return Promise.resolve(cachedZones || getZones(domainNames)).then(zones => {
if (!cachedZones) cachedZones = zones; if (!cachedZones) cachedZones = zones;
@ -191,5 +190,3 @@ function ingestLogs(startDate, endDate) {
); );
}); });
} }
module.exports = ingestLogs;

View File

@ -0,0 +1,15 @@
import express from 'express';
import cors from 'cors';
import userToken from '../middleware/userToken';
import showAuth from '../actions/showAuth';
const app = express();
app.disable('x-powered-by');
app.use(cors());
app.use(userToken);
app.use(showAuth);
export default app;

View File

@ -0,0 +1,27 @@
import express from 'express';
import cors from 'cors';
// import checkBlacklist from '../middleware/checkBlacklist';
import fetchPackage from '../middleware/fetchPackage';
import findFile from '../middleware/findFile';
import redirectLegacyURLs from '../middleware/redirectLegacyURLs';
import validatePackageURL from '../middleware/validatePackageURL';
import validatePackageName from '../middleware/validatePackageName';
import validateQuery from '../middleware/validateQuery';
import serveAutoIndexPage from '../actions/serveAutoIndexPage';
const app = express();
app.disable('x-powered-by');
app.use(cors());
app.use(redirectLegacyURLs);
app.use(validatePackageURL);
app.use(validatePackageName);
app.use(validateQuery);
// app.use(checkBlacklist);
app.use(fetchPackage);
app.use(findFile);
app.use(serveAutoIndexPage);
export default app;

View File

@ -0,0 +1,27 @@
import express from 'express';
import cors from 'cors';
// import checkBlacklist from '../middleware/checkBlacklist';
import fetchPackage from '../middleware/fetchPackage';
import findFile from '../middleware/findFile';
import redirectLegacyURLs from '../middleware/redirectLegacyURLs';
import validatePackageURL from '../middleware/validatePackageURL';
import validatePackageName from '../middleware/validatePackageName';
import validateQuery from '../middleware/validateQuery';
import serveFile from '../actions/serveFile';
const app = express();
app.disable('x-powered-by');
app.use(cors());
app.use(redirectLegacyURLs);
app.use(validatePackageURL);
app.use(validatePackageName);
app.use(validateQuery);
// app.use(checkBlacklist);
app.use(fetchPackage);
app.use(findFile);
app.use(serveFile);
export default app;

View File

@ -0,0 +1,13 @@
import express from 'express';
import cors from 'cors';
import showPublicKey from '../actions/showPublicKey';
const app = express();
app.disable('x-powered-by');
app.use(cors());
app.use(showPublicKey);
export default app;

View File

@ -0,0 +1,13 @@
import express from 'express';
import cors from 'cors';
import showStats from '../actions/showStats';
const app = express();
app.disable('x-powered-by');
app.use(cors());
app.use(showStats);
export default app;

View File

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

View File

@ -1,7 +1,7 @@
const BlacklistAPI = require('../BlacklistAPI'); import { includesPackage } from '../utils/blacklist';
function checkBlacklist(req, res, next) { export default function checkBlacklist(req, res, next) {
BlacklistAPI.includesPackage(req.packageName).then( includesPackage(req.packageName).then(
blacklisted => { blacklisted => {
// Disallow packages that have been blacklisted. // Disallow packages that have been blacklisted.
if (blacklisted) { if (blacklisted) {
@ -21,5 +21,3 @@ function checkBlacklist(req, res, next) {
} }
); );
} }
module.exports = checkBlacklist;

View File

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

View File

@ -1,10 +1,10 @@
const semver = require('semver'); import semver from 'semver';
const addLeadingSlash = require('../utils/addLeadingSlash'); import addLeadingSlash from '../utils/addLeadingSlash';
const createPackageURL = require('../utils/createPackageURL'); import createPackageURL from '../utils/createPackageURL';
const createSearch = require('../utils/createSearch'); import createSearch from '../utils/createSearch';
const getNpmPackageInfo = require('../utils/getNpmPackageInfo'); import getNpmPackageInfo from '../utils/getNpmPackageInfo';
const incrementCounter = require('../utils/incrementCounter'); // import incrementCounter from '../utils/incrementCounter';
function tagRedirect(req, res) { function tagRedirect(req, res) {
const version = req.packageInfo['dist-tags'][req.packageVersion]; const version = req.packageInfo['dist-tags'][req.packageVersion];
@ -64,11 +64,11 @@ function filenameRedirect(req, res) {
// Count which packages are using this so we can warn them when we // Count which packages are using this so we can warn them when we
// remove this functionality. // remove this functionality.
incrementCounter( // incrementCounter(
'package-json-custom-main', // 'package-json-custom-main',
req.packageSpec + '?main=' + req.query.main, // req.packageSpec + '?main=' + req.query.main,
1 // 1
); // );
} else if ( } else if (
req.packageConfig.unpkg && req.packageConfig.unpkg &&
typeof req.packageConfig.unpkg === 'string' typeof req.packageConfig.unpkg === 'string'
@ -83,7 +83,7 @@ function filenameRedirect(req, res) {
// Count which packages are using this so we can warn them when we // Count which packages are using this so we can warn them when we
// remove this functionality. // remove this functionality.
incrementCounter('package-json-browser-fallback', req.packageSpec, 1); // incrementCounter('package-json-browser-fallback', req.packageSpec, 1);
} else { } else {
filename = req.packageConfig.main || '/index.js'; filename = req.packageConfig.main || '/index.js';
} }
@ -111,7 +111,7 @@ function filenameRedirect(req, res) {
* version if the request targets a tag or uses a semver version, or to the * version if the request targets a tag or uses a semver version, or to the
* exact filename if the request omits the filename. * exact filename if the request omits the filename.
*/ */
function fetchPackage(req, res, next) { export default function fetchPackage(req, res, next) {
getNpmPackageInfo(req.packageName).then( getNpmPackageInfo(req.packageName).then(
packageInfo => { packageInfo => {
if (packageInfo == null || packageInfo.versions == null) { if (packageInfo == null || packageInfo.versions == null) {
@ -149,5 +149,3 @@ function fetchPackage(req, res, next) {
} }
); );
} }
module.exports = fetchPackage;

View File

@ -1,11 +1,11 @@
const path = require('path'); import path from 'path';
const addLeadingSlash = require('../utils/addLeadingSlash'); import addLeadingSlash from '../utils/addLeadingSlash';
const createPackageURL = require('../utils/createPackageURL'); import createPackageURL from '../utils/createPackageURL';
const createSearch = require('../utils/createSearch'); import createSearch from '../utils/createSearch';
const fetchNpmPackage = require('../utils/fetchNpmPackage'); import fetchNpmPackage from '../utils/fetchNpmPackage';
const getIntegrity = require('../utils/getIntegrity'); import getIntegrity from '../utils/getIntegrity';
const getContentType = require('../utils/getContentType'); import getContentType from '../utils/getContentType';
function indexRedirect(req, res, entry) { function indexRedirect(req, res, entry) {
// Redirect to the index file so relative imports // Redirect to the index file so relative imports
@ -124,7 +124,7 @@ const multipleSlash = /\/\/+/;
* Fetch and search the archive to try and find the requested file. * Fetch and search the archive to try and find the requested file.
* Redirect to the "index" file if a directory was requested. * 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 => { fetchNpmPackage(req.packageConfig).then(tarballStream => {
const entryName = req.filename const entryName = req.filename
.replace(multipleSlash, '/') .replace(multipleSlash, '/')
@ -173,5 +173,3 @@ function findFile(req, res, next) {
); );
}); });
} }
module.exports = findFile;

View File

@ -1,9 +1,9 @@
const createSearch = require('../utils/createSearch'); import createSearch from '../utils/createSearch';
/** /**
* Redirect old URLs that we no longer support. * Redirect old URLs that we no longer support.
*/ */
function redirectLegacyURLs(req, res, next) { export default function redirectLegacyURLs(req, res, next) {
// Permanently redirect /_meta/path to /path?meta. // Permanently redirect /_meta/path to /path?meta.
if (req.path.match(/^\/_meta\//)) { if (req.path.match(/^\/_meta\//)) {
req.query.meta = ''; req.query.meta = '';
@ -19,5 +19,3 @@ function redirectLegacyURLs(req, res, next) {
next(); next();
} }
module.exports = redirectLegacyURLs;

View File

@ -2,7 +2,7 @@
* Adds the given scope to the array in req.auth if the user has sufficient * Adds the given scope to the array in req.auth if the user has sufficient
* permissions. Otherwise rejects the request. * permissions. Otherwise rejects the request.
*/ */
function requireAuth(scope) { export default function requireAuth(scope) {
let checkScopes; let checkScopes;
if (scope.includes('.')) { if (scope.includes('.')) {
const parts = scope.split('.'); const parts = scope.split('.');
@ -36,5 +36,3 @@ function requireAuth(scope) {
next(); next();
}; };
} }
module.exports = requireAuth;

View File

@ -1,31 +0,0 @@
const fs = require('fs');
const invariant = require('invariant');
const createAssets = require('./utils/createAssets');
/**
* An express middleware that sets req.assets from the build
* info in the given stats file. Should be used in production.
*/
function staticAssets(webpackStatsFile) {
let stats;
try {
stats = JSON.parse(fs.readFileSync(webpackStatsFile, 'utf8'));
} catch (error) {
invariant(
false,
'staticAssets middleware cannot read the build stats in %s; ' +
'run the `build` script before starting the server',
webpackStatsFile
);
}
const assets = createAssets(stats);
return (req, res, next) => {
req.assets = assets;
next();
};
}
module.exports = staticAssets;

View File

@ -1,6 +1,4 @@
const AuthAPI = require('../AuthAPI'); import { verifyToken } from '../utils/auth';
const ReadMethods = { GET: true, HEAD: true };
function decodeBase64(string) { function decodeBase64(string) {
return Buffer.from(string, 'base64').toString(); return Buffer.from(string, 'base64').toString();
@ -9,22 +7,20 @@ function decodeBase64(string) {
/** /**
* Sets req.user from the payload in the auth token in the request. * Sets req.user from the payload in the auth token in the request.
*/ */
function userToken(req, res, next) { export default function userToken(req, res, next) {
if (req.user) { if (req.user !== undefined) {
return next(); return next();
} }
const auth = req.get('Authorization'); const auth = req.get('Authorization');
const token = auth const token = auth && decodeBase64(auth);
? decodeBase64(auth)
: (ReadMethods[req.method] ? req.query : req.body).token;
if (!token) { if (!token) {
req.user = null; req.user = null;
return next(); return next();
} }
AuthAPI.verifyToken(token).then( verifyToken(token).then(
payload => { payload => {
req.user = payload; req.user = payload;
next(); next();
@ -44,5 +40,3 @@ function userToken(req, res, next) {
} }
); );
} }
module.exports = userToken;

View File

@ -1,40 +0,0 @@
/**
* Creates an assets object that is stored on req.assets.
*/
function createAssets(webpackStats) {
const { publicPath, assetsByChunkName } = webpackStats;
/**
* Returns a public URL to the given asset.
*/
const createURL = asset => publicPath + asset;
/**
* Returns an array of URLs to all assets in the given chunks.
*/
const getAll = (chunks = ['main']) =>
(Array.isArray(chunks) ? chunks : [chunks])
.reduce((memo, chunk) => memo.concat(assetsByChunkName[chunk] || []), [])
.map(createURL);
/**
* Returns an array of URLs to all JavaScript files in the given chunks.
*/
const getScripts = (...chunks) =>
getAll(...chunks).filter(asset => /\.js$/.test(asset));
/**
* Returns an array of URLs to all CSS files in the given chunks.
*/
const getStyles = (...chunks) =>
getAll(...chunks).filter(asset => /\.css$/.test(asset));
return {
createURL,
getAll,
getScripts,
getStyles
};
}
module.exports = createAssets;

View File

@ -1,4 +1,4 @@
const validateNpmPackageName = require('validate-npm-package-name'); import validateNpmPackageName from 'validate-npm-package-name';
const hexValue = /^[a-f0-9]+$/i; const hexValue = /^[a-f0-9]+$/i;
@ -9,7 +9,7 @@ function isHash(value) {
/** /**
* Reject requests for invalid npm package names. * Reject requests for invalid npm package names.
*/ */
function validatePackageName(req, res, next) { export default function validatePackageName(req, res, next) {
if (isHash(req.packageName)) { if (isHash(req.packageName)) {
return res return res
.status(403) .status(403)
@ -30,5 +30,3 @@ function validatePackageName(req, res, next) {
next(); next();
} }
module.exports = validatePackageName;

View File

@ -1,10 +1,10 @@
const parsePackageURL = require('../utils/parsePackageURL'); import parsePackageURL from '../utils/parsePackageURL';
/** /**
* Parse the URL and add various properties to the request object to * Parse the URL and add various properties to the request object to
* do with the package/file being requested. Reject invalid URLs. * do with the package/file being requested. Reject invalid URLs.
*/ */
function validatePackageURL(req, res, next) { export default function validatePackageURL(req, res, next) {
const url = parsePackageURL(req.url); const url = parsePackageURL(req.url);
if (url == null) { if (url == null) {
@ -21,5 +21,3 @@ function validatePackageURL(req, res, next) {
next(); next();
} }
module.exports = validatePackageURL;

View File

@ -1,4 +1,4 @@
const createSearch = require('../utils/createSearch'); import createSearch from '../utils/createSearch';
const knownQueryParams = { const knownQueryParams = {
main: true, // Deprecated, see #63 main: true, // Deprecated, see #63
@ -23,12 +23,10 @@ function sanitizeQuery(originalQuery) {
/** /**
* Reject URLs with invalid query parameters to increase cache hit rates. * Reject URLs with invalid query parameters to increase cache hit rates.
*/ */
function validateQuery(req, res, next) { export default function validateQuery(req, res, next) {
if (!Object.keys(req.query).every(isKnownQueryParam)) { if (!Object.keys(req.query).every(isKnownQueryParam)) {
return res.redirect(302, req.path + createSearch(sanitizeQuery(req.query))); return res.redirect(302, req.path + createSearch(sanitizeQuery(req.query)));
} }
next(); next();
} }
module.exports = validateQuery;

View File

@ -1,5 +1,6 @@
const babel = require('babel-core'); import babel from 'babel-core';
const unpkgRewrite = require('../unpkgRewrite');
import unpkgRewrite from '../unpkgRewrite';
const testCases = [ const testCases = [
{ {

View File

@ -1,7 +1,7 @@
const URL = require('whatwg-url'); import URL from 'whatwg-url';
const warning = require('warning'); import warning from 'warning';
const origin = require('../serverConfig').origin; import { origin } from '../config';
const bareIdentifierFormat = /^((?:@[^/]+\/)?[^/]+)(\/.*)?$/; const bareIdentifierFormat = /^((?:@[^/]+\/)?[^/]+)(\/.*)?$/;
@ -47,7 +47,7 @@ function rewriteValue(/* StringLiteral */ node, dependencies) {
} }
} }
function unpkgRewrite(dependencies = {}) { export default function unpkgRewrite(dependencies = {}) {
return { return {
manipulateOptions(opts, parserOpts) { manipulateOptions(opts, parserOpts) {
parserOpts.plugins.push( parserOpts.plugins.push(
@ -88,5 +88,3 @@ function unpkgRewrite(dependencies = {}) {
} }
}; };
} }
module.exports = unpkgRewrite;

View File

@ -1,7 +1,7 @@
const fs = require('fs'); import fs from 'fs';
const path = require('path'); import path from 'path';
const forge = require('node-forge'); import forge from 'node-forge';
const invariant = require('invariant'); import invariant from 'invariant';
let secretKey; let secretKey;
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
@ -28,4 +28,4 @@ if (process.env.NODE_ENV === 'production') {
}; };
} }
module.exports = secretKey; export default secretKey;

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