diff --git a/jest.config.js b/jest.config.js index cdafc241..7a8f4c72 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,9 +1,9 @@ module.exports = { moduleNameMapper: { - "\\.css$": "/modules/__mocks__/styleMock.js" + 'entry-manifest': '/modules/__mocks__/entryManifest.js', + '\\.png$': '/modules/__mocks__/imageMock.js', + '\\.css$': '/modules/__mocks__/styleMock.js' }, - setupTestFrameworkScriptFile: - "/modules/__tests__/setupTestFramework.js", - testMatch: ["**/__tests__/*-test.js"], - testURL: "http://localhost/" + testMatch: ['**/__tests__/*-test.js'], + testURL: 'http://localhost/' }; diff --git a/modules/.babelrc b/modules/.babelrc new file mode 100644 index 00000000..4df14a99 --- /dev/null +++ b/modules/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": [["@babel/preset-env", { "loose": true, "targets": "node 8" }]] +} diff --git a/modules/__mocks__/entryManifest.js b/modules/__mocks__/entryManifest.js new file mode 100644 index 00000000..d6d1738d --- /dev/null +++ b/modules/__mocks__/entryManifest.js @@ -0,0 +1 @@ +export default []; diff --git a/modules/__mocks__/imageMock.js b/modules/__mocks__/imageMock.js new file mode 100644 index 00000000..08d725cd --- /dev/null +++ b/modules/__mocks__/imageMock.js @@ -0,0 +1 @@ +export default ''; diff --git a/modules/__tests__/invalidPackageNames-test.js b/modules/__tests__/invalidPackageNames-test.js new file mode 100644 index 00000000..3e8a8bbf --- /dev/null +++ b/modules/__tests__/invalidPackageNames-test.js @@ -0,0 +1,19 @@ +import request from 'supertest'; + +import createServer from '../createServer'; + +describe('Invalid package names', () => { + let server; + beforeEach(() => { + server = createServer(); + }); + + it('are rejected', done => { + request(server) + .get('/_invalid/index.js') + .end((err, res) => { + expect(res.statusCode).toBe(403); + done(); + }); + }); +}); diff --git a/modules/__tests__/invalidQueryParams-test.js b/modules/__tests__/invalidQueryParams-test.js new file mode 100644 index 00000000..a454792f --- /dev/null +++ b/modules/__tests__/invalidQueryParams-test.js @@ -0,0 +1,20 @@ +import request from 'supertest'; + +import createServer from '../createServer'; + +describe('Invalid query params', () => { + let server; + beforeEach(() => { + server = createServer(); + }); + + it('redirect to the same path w/out those params', done => { + request(server) + .get('/d3?module&invalid-param') + .end((err, res) => { + expect(res.statusCode).toBe(302); + expect(res.headers.location).toBe('/d3?module'); + done(); + }); + }); +}); diff --git a/modules/__tests__/legacyURLs-test.js b/modules/__tests__/legacyURLs-test.js new file mode 100644 index 00000000..90658fd5 --- /dev/null +++ b/modules/__tests__/legacyURLs-test.js @@ -0,0 +1,30 @@ +import request from 'supertest'; + +import createServer from '../createServer'; + +describe('Legacy URLs', () => { + let server; + beforeEach(() => { + server = createServer(); + }); + + it('redirect /_meta to ?meta', done => { + request(server) + .get('/_meta/react') + .end((err, res) => { + expect(res.statusCode).toBe(301); + expect(res.headers.location).toBe('/react?meta'); + done(); + }); + }); + + it('redirect ?json to ?meta', done => { + request(server) + .get('/react?json') + .end((err, res) => { + expect(res.statusCode).toBe(301); + expect(res.headers.location).toBe('/react?meta'); + done(); + }); + }); +}); diff --git a/modules/__tests__/server-test.js b/modules/__tests__/server-test.js deleted file mode 100644 index 0b4a8520..00000000 --- a/modules/__tests__/server-test.js +++ /dev/null @@ -1,49 +0,0 @@ -import request from 'supertest'; - -import createServer from '../createServer'; - -describe('The server', () => { - let server; - beforeEach(() => { - server = createServer(); - }); - - it('redirects /_meta to ?meta', done => { - request(server) - .get('/_meta/react') - .end((err, res) => { - expect(res.statusCode).toBe(301); - expect(res.headers.location).toBe('/react?meta'); - done(); - }); - }); - - it('redirects ?json to ?meta', done => { - request(server) - .get('/react?json') - .end((err, res) => { - expect(res.statusCode).toBe(301); - expect(res.headers.location).toBe('/react?meta'); - done(); - }); - }); - - it('redirects invalid query params', done => { - request(server) - .get('/react?main=index&invalid') - .end((err, res) => { - expect(res.statusCode).toBe(302); - expect(res.headers.location).toBe('/react?main=index'); - done(); - }); - }); - - it('rejects invalid package names', done => { - request(server) - .get('/_invalid/index.js') - .end((err, res) => { - expect(res.statusCode).toBe(403); - done(); - }); - }); -}); diff --git a/modules/actions/serveAutoIndexPage.js b/modules/actions/serveAutoIndexPage.js index 83cf8736..1686d582 100644 --- a/modules/actions/serveAutoIndexPage.js +++ b/modules/actions/serveAutoIndexPage.js @@ -1,14 +1,14 @@ import { renderToString, renderToStaticMarkup } from 'react-dom/server'; import semver from 'semver'; -import AutoIndexApp from '../client/autoIndex/App'; +import AutoIndexApp from '../client/autoIndex/App.js'; -import createElement from './utils/createElement'; -import createHTML from './utils/createHTML'; -import createScript from './utils/createScript'; -import getEntryPoint from './utils/getEntryPoint'; -import getGlobalScripts from './utils/getGlobalScripts'; -import MainTemplate from './utils/MainTemplate'; +import MainTemplate from './utils/MainTemplate.js'; +import createElement from './utils/createElement.js'; +import createHTML from './utils/createHTML.js'; +import createScript from './utils/createScript.js'; +import getEntryPoint from './utils/getEntryPoint.js'; +import getGlobalScripts from './utils/getGlobalScripts.js'; const doctype = ''; const globalURLs = diff --git a/modules/actions/serveFile.js b/modules/actions/serveFile.js index 838a793e..3d0c52c4 100644 --- a/modules/actions/serveFile.js +++ b/modules/actions/serveFile.js @@ -1,7 +1,7 @@ -import serveAutoIndexPage from './serveAutoIndexPage'; -import serveMetadata from './serveMetadata'; -import serveModule from './serveModule'; -import serveStaticFile from './serveStaticFile'; +import serveAutoIndexPage from './serveAutoIndexPage.js'; +import serveMetadata from './serveMetadata.js'; +import serveModule from './serveModule.js'; +import serveStaticFile from './serveStaticFile.js'; /** * Send the file, JSON metadata, or HTML directory listing. diff --git a/modules/actions/serveHTMLModule.js b/modules/actions/serveHTMLModule.js index 0b13d3d2..f6d1054b 100644 --- a/modules/actions/serveHTMLModule.js +++ b/modules/actions/serveHTMLModule.js @@ -1,8 +1,8 @@ import etag from 'etag'; import cheerio from 'cheerio'; -import getContentTypeHeader from '../utils/getContentTypeHeader'; -import rewriteBareModuleIdentifiers from '../utils/rewriteBareModuleIdentifiers'; +import getContentTypeHeader from '../utils/getContentTypeHeader.js'; +import rewriteBareModuleIdentifiers from '../utils/rewriteBareModuleIdentifiers.js'; export default function serveHTMLModule(req, res) { try { @@ -40,9 +40,7 @@ export default function serveHTMLModule(req, res) { .status(500) .type('text') .send( - `Cannot generate module for ${req.packageSpec}${ - req.filename - }\n\n${debugInfo}` + `Cannot generate module for ${req.packageSpec}${req.filename}\n\n${debugInfo}` ); } } diff --git a/modules/actions/serveJavaScriptModule.js b/modules/actions/serveJavaScriptModule.js index 5b7621ab..97c35517 100644 --- a/modules/actions/serveJavaScriptModule.js +++ b/modules/actions/serveJavaScriptModule.js @@ -1,7 +1,7 @@ import etag from 'etag'; -import getContentTypeHeader from '../utils/getContentTypeHeader'; -import rewriteBareModuleIdentifiers from '../utils/rewriteBareModuleIdentifiers'; +import getContentTypeHeader from '../utils/getContentTypeHeader.js'; +import rewriteBareModuleIdentifiers from '../utils/rewriteBareModuleIdentifiers.js'; export default function serveJavaScriptModule(req, res) { try { @@ -34,9 +34,7 @@ export default function serveJavaScriptModule(req, res) { .status(500) .type('text') .send( - `Cannot generate module for ${req.packageSpec}${ - req.filename - }\n\n${debugInfo}` + `Cannot generate module for ${req.packageSpec}${req.filename}\n\n${debugInfo}` ); } } diff --git a/modules/actions/serveMainPage.js b/modules/actions/serveMainPage.js index 6e3bfc49..85aadaa0 100644 --- a/modules/actions/serveMainPage.js +++ b/modules/actions/serveMainPage.js @@ -1,13 +1,13 @@ import { renderToString, renderToStaticMarkup } from 'react-dom/server'; -import MainApp from '../client/main/App'; +import MainApp from '../client/main/App.js'; -import createElement from './utils/createElement'; -import createHTML from './utils/createHTML'; -import createScript from './utils/createScript'; -import getEntryPoint from './utils/getEntryPoint'; -import getGlobalScripts from './utils/getGlobalScripts'; -import MainTemplate from './utils/MainTemplate'; +import MainTemplate from './utils/MainTemplate.js'; +import createElement from './utils/createElement.js'; +import createHTML from './utils/createHTML.js'; +import createScript from './utils/createScript.js'; +import getEntryPoint from './utils/getEntryPoint.js'; +import getGlobalScripts from './utils/getGlobalScripts.js'; const doctype = ''; const globalURLs = diff --git a/modules/actions/serveMetadata.js b/modules/actions/serveMetadata.js index 9ed3f8d7..10436712 100644 --- a/modules/actions/serveMetadata.js +++ b/modules/actions/serveMetadata.js @@ -1,6 +1,6 @@ import path from 'path'; -import addLeadingSlash from '../utils/addLeadingSlash'; +import addLeadingSlash from '../utils/addLeadingSlash.js'; function getMatchingEntries(entry, entries) { const dirname = entry.name || '.'; diff --git a/modules/actions/serveModule.js b/modules/actions/serveModule.js index fb408a70..10c9039a 100644 --- a/modules/actions/serveModule.js +++ b/modules/actions/serveModule.js @@ -1,5 +1,5 @@ -import serveHTMLModule from './serveHTMLModule'; -import serveJavaScriptModule from './serveJavaScriptModule'; +import serveHTMLModule from './serveHTMLModule.js'; +import serveJavaScriptModule from './serveJavaScriptModule.js'; export default function serveModule(req, res) { if (req.entry.contentType === 'application/javascript') { diff --git a/modules/actions/serveStaticFile.js b/modules/actions/serveStaticFile.js index 5d6b1d6b..a7ddcdc0 100644 --- a/modules/actions/serveStaticFile.js +++ b/modules/actions/serveStaticFile.js @@ -1,7 +1,7 @@ import path from 'path'; import etag from 'etag'; -import getContentTypeHeader from '../utils/getContentTypeHeader'; +import getContentTypeHeader from '../utils/getContentTypeHeader.js'; export default function serveStaticFile(req, res) { const tags = ['file']; diff --git a/modules/actions/utils/MainTemplate.js b/modules/actions/utils/MainTemplate.js index 663c767e..7df4d628 100644 --- a/modules/actions/utils/MainTemplate.js +++ b/modules/actions/utils/MainTemplate.js @@ -1,8 +1,8 @@ import PropTypes from 'prop-types'; -import e from './createElement'; -import h from './createHTML'; -import x from './createScript'; +import e from './createElement.js'; +import h from './createHTML.js'; +import x from './createScript.js'; const promiseShim = 'window.Promise || document.write(\'\\x3Cscript src="/es6-promise@4.2.5/dist/es6-promise.min.js">\\x3C/script>\\x3Cscript>ES6Promise.polyfill()\\x3C/script>\')'; @@ -11,12 +11,12 @@ const fetchShim = 'window.fetch || document.write(\'\\x3Cscript src="/whatwg-fetch@3.0.0/dist/fetch.umd.js">\\x3C/script>\')'; export default function MainTemplate({ - title, - description, - favicon, + title = 'UNPKG', + description = 'The CDN for everything on npm', + favicon = '/favicon.ico', data, - content, - elements + content = h(''), + elements = [] }) { return e( 'html', @@ -47,14 +47,6 @@ export default function MainTemplate({ ); } -MainTemplate.defaultProps = { - title: 'UNPKG', - description: 'The CDN for everything on npm', - favicon: '/favicon.ico', - content: h(''), - elements: [] -}; - if (process.env.NODE_ENV !== 'production') { const htmlType = PropTypes.shape({ __html: PropTypes.string diff --git a/modules/actions/utils/cloudflare.js b/modules/actions/utils/cloudflare.js index 1854de32..c3ff2c28 100644 --- a/modules/actions/utils/cloudflare.js +++ b/modules/actions/utils/cloudflare.js @@ -1,16 +1,18 @@ import fetch from 'isomorphic-fetch'; -import invariant from 'invariant'; const cloudflareURL = 'https://api.cloudflare.com/client/v4'; const cloudflareEmail = process.env.CLOUDFLARE_EMAIL; const cloudflareKey = process.env.CLOUDFLARE_KEY; -invariant( - cloudflareEmail, - 'Missing the $CLOUDFLARE_EMAIL environment variable' -); +if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { + if (!cloudflareEmail) { + throw new Error('Missing the $CLOUDFLARE_EMAIL environment variable'); + } -invariant(cloudflareKey, 'Missing the $CLOUDFLARE_KEY environment variable'); + if (!cloudflareKey) { + throw new Error('Missing the $CLOUDFLARE_KEY environment variable'); + } +} function get(path, headers) { return fetch(`${cloudflareURL}${path}`, { diff --git a/modules/actions/utils/createScript.js b/modules/actions/utils/createScript.js index 6bd3ab9d..67bd1c77 100644 --- a/modules/actions/utils/createScript.js +++ b/modules/actions/utils/createScript.js @@ -1,5 +1,5 @@ -import createElement from './createElement'; -import createHTML from './createHTML'; +import createElement from './createElement.js'; +import createHTML from './createHTML.js'; export default function createScript(script) { return createElement('script', { diff --git a/modules/actions/utils/getEntryPoint.js b/modules/actions/utils/getEntryPoint.js index c4502844..ed5fe3f1 100644 --- a/modules/actions/utils/getEntryPoint.js +++ b/modules/actions/utils/getEntryPoint.js @@ -1,4 +1,5 @@ // Virtual module id; see rollup.config.js +// eslint-disable-next-line import/no-unresolved import entryManifest from 'entry-manifest'; export default function getEntryPoint(name, format) { diff --git a/modules/actions/utils/getGlobalScripts.js b/modules/actions/utils/getGlobalScripts.js index cdb3fa0b..9bfe5402 100644 --- a/modules/actions/utils/getGlobalScripts.js +++ b/modules/actions/utils/getGlobalScripts.js @@ -1,10 +1,13 @@ -import invariant from 'invariant'; - -import createElement from './createElement'; +import createElement from './createElement.js'; export default function getGlobalScripts(entryPoint, globalURLs) { return entryPoint.globalImports.map(id => { - invariant(globalURLs[id], 'Missing global URL for id "%s"', id); + if (process.env.NODE_ENV !== 'production') { + if (!globalURLs[id]) { + throw new Error('Missing global URL for id "%s"', id); + } + } + return createElement('script', { src: globalURLs[id] }); }); } diff --git a/modules/actions/utils/getStats.js b/modules/actions/utils/getStats.js index 8fc82c56..6505f0e2 100644 --- a/modules/actions/utils/getStats.js +++ b/modules/actions/utils/getStats.js @@ -1,4 +1,4 @@ -import * as cloudflare from './cloudflare.js'; +import { getZones, getZoneAnalyticsDashboard } from './cloudflare.js'; function extractPublicInfo(data) { return { @@ -28,12 +28,8 @@ function extractPublicInfo(data) { const DomainNames = ['unpkg.com', 'npmcdn.com']; export default async function getStats(since, until) { - const zones = await cloudflare.getZones(DomainNames); - const dashboard = await cloudflare.getZoneAnalyticsDashboard( - zones, - since, - until - ); + const zones = await getZones(DomainNames); + const dashboard = await getZoneAnalyticsDashboard(zones, since, until); return { timeseries: dashboard.timeseries.map(extractPublicInfo), diff --git a/modules/client/.babelrc b/modules/client/.babelrc index 4c18fb3e..6b6125ce 100644 --- a/modules/client/.babelrc +++ b/modules/client/.babelrc @@ -1,9 +1,7 @@ { "presets": [ - ["@babel/preset-env", { "loose": true }], + ["@babel/preset-env", { "loose": true, "targets": "> 0.25%, not dead" }], "@babel/preset-react" ], - "plugins": [ - ["@babel/plugin-proposal-class-properties", { "loose": true }] - ] + "plugins": [["@babel/plugin-proposal-class-properties", { "loose": true }]] } diff --git a/modules/client/.eslintrc b/modules/client/.eslintrc index d17408da..b1e8fbe0 100644 --- a/modules/client/.eslintrc +++ b/modules/client/.eslintrc @@ -1,4 +1,7 @@ { + "env": { + "browser": true + }, "plugins": [ "react" ], @@ -10,8 +13,5 @@ "react": { "version": "16" } - }, - "env": { - "browser": true } } diff --git a/modules/createServer.js b/modules/createServer.js new file mode 100644 index 00000000..4080e89f --- /dev/null +++ b/modules/createServer.js @@ -0,0 +1,49 @@ +import express from 'express'; + +import serveFile from './actions/serveFile.js'; +import serveMainPage from './actions/serveMainPage.js'; +import serveStats from './actions/serveStats.js'; + +import cors from './middleware/cors.js'; +import fetchPackage from './middleware/fetchPackage.js'; +import findFile from './middleware/findFile.js'; +import logger from './middleware/logger.js'; +import redirectLegacyURLs from './middleware/redirectLegacyURLs.js'; +import staticFiles from './middleware/staticFiles.js'; +import validatePackageURL from './middleware/validatePackageURL.js'; +import validatePackageName from './middleware/validatePackageName.js'; +import validateQuery from './middleware/validateQuery.js'; + +export default function createServer() { + const app = express(); + + app.disable('x-powered-by'); + app.enable('trust proxy'); + + app.use(logger); + app.use(cors); + app.use(staticFiles); + + // Special startup request from App Engine + // https://cloud.google.com/appengine/docs/standard/nodejs/how-instances-are-managed + app.get('/_ah/start', (req, res) => { + res.status(200).end(); + }); + + app.get('/', serveMainPage); + app.get('/api/stats', serveStats); + + app.use(redirectLegacyURLs); + + app.get( + '*', + validatePackageURL, + validatePackageName, + validateQuery, + fetchPackage, + findFile, + serveFile + ); + + return app; +} diff --git a/modules/middleware/fetchPackage.js b/modules/middleware/fetchPackage.js index a021f259..827607d0 100644 --- a/modules/middleware/fetchPackage.js +++ b/modules/middleware/fetchPackage.js @@ -1,9 +1,9 @@ import semver from 'semver'; -import addLeadingSlash from '../utils/addLeadingSlash'; -import createPackageURL from '../utils/createPackageURL'; -import createSearch from '../utils/createSearch'; -import { getPackageInfo as getNpmPackageInfo } from '../utils/npm'; +import addLeadingSlash from '../utils/addLeadingSlash.js'; +import createPackageURL from '../utils/createPackageURL.js'; +import createSearch from '../utils/createSearch.js'; +import { getPackageInfo as getNpmPackageInfo } from '../utils/npm.js'; function tagRedirect(req, res) { const version = req.packageInfo['dist-tags'][req.packageVersion]; @@ -114,41 +114,41 @@ function filenameRedirect(req, res) { * version if the request targets a tag or uses a semver version, or to the * exact filename if the request omits the filename. */ -export default function fetchPackage(req, res, next) { - getNpmPackageInfo(req.packageName).then( - packageInfo => { - if (packageInfo == null || packageInfo.versions == null) { - return res - .status(404) - .type('text') - .send(`Cannot find package "${req.packageName}"`); - } - - req.packageInfo = packageInfo; - req.packageConfig = req.packageInfo.versions[req.packageVersion]; +export default async function fetchPackage(req, res, next) { + let packageInfo; + try { + packageInfo = await getNpmPackageInfo(req.packageName); + } catch (error) { + console.error(error); - if (!req.packageConfig) { - // Redirect to a fully-resolved version. - if (req.packageVersion in req.packageInfo['dist-tags']) { - return tagRedirect(req, res); - } else { - return semverRedirect(req, res); - } - } + return res + .status(500) + .type('text') + .send(`Cannot get info for package "${req.packageName}"`); + } - if (!req.filename) { - return filenameRedirect(req, res); - } + if (packageInfo == null || packageInfo.versions == null) { + return res + .status(404) + .type('text') + .send(`Cannot find package "${req.packageName}"`); + } - next(); - }, - error => { - console.error(error); + req.packageInfo = packageInfo; + req.packageConfig = req.packageInfo.versions[req.packageVersion]; - return res - .status(500) - .type('text') - .send(`Cannot get info for package "${req.packageName}"`); + if (!req.packageConfig) { + // Redirect to a fully-resolved version. + if (req.packageVersion in req.packageInfo['dist-tags']) { + return tagRedirect(req, res); + } else { + return semverRedirect(req, res); } - ); + } + + if (!req.filename) { + return filenameRedirect(req, res); + } + + next(); } diff --git a/modules/middleware/findFile.js b/modules/middleware/findFile.js index 95871dda..b1e363f7 100644 --- a/modules/middleware/findFile.js +++ b/modules/middleware/findFile.js @@ -1,11 +1,11 @@ import path from 'path'; -import addLeadingSlash from '../utils/addLeadingSlash'; -import createPackageURL from '../utils/createPackageURL'; -import createSearch from '../utils/createSearch'; -import { fetchPackage as fetchNpmPackage } from '../utils/npm'; -import getIntegrity from '../utils/getIntegrity'; -import getContentType from '../utils/getContentType'; +import addLeadingSlash from '../utils/addLeadingSlash.js'; +import createPackageURL from '../utils/createPackageURL.js'; +import createSearch from '../utils/createSearch.js'; +import { fetchPackage as fetchNpmPackage } from '../utils/npm.js'; +import getIntegrity from '../utils/getIntegrity.js'; +import getContentType from '../utils/getContentType.js'; function indexRedirect(req, res, entry) { // Redirect to the index file so relative imports @@ -143,62 +143,59 @@ const trailingSlash = /\/$/; * Fetch and search the archive to try and find the requested file. * Redirect to the "index" file if a directory was requested. */ -export default function findFile(req, res, next) { - fetchNpmPackage(req.packageConfig).then(tarballStream => { - const wantsIndex = trailingSlash.test(req.filename); - - // The name of the file/directory we're looking for. - const entryName = req.filename - .replace(multipleSlash, '/') - .replace(trailingSlash, '') - .replace(leadingSlash, ''); - - searchEntries(tarballStream, entryName, wantsIndex).then( - ({ entries, foundEntry }) => { - if (!foundEntry) { - return res - .status(404) - .set({ - 'Cache-Control': 'public, max-age=31536000', // 1 year - 'Cache-Tag': 'missing, missing-entry' - }) - .type('text') - .send(`Cannot find "${req.filename}" in ${req.packageSpec}`); - } +export default async function findFile(req, res, next) { + const wantsIndex = trailingSlash.test(req.filename); + + // The name of the file/directory we're looking for. + const entryName = req.filename + .replace(multipleSlash, '/') + .replace(trailingSlash, '') + .replace(leadingSlash, ''); + + const tarballStream = await fetchNpmPackage(req.packageConfig); + const { entries, foundEntry } = await searchEntries( + tarballStream, + entryName, + wantsIndex + ); + + if (!foundEntry) { + return res + .status(404) + .set({ + 'Cache-Control': 'public, max-age=31536000', // 1 year + 'Cache-Tag': 'missing, missing-entry' + }) + .type('text') + .send(`Cannot find "${req.filename}" in ${req.packageSpec}`); + } + + // If the foundEntry is a directory and there is no trailing slash + // on the request path, we need to redirect to some "index" file + // inside that directory. This is so our URLs work in a similar way + // to require("lib") in node where it searches for `lib/index.js` + // and `lib/index.json` when `lib` is a directory. + if (foundEntry.type === 'directory' && !wantsIndex) { + const indexEntry = + entries[path.join(entryName, 'index.js')] || + entries[path.join(entryName, 'index.json')]; + + if (indexEntry && indexEntry.type === 'file') { + return indexRedirect(req, res, indexEntry); + } - // If the foundEntry is a directory and there is no trailing slash - // on the request path, we need to redirect to some "index" file - // inside that directory. This is so our URLs work in a similar way - // to require("lib") in node where it searches for `lib/index.js` - // and `lib/index.json` when `lib` is a directory. - if (foundEntry.type === 'directory' && !wantsIndex) { - const indexEntry = - entries[path.join(entryName, 'index.js')] || - entries[path.join(entryName, 'index.json')]; - - if (indexEntry && indexEntry.type === 'file') { - return indexRedirect(req, res, indexEntry); - } else { - return res - .status(404) - .set({ - 'Cache-Control': 'public, max-age=31536000', // 1 year - 'Cache-Tag': 'missing, missing-index' - }) - .type('text') - .send( - `Cannot find an index in "${req.filename}" in ${ - req.packageSpec - }` - ); - } - } + return res + .status(404) + .set({ + 'Cache-Control': 'public, max-age=31536000', // 1 year + 'Cache-Tag': 'missing, missing-index' + }) + .type('text') + .send(`Cannot find an index in "${req.filename}" in ${req.packageSpec}`); + } - req.entries = entries; - req.entry = foundEntry; + req.entries = entries; + req.entry = foundEntry; - next(); - } - ); - }); + next(); } diff --git a/modules/middleware/redirectLegacyURLs.js b/modules/middleware/redirectLegacyURLs.js index 03f46f9c..afe2d0e0 100644 --- a/modules/middleware/redirectLegacyURLs.js +++ b/modules/middleware/redirectLegacyURLs.js @@ -1,15 +1,20 @@ +import createSearch from '../utils/createSearch.js'; + /** * Redirect old URLs that we no longer support. */ export default function redirectLegacyURLs(req, res, next) { - // Permanently redirect /_meta/path to /_metadata/path + // Permanently redirect /_meta/path to /path?meta if (req.path.match(/^\/_meta\//)) { - return res.redirect(301, '/_metadata' + req.path.substr(6)); + req.query.meta = ''; + return res.redirect(301, req.path.substr(6) + createSearch(req.query)); } // Permanently redirect /path?json => /path?meta if (req.query.json != null) { - return res.redirect(301, '/_metadata' + req.path); + delete req.query.json; + req.query.meta = ''; + return res.redirect(301, req.path + createSearch(req.query)); } next(); diff --git a/modules/middleware/validatePackageURL.js b/modules/middleware/validatePackageURL.js index 878e9d6e..153adf75 100644 --- a/modules/middleware/validatePackageURL.js +++ b/modules/middleware/validatePackageURL.js @@ -1,4 +1,4 @@ -import parsePackageURL from '../utils/parsePackageURL'; +import parsePackageURL from '../utils/parsePackageURL.js'; /** * Parse the URL and add various properties to the request object to diff --git a/modules/middleware/validateQuery.js b/modules/middleware/validateQuery.js index 9b3fade5..a5789441 100644 --- a/modules/middleware/validateQuery.js +++ b/modules/middleware/validateQuery.js @@ -1,4 +1,4 @@ -import createSearch from '../utils/createSearch'; +import createSearch from '../utils/createSearch.js'; const knownQueryParams = { main: true, // Deprecated, see #63 diff --git a/modules/plugins/__tests__/unpkgRewrite-test.js b/modules/plugins/__tests__/unpkgRewrite-test.js index f5146e48..d827be0f 100644 --- a/modules/plugins/__tests__/unpkgRewrite-test.js +++ b/modules/plugins/__tests__/unpkgRewrite-test.js @@ -1,6 +1,6 @@ -import babel from 'babel-core'; +import * as babel from '@babel/core'; -import unpkgRewrite from '../unpkgRewrite'; +import unpkgRewrite from '../unpkgRewrite.js'; const testCases = [ { @@ -66,6 +66,7 @@ const testCases = [ } ]; +const origin = 'https://unpkg.com'; const dependencies = { react: '15.6.1', '@angular/router': '4.3.5', @@ -75,9 +76,9 @@ const dependencies = { describe('Rewriting imports/exports', () => { testCases.forEach(testCase => { - it(`successfully rewrites '${testCase.before}'`, () => { + it(`rewrites '${testCase.before}' => '${testCase.after}'`, () => { const result = babel.transform(testCase.before, { - plugins: [unpkgRewrite(dependencies)] + plugins: [unpkgRewrite(origin, dependencies)] }); expect(result.code).toEqual(testCase.after); diff --git a/modules/server.js b/modules/server.js index a0bf2e1f..48253ad4 100644 --- a/modules/server.js +++ b/modules/server.js @@ -1,59 +1,8 @@ -import express from 'express'; - -import serveFile from './actions/serveFile'; -import serveMainPage from './actions/serveMainPage'; -import serveStats from './actions/serveStats'; - -import cors from './middleware/cors'; -import fetchPackage from './middleware/fetchPackage'; -import findFile from './middleware/findFile'; -import logger from './middleware/logger'; -import redirectLegacyURLs from './middleware/redirectLegacyURLs'; -import staticFiles from './middleware/staticFiles'; -import validatePackageURL from './middleware/validatePackageURL'; -import validatePackageName from './middleware/validatePackageName'; -import validateQuery from './middleware/validateQuery'; - -import createRouter from './utils/createRouter'; +import createServer from './createServer.js'; +const server = createServer(); const port = process.env.PORT || '8080'; -const app = express(); - -app.disable('x-powered-by'); -app.enable('trust proxy'); - -app.use(logger); -app.use(cors); -app.use(staticFiles); - -// Special startup request from App Engine -// https://cloud.google.com/appengine/docs/standard/nodejs/how-instances-are-managed -app.get('/_ah/start', (req, res) => { - res.status(200).end(); -}); - -app.get('/', serveMainPage); - -app.use(redirectLegacyURLs); - -app.use( - '/api', - createRouter(app => { - app.get('/stats', serveStats); - }) -); - -app.get( - '*', - validatePackageURL, - validatePackageName, - validateQuery, - fetchPackage, - findFile, - serveFile -); - -app.listen(port, () => { +server.listen(port, () => { console.log('Server listening on port %s, Ctrl+C to quit', port); }); diff --git a/modules/utils/__tests__/createSearch-test.js b/modules/utils/__tests__/createSearch-test.js index c55050cb..0a887a0f 100644 --- a/modules/utils/__tests__/createSearch-test.js +++ b/modules/utils/__tests__/createSearch-test.js @@ -1,4 +1,4 @@ -import createSearch from '../createSearch'; +import createSearch from '../createSearch.js'; describe('createSearch', () => { it('omits the trailing = for empty string values', () => { diff --git a/modules/utils/__tests__/getContentType-test.js b/modules/utils/__tests__/getContentType-test.js index ee5f4f25..e878aa66 100644 --- a/modules/utils/__tests__/getContentType-test.js +++ b/modules/utils/__tests__/getContentType-test.js @@ -1,4 +1,4 @@ -import getContentType from '../getContentType'; +import getContentType from '../getContentType.js'; it('gets a content type of text/plain for LICENSE|README|CHANGES|AUTHORS|Makefile', () => { expect(getContentType('AUTHORS')).toBe('text/plain'); diff --git a/modules/utils/__tests__/parsePackageURL-test.js b/modules/utils/__tests__/parsePackageURL-test.js index b89eef27..3ead80ad 100644 --- a/modules/utils/__tests__/parsePackageURL-test.js +++ b/modules/utils/__tests__/parsePackageURL-test.js @@ -1,4 +1,4 @@ -import parsePackageURL from '../parsePackageURL'; +import parsePackageURL from '../parsePackageURL.js'; describe('parsePackageURL', () => { it('parses plain packages', () => { diff --git a/modules/utils/bufferStream.js b/modules/utils/bufferStream.js index 421d715e..34531f68 100644 --- a/modules/utils/bufferStream.js +++ b/modules/utils/bufferStream.js @@ -1,10 +1,10 @@ export default function bufferStream(stream) { - return new Promise((resolve, reject) => { + return new Promise((accept, reject) => { const chunks = []; stream .on('error', reject) .on('data', chunk => chunks.push(chunk)) - .on('end', () => resolve(Buffer.concat(chunks))); + .on('end', () => accept(Buffer.concat(chunks))); }); } diff --git a/modules/utils/createRouter.js b/modules/utils/createRouter.js deleted file mode 100644 index 898c9e77..00000000 --- a/modules/utils/createRouter.js +++ /dev/null @@ -1,7 +0,0 @@ -import express from 'express'; - -export default function createRouter(configureRouter) { - const router = express.Router(); - configureRouter(router); - return router; -} diff --git a/modules/utils/createSearch.js b/modules/utils/createSearch.js index 44aa19bb..bfa89918 100644 --- a/modules/utils/createSearch.js +++ b/modules/utils/createSearch.js @@ -3,9 +3,7 @@ export default function createSearch(query) { const params = keys.reduce( (memo, key) => memo.concat( - query[key] === '' - ? key // Omit the trailing "=" from key= - : `${key}=${encodeURIComponent(query[key])}` + query[key] ? `${key}=${encodeURIComponent(query[key])}` : key ), [] ); diff --git a/modules/utils/npm.js b/modules/utils/npm.js index 9d59bc04..9d58f2f4 100644 --- a/modules/utils/npm.js +++ b/modules/utils/npm.js @@ -4,8 +4,8 @@ import gunzip from 'gunzip-maybe'; import tar from 'tar-stream'; import LRUCache from 'lru-cache'; -import debug from './debug'; -import bufferStream from './bufferStream'; +import debug from './debug.js'; +import bufferStream from './bufferStream.js'; const npmRegistryURL = process.env.NPM_REGISTRY_URL || 'https://registry.npmjs.org'; diff --git a/modules/utils/rewriteBareModuleIdentifiers.js b/modules/utils/rewriteBareModuleIdentifiers.js index d540747b..be9fbcc2 100644 --- a/modules/utils/rewriteBareModuleIdentifiers.js +++ b/modules/utils/rewriteBareModuleIdentifiers.js @@ -1,6 +1,6 @@ import babel from '@babel/core'; -import unpkgRewrite from '../plugins/unpkgRewrite'; +import unpkgRewrite from '../plugins/unpkgRewrite.js'; const origin = process.env.ORIGIN || 'https://unpkg.com'; diff --git a/package.json b/package.json index 637f320a..74f42a90 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "etag": "^1.8.1", "express": "^4.16.4", "gunzip-maybe": "^1.4.1", - "invariant": "^2.2.4", "isomorphic-fetch": "^2.2.1", "lru-cache": "^5.1.1", "mime": "^2.4.0", diff --git a/yarn.lock b/yarn.lock index 63c98779..81c3326a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3142,7 +3142,7 @@ inquirer@^3.0.6: strip-ansi "^4.0.0" through "^2.3.6" -invariant@^2.2.0, invariant@^2.2.2, invariant@^2.2.4: +invariant@^2.2.0, invariant@^2.2.2: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==