diff --git a/jest.config.js b/jest.config.js index cdafc24..7a8f4c7 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 0000000..4df14a9 --- /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 0000000..d6d1738 --- /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 0000000..08d725c --- /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 0000000..3e8a8bb --- /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 0000000..a454792 --- /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 0000000..90658fd --- /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 0b4a852..0000000 --- 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 83cf873..1686d58 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 838a793..3d0c52c 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 0b13d3d..f6d1054 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 5b7621a..97c3551 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 6e3bfc4..85aadaa 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 9ed3f8d..1043671 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 fb408a7..10c9039 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 5d6b1d6..a7ddcdc 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 663c767..7df4d62 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 1854de3..c3ff2c2 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 6bd3ab9..67bd1c7 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 c450284..ed5fe3f 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 cdb3fa0..9bfe540 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 8fc82c5..6505f0e 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 4c18fb3..6b6125c 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 d17408d..b1e8fbe 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 0000000..4080e89 --- /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 a021f25..827607d 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}"`); - } +export default async function fetchPackage(req, res, next) { + let packageInfo; + try { + packageInfo = await getNpmPackageInfo(req.packageName); + } catch (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 (packageInfo == null || packageInfo.versions == null) { + return res + .status(404) + .type('text') + .send(`Cannot find package "${req.packageName}"`); + } - if (!req.filename) { - return filenameRedirect(req, res); - } + req.packageInfo = packageInfo; + req.packageConfig = req.packageInfo.versions[req.packageVersion]; - next(); - }, - error => { - console.error(error); - - 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 95871dd..b1e363f 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); +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, ''); + // 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}`); - } + const tarballStream = await fetchNpmPackage(req.packageConfig); + const { entries, foundEntry } = await searchEntries( + tarballStream, + entryName, + wantsIndex + ); - // 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 (!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 (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 - }` - ); - } - } + // 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')]; - req.entries = entries; - req.entry = foundEntry; + if (indexEntry && indexEntry.type === 'file') { + return indexRedirect(req, res, indexEntry); + } - next(); - } - ); - }); + 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; + + next(); } diff --git a/modules/middleware/redirectLegacyURLs.js b/modules/middleware/redirectLegacyURLs.js index 03f46f9..afe2d0e 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 878e9d6..153adf7 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 9b3fade..a578944 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 f5146e4..d827be0 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 a0bf2e1..48253ad 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 c55050c..0a887a0 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 ee5f4f2..e878aa6 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 b89eef2..3ead80a 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 421d715..34531f6 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 898c9e7..0000000 --- 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 44aa19b..bfa8991 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 9d59bc0..9d58f2f 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 d540747..be9fbcc 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 637f320..74f42a9 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 63c9877..81c3326 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==