New "browse" UI
Also, separated out browse, ?meta, and ?module request handlers. Fixes #82
This commit is contained in:
		| @ -9,13 +9,8 @@ import getIntegrity from '../utils/getIntegrity.js'; | ||||
| import getContentType from '../utils/getContentType.js'; | ||||
| import bufferStream from '../utils/bufferStream.js'; | ||||
| 
 | ||||
| const leadingSlashes = /^\/*/; | ||||
| const multipleSlashes = /\/*/; | ||||
| const trailingSlashes = /\/*$/; | ||||
| const leadingSegment = /^[^/]+\/?/; | ||||
| 
 | ||||
| function fileRedirect(req, res, entry) { | ||||
|   // Redirect to the file with the extension so it's more
 | ||||
|   // Redirect to the file with the extension so it's
 | ||||
|   // clear which file is being served.
 | ||||
|   res | ||||
|     .set({ | ||||
| @ -27,7 +22,7 @@ function fileRedirect(req, res, entry) { | ||||
|       createPackageURL( | ||||
|         req.packageName, | ||||
|         req.packageVersion, | ||||
|         entry.name.replace(leadingSlashes, '/'), | ||||
|         entry.path, | ||||
|         createSearch(req.query) | ||||
|       ) | ||||
|     ); | ||||
| @ -46,7 +41,7 @@ function indexRedirect(req, res, entry) { | ||||
|       createPackageURL( | ||||
|         req.packageName, | ||||
|         req.packageVersion, | ||||
|         entry.name.replace(leadingSlashes, '/'), | ||||
|         entry.path, | ||||
|         createSearch(req.query) | ||||
|       ) | ||||
|     ); | ||||
| @ -57,16 +52,17 @@ function indexRedirect(req, res, entry) { | ||||
|  * Follows node's resolution algorithm. | ||||
|  * https://nodejs.org/api/modules.html#modules_all_together
 | ||||
|  */ | ||||
| function searchEntries(stream, entryName, wantsIndex) { | ||||
| function searchEntries(stream, filename) { | ||||
|   // filename = /some/file/name.js or /some/dir/name
 | ||||
|   return new Promise((accept, reject) => { | ||||
|     const jsEntryName = `${entryName}.js`; | ||||
|     const jsonEntryName = `${entryName}.json`; | ||||
|     const entries = {}; | ||||
|     const jsEntryFilename = `${filename}.js`; | ||||
|     const jsonEntryFilename = `${filename}.json`; | ||||
| 
 | ||||
|     const matchingEntries = {}; | ||||
|     let foundEntry; | ||||
| 
 | ||||
|     if (entryName === '') { | ||||
|       foundEntry = entries[''] = { name: '', type: 'directory' }; | ||||
|     if (filename === '/') { | ||||
|       foundEntry = matchingEntries['/'] = { name: '/', type: 'directory' }; | ||||
|     } | ||||
| 
 | ||||
|     stream | ||||
| @ -79,41 +75,43 @@ function searchEntries(stream, entryName, wantsIndex) { | ||||
|           // 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: header.name.replace(leadingSegment, ''), | ||||
|           path: header.name.replace(/^[^/]+/g, ''), | ||||
|           type: header.type | ||||
|         }; | ||||
| 
 | ||||
|         // Skip non-files and files that don't match the entryName.
 | ||||
|         if (entry.type !== 'file' || entry.name.indexOf(entryName) !== 0) { | ||||
|         if (entry.type !== 'file' || !entry.path.startsWith(filename)) { | ||||
|           stream.resume(); | ||||
|           stream.on('end', next); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         entries[entry.name] = entry; | ||||
|         matchingEntries[entry.path] = 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 dir = path.dirname(entry.name); | ||||
|         while (dir !== '.') { | ||||
|           entries[dir] = entries[dir] || { name: dir, type: 'directory' }; | ||||
|         let dir = path.dirname(entry.path); | ||||
|         while (dir !== '/') { | ||||
|           if (!matchingEntries[dir]) { | ||||
|             matchingEntries[dir] = { name: dir, type: 'directory' }; | ||||
|           } | ||||
|           dir = path.dirname(dir); | ||||
|         } | ||||
| 
 | ||||
|         if ( | ||||
|           entry.name === entryName || | ||||
|           entry.path === filename || | ||||
|           // Allow accessing e.g. `/index.js` or `/index.json`
 | ||||
|           // using `/index` for compatibility with npm
 | ||||
|           (!wantsIndex && entry.name === jsEntryName) || | ||||
|           (!wantsIndex && entry.name === jsonEntryName) | ||||
|           entry.path === jsEntryFilename || | ||||
|           entry.path === jsonEntryFilename | ||||
|         ) { | ||||
|           if (foundEntry) { | ||||
|             if ( | ||||
|               foundEntry.name !== entryName && | ||||
|               (entry.name === entryName || | ||||
|                 (entry.name === jsEntryName && | ||||
|                   foundEntry.name === jsonEntryName)) | ||||
|               foundEntry.path !== filename && | ||||
|               (entry.path === filename || | ||||
|                 (entry.path === jsEntryFilename && | ||||
|                   foundEntry.path === jsonEntryFilename)) | ||||
|             ) { | ||||
|               // This entry is higher priority than the one
 | ||||
|               // we already found. Replace it.
 | ||||
| @ -127,9 +125,7 @@ function searchEntries(stream, entryName, wantsIndex) { | ||||
| 
 | ||||
|         const content = await bufferStream(stream); | ||||
| 
 | ||||
|         // Set some extra properties for files that we will
 | ||||
|         // need to serve them and for ?meta listings.
 | ||||
|         entry.contentType = getContentType(entry.name); | ||||
|         entry.contentType = getContentType(entry.path); | ||||
|         entry.integrity = getIntegrity(content); | ||||
|         entry.lastModified = header.mtime.toUTCString(); | ||||
|         entry.size = content.length; | ||||
| @ -144,10 +140,10 @@ function searchEntries(stream, entryName, wantsIndex) { | ||||
|       }) | ||||
|       .on('finish', () => { | ||||
|         accept({ | ||||
|           entries, | ||||
|           // If we didn't find a matching file entry,
 | ||||
|           // try a directory entry with the same name.
 | ||||
|           foundEntry: foundEntry || entries[entryName] || null | ||||
|           foundEntry: foundEntry || matchingEntries[filename] || null, | ||||
|           matchingEntries: matchingEntries | ||||
|         }); | ||||
|       }); | ||||
|   }); | ||||
| @ -157,23 +153,14 @@ function searchEntries(stream, entryName, wantsIndex) { | ||||
|  * Fetch and search the archive to try and find the requested file. | ||||
|  * Redirect to the "index" file if a directory was requested. | ||||
|  */ | ||||
| export default async function findFile(req, res, next) { | ||||
|   const wantsIndex = req.filename.endsWith('/'); | ||||
| 
 | ||||
|   // The name of the file/directory we're looking for.
 | ||||
|   const entryName = req.filename | ||||
|     .replace(multipleSlashes, '/') | ||||
|     .replace(trailingSlashes, '') | ||||
|     .replace(leadingSlashes, ''); | ||||
| 
 | ||||
| export default async function findEntry(req, res, next) { | ||||
|   const stream = await getPackage(req.packageName, req.packageVersion); | ||||
|   const { entries, foundEntry } = await searchEntries( | ||||
|   const { foundEntry: entry, matchingEntries: entries } = await searchEntries( | ||||
|     stream, | ||||
|     entryName, | ||||
|     wantsIndex | ||||
|     req.filename | ||||
|   ); | ||||
| 
 | ||||
|   if (!foundEntry) { | ||||
|   if (!entry) { | ||||
|     return res | ||||
|       .status(404) | ||||
|       .set({ | ||||
| @ -184,18 +171,17 @@ 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 (entry.type === 'file' && entry.path !== req.filename) { | ||||
|     return fileRedirect(req, res, entry); | ||||
|   } | ||||
| 
 | ||||
|   // 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) { | ||||
|   if (entry.type === 'directory') { | ||||
|     // We need to redirect to some "index" file inside the directory so
 | ||||
|     // our URLs work in a similar way to require("lib") in node where it
 | ||||
|     // uses `lib/index.js` when `lib` is a directory.
 | ||||
|     const indexEntry = | ||||
|       entries[`${entryName}/index.js`] || entries[`${entryName}/index.json`]; | ||||
|       entries[`${req.filename}/index.js`] || | ||||
|       entries[`${req.filename}/index.json`]; | ||||
| 
 | ||||
|     if (indexEntry && indexEntry.type === 'file') { | ||||
|       return indexRedirect(req, res, indexEntry); | ||||
| @ -211,8 +197,7 @@ export default async function findFile(req, res, next) { | ||||
|       .send(`Cannot find an index in "${req.filename}" in ${req.packageSpec}`); | ||||
|   } | ||||
| 
 | ||||
|   req.entries = entries; | ||||
|   req.entry = foundEntry; | ||||
|   req.entry = entry; | ||||
| 
 | ||||
|   next(); | ||||
| } | ||||
| @ -1,18 +1,5 @@ | ||||
| import createPackageURL from '../utils/createPackageURL.js'; | ||||
| import createSearch from '../utils/createSearch.js'; | ||||
| import { getPackageConfig, resolveVersion } from '../utils/npm.js'; | ||||
| 
 | ||||
| function semverRedirect(req, res, newVersion) { | ||||
|   res | ||||
|     .set({ | ||||
|       'Cache-Control': 'public, s-maxage=600, max-age=60', // 10 mins on CDN, 1 min on clients
 | ||||
|       'Cache-Tag': 'redirect, semver-redirect' | ||||
|     }) | ||||
|     .redirect( | ||||
|       302, | ||||
|       createPackageURL(req.packageName, newVersion, req.filename, req.search) | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| const leadingSlashes = /^\/*/; | ||||
| 
 | ||||
| @ -83,37 +70,9 @@ function filenameRedirect(req, res) { | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Fetch the package config. Redirect to the exact version if the request | ||||
|  * targets a tag or uses semver, or to the exact filename if the request | ||||
|  * omits the filename. | ||||
|  * Redirect to the exact filename if the request omits one. | ||||
|  */ | ||||
| export default async function fetchPackage(req, res, next) { | ||||
|   const version = await resolveVersion(req.packageName, req.packageVersion); | ||||
| 
 | ||||
|   if (!version) { | ||||
|     return res | ||||
|       .status(404) | ||||
|       .type('text') | ||||
|       .send(`Cannot find package ${req.packageSpec}`); | ||||
|   } | ||||
| 
 | ||||
|   if (version !== req.packageVersion) { | ||||
|     return semverRedirect(req, res, version); | ||||
|   } | ||||
| 
 | ||||
|   req.packageConfig = await getPackageConfig( | ||||
|     req.packageName, | ||||
|     req.packageVersion | ||||
|   ); | ||||
| 
 | ||||
|   if (!req.packageConfig) { | ||||
|     // TODO: Log why.
 | ||||
|     return res | ||||
|       .status(500) | ||||
|       .type('text') | ||||
|       .send(`Cannot get config for package ${req.packageSpec}`); | ||||
|   } | ||||
| 
 | ||||
| export default async function validateFilename(req, res, next) { | ||||
|   if (!req.filename) { | ||||
|     return filenameRedirect(req, res); | ||||
|   } | ||||
							
								
								
									
										49
									
								
								modules/middleware/validateVersion.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								modules/middleware/validateVersion.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | ||||
| import createPackageURL from '../utils/createPackageURL.js'; | ||||
| import { getPackageConfig, resolveVersion } from '../utils/npm.js'; | ||||
|  | ||||
| function semverRedirect(req, res, newVersion) { | ||||
|   res | ||||
|     .set({ | ||||
|       'Cache-Control': 'public, s-maxage=600, max-age=60', // 10 mins on CDN, 1 min on clients | ||||
|       'Cache-Tag': 'redirect, semver-redirect' | ||||
|     }) | ||||
|     .redirect( | ||||
|       302, | ||||
|       createPackageURL(req.packageName, newVersion, req.filename, req.search) | ||||
|     ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Check the package version/tag in the URL and make sure it's good. Also | ||||
|  * fetch the package config and add it to req.packageConfig. Redirect to | ||||
|  * the resolved version number if necessary. | ||||
|  */ | ||||
| export default async function validateVersion(req, res, next) { | ||||
|   const version = await resolveVersion(req.packageName, req.packageVersion); | ||||
|  | ||||
|   if (!version) { | ||||
|     return res | ||||
|       .status(404) | ||||
|       .type('text') | ||||
|       .send(`Cannot find package ${req.packageSpec}`); | ||||
|   } | ||||
|  | ||||
|   if (version !== req.packageVersion) { | ||||
|     return semverRedirect(req, res, version); | ||||
|   } | ||||
|  | ||||
|   req.packageConfig = await getPackageConfig( | ||||
|     req.packageName, | ||||
|     req.packageVersion | ||||
|   ); | ||||
|  | ||||
|   if (!req.packageConfig) { | ||||
|     // TODO: Log why. | ||||
|     return res | ||||
|       .status(500) | ||||
|       .type('text') | ||||
|       .send(`Cannot get config for package ${req.packageSpec}`); | ||||
|   } | ||||
|  | ||||
|   next(); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user