import path from 'path'; import addLeadingSlash from '../utils/addLeadingSlash'; import createPackageURL from '../utils/createPackageURL'; import createSearch from '../utils/createSearch'; import fetchNpmPackage from '../utils/fetchNpmPackage'; import getIntegrity from '../utils/getIntegrity'; import getContentType from '../utils/getContentType'; function indexRedirect(req, res, entry) { // Redirect to the index file so relative imports // resolve correctly. res .set({ 'Cache-Control': 'public, max-age=31536000', // 1 year 'Cache-Tag': 'redirect, index-redirect' }) .redirect( 302, createPackageURL( req.packageName, req.packageVersion, addLeadingSlash(entry.name), createSearch(req.query) ) ); } function stripLeadingSegment(name) { return name.replace(/^[^/]+\/?/, ''); } function searchEntries(tarballStream, entryName, wantsIndex) { return new Promise((resolve, reject) => { const entries = {}; let foundEntry = null; if (entryName === '') { foundEntry = entries[''] = { name: '', type: 'directory' }; } tarballStream .on('error', reject) .on('finish', () => resolve({ entries, foundEntry })) .on('entry', (header, stream, next) => { const entry = { // Most packages have header names that look like `package/index.js` // so we shorten that to just `index.js` here. A few packages use a // prefix other than `package/`. e.g. the firebase package uses the // `firebase_npm/` prefix. So we just strip the first dir name. name: stripLeadingSegment(header.name), type: header.type }; // We are only interested in files that match the entryName. if (entry.type !== 'file' || entry.name.indexOf(entryName) !== 0) { stream.resume(); stream.on('end', next); return; } entries[entry.name] = entry; // Dynamically create "directory" entries for all directories // that are in this file's path. Some tarballs omit these entries // for some reason, so this is the brute force method. let dirname = path.dirname(entry.name); while (dirname !== '.') { const directoryEntry = { name: dirname, type: 'directory' }; if (!entries[dirname]) { entries[dirname] = directoryEntry; if (directoryEntry.name === entryName) { foundEntry = directoryEntry; } } dirname = path.dirname(dirname); } // Set the foundEntry variable if this entry name // matches exactly or if it's an index.html file // and the client wants HTML. if ( entry.name === entryName || // Allow accessing e.g. `/index.js` or `/index.json` using // `/index` for compatibility with CommonJS (!wantsIndex && entry.name === `${entryName}.js`) || (!wantsIndex && entry.name === `${entryName}.json`) ) { foundEntry = entry; } const chunks = []; stream.on('data', chunk => chunks.push(chunk)).on('end', () => { const content = Buffer.concat(chunks); // Set some extra properties for files that we will // need to serve them and for ?meta listings. entry.contentType = getContentType(entry.name); entry.integrity = getIntegrity(content); entry.lastModified = header.mtime.toUTCString(); entry.size = content.length; // Set the content only for the foundEntry and // discard the buffer for all others. if (entry === foundEntry) { entry.content = content; } next(); }); }); }); } const leadingSlash = /^\//; const trailingSlash = /\/$/; const multipleSlash = /\/\/+/; /** * 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 entryName = req.filename .replace(multipleSlash, '/') .replace(trailingSlash, '') .replace(leadingSlash, ''); const wantsIndex = trailingSlash.test(req.filename); 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}`); } // 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 }` ); } } req.entries = entries; req.entry = foundEntry; next(); } ); }); }