diff --git a/modules/__tests__/directoryIndex-test.js b/modules/__tests__/directoryIndex-test.js new file mode 100644 index 0000000..1d753f2 --- /dev/null +++ b/modules/__tests__/directoryIndex-test.js @@ -0,0 +1,34 @@ +import request from 'supertest'; + +import createServer from '../createServer.js'; + +describe('A request that targets a directory with a trailing slash', () => { + let server; + beforeEach(() => { + server = createServer(); + }); + + describe('when the directory exists', () => { + it('returns an HTML page', done => { + request(server) + .get('/react@16.8.0/umd/') + .end((err, res) => { + expect(res.statusCode).toBe(200); + expect(res.headers['content-type']).toMatch(/\btext\/html\b/); + done(); + }); + }); + }); + + describe('when the directory does not exist', () => { + it('returns a 404 text error', done => { + request(server) + .get('/react@16.8.0/not-here/') + .end((err, res) => { + expect(res.statusCode).toBe(404); + expect(res.headers['content-type']).toMatch(/\btext\/plain\b/); + done(); + }); + }); + }); +}); diff --git a/modules/__tests__/directoryRedirect-test.js b/modules/__tests__/directoryRedirect-test.js new file mode 100644 index 0000000..ba05994 --- /dev/null +++ b/modules/__tests__/directoryRedirect-test.js @@ -0,0 +1,40 @@ +import request from 'supertest'; + +import createServer from '../createServer.js'; + +describe('A request for a directory', () => { + let server; + beforeEach(() => { + server = createServer(); + }); + + describe('when a .js file exists with the same name', () => { + it('is redirected to the .js file', done => { + request(server) + .get('/preact@8.4.2/devtools') + .end((err, res) => { + expect(res.statusCode).toBe(302); + expect(res.headers.location).toEqual('/preact@8.4.2/devtools.js'); + done(); + }); + }); + }); + + describe('when a .json file exists with the same name', () => { + it('is redirected to the .json file'); + }); + + describe('when it contains an index.js file', () => { + it('is redirected to the index.js file', done => { + request(server) + .get('/preact@8.4.2/src/dom') + .end((err, res) => { + expect(res.statusCode).toBe(302); + expect(res.headers.location).toEqual( + '/preact@8.4.2/src/dom/index.js' + ); + done(); + }); + }); + }); +}); diff --git a/modules/__tests__/missingFile-test.js b/modules/__tests__/missingFile-test.js new file mode 100644 index 0000000..0a4b0fd --- /dev/null +++ b/modules/__tests__/missingFile-test.js @@ -0,0 +1,20 @@ +import request from 'supertest'; + +import createServer from '../createServer.js'; + +describe('A request for a non-existent file', () => { + let server; + beforeEach(() => { + server = createServer(); + }); + + it('returns a 404 text error', done => { + request(server) + .get('/preact@8.4.2/not-here.js') + .end((err, res) => { + expect(res.statusCode).toBe(404); + expect(res.headers['content-type']).toMatch(/\btext\/plain\b/); + done(); + }); + }); +}); diff --git a/modules/actions/serveAutoIndexPage.js b/modules/actions/serveAutoIndexPage.js index 29fe6ff..1a5ccdd 100644 --- a/modules/actions/serveAutoIndexPage.js +++ b/modules/actions/serveAutoIndexPage.js @@ -4,13 +4,8 @@ import semver from 'semver'; import AutoIndexApp from '../client/autoIndex/App.js'; import MainTemplate from './utils/MainTemplate.js'; -import getEntryPoint from './utils/getEntryPoint.js'; -import getGlobalScripts from './utils/getGlobalScripts.js'; -import { - createElement, - createHTML, - createScript -} from './utils/markupHelpers.js'; +import getScripts from './utils/getScripts.js'; +import { createElement, createHTML } from './utils/markupHelpers.js'; const doctype = ''; const globalURLs = @@ -40,10 +35,7 @@ export default function serveAutoIndexPage(req, res) { entries: req.entries }; const content = createHTML(renderToString(createElement(AutoIndexApp, data))); - const entryPoint = getEntryPoint('autoIndex', 'iife'); - const elements = getGlobalScripts(entryPoint, globalURLs).concat( - createScript(entryPoint.code) - ); + const elements = getScripts('autoIndex', 'iife', globalURLs); const html = doctype + diff --git a/modules/actions/serveMainPage.js b/modules/actions/serveMainPage.js index 178e990..a292195 100644 --- a/modules/actions/serveMainPage.js +++ b/modules/actions/serveMainPage.js @@ -3,13 +3,8 @@ import { renderToString, renderToStaticMarkup } from 'react-dom/server'; import MainApp from '../client/main/App.js'; import MainTemplate from './utils/MainTemplate.js'; -import getEntryPoint from './utils/getEntryPoint.js'; -import getGlobalScripts from './utils/getGlobalScripts.js'; -import { - createElement, - createHTML, - createScript -} from './utils/markupHelpers.js'; +import getScripts from './utils/getScripts.js'; +import { createElement, createHTML } from './utils/markupHelpers.js'; const doctype = ''; const globalURLs = @@ -27,10 +22,7 @@ const globalURLs = export default function serveMainPage(req, res) { const content = createHTML(renderToString(createElement(MainApp))); - const entryPoint = getEntryPoint('main', 'iife'); - const elements = getGlobalScripts(entryPoint, globalURLs).concat( - createScript(entryPoint.code) - ); + const elements = getScripts('main', 'iife', globalURLs); const html = doctype + diff --git a/modules/actions/utils/getEntryPoint.js b/modules/actions/utils/getEntryPoint.js deleted file mode 100644 index ed5fe3f..0000000 --- a/modules/actions/utils/getEntryPoint.js +++ /dev/null @@ -1,18 +0,0 @@ -// 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) { - let entryPoints; - entryManifest.forEach(manifest => { - if (name in manifest) { - entryPoints = manifest[name]; - } - }); - - if (entryPoints) { - return entryPoints.find(e => e.format === format); - } - - return null; -} diff --git a/modules/actions/utils/getGlobalScripts.js b/modules/actions/utils/getGlobalScripts.js deleted file mode 100644 index 99037d0..0000000 --- a/modules/actions/utils/getGlobalScripts.js +++ /dev/null @@ -1,13 +0,0 @@ -import { createElement } from './markupHelpers.js'; - -export default function getGlobalScripts(entryPoint, globalURLs) { - return entryPoint.globalImports.map(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/getScripts.js b/modules/actions/utils/getScripts.js new file mode 100644 index 0000000..ae02dee --- /dev/null +++ b/modules/actions/utils/getScripts.js @@ -0,0 +1,42 @@ +// Virtual module id; see rollup.config.js +// eslint-disable-next-line import/no-unresolved +import entryManifest from 'entry-manifest'; + +import { createElement, createScript } from './markupHelpers.js'; + +function getEntryPoint(name, format) { + let entryPoints; + entryManifest.forEach(manifest => { + if (name in manifest) { + entryPoints = manifest[name]; + } + }); + + if (entryPoints) { + return entryPoints.find(e => e.format === format); + } + + return null; +} + +function getGlobalScripts(entryPoint, globalURLs) { + return entryPoint.globalImports.map(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] }); + }); +} + +export default function getScripts(entryName, format, globalURLs) { + const entryPoint = getEntryPoint(entryName, format); + + if (!entryPoint) return []; + + return getGlobalScripts(entryPoint, globalURLs).concat( + createScript(entryPoint.code) + ); +} diff --git a/modules/middleware/findFile.js b/modules/middleware/findFile.js index b1e363f..fa99cdf 100644 --- a/modules/middleware/findFile.js +++ b/modules/middleware/findFile.js @@ -7,6 +7,25 @@ import { fetchPackage as fetchNpmPackage } from '../utils/npm.js'; import getIntegrity from '../utils/getIntegrity.js'; import getContentType from '../utils/getContentType.js'; +function fileRedirect(req, res, entry) { + // Redirect to the file with the extension so it's more + // clear which file is being served. + res + .set({ + 'Cache-Control': 'public, max-age=31536000', // 1 year + 'Cache-Tag': 'redirect, file-redirect' + }) + .redirect( + 302, + createPackageURL( + req.packageName, + req.packageVersion, + addLeadingSlash(entry.name), + createSearch(req.query) + ) + ); +} + function indexRedirect(req, res, entry) { // Redirect to the index file so relative imports // resolve correctly. @@ -104,7 +123,9 @@ function searchEntries(tarballStream, entryName, wantsIndex) { const chunks = []; stream - .on('data', chunk => chunks.push(chunk)) + .on('data', chunk => { + chunks.push(chunk); + }) .on('end', () => { const content = Buffer.concat(chunks); @@ -170,6 +191,10 @@ export default async function findFile(req, res, next) { .send(`Cannot find "${req.filename}" in ${req.packageSpec}`); } + if (foundEntry.type === 'file' && foundEntry.name !== entryName) { + return fileRedirect(req, res, foundEntry); + } + // 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 @@ -177,8 +202,7 @@ export default async function findFile(req, res, next) { // 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')]; + entries[`${entryName}/index.js`] || entries[`${entryName}/index.json`]; if (indexEntry && indexEntry.type === 'file') { return indexRedirect(req, res, indexEntry);