forked from unpkg/unpkg
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Also, separated out browse, ?meta, and ?module request handlers. Fixes unpkg#82
- Loading branch information
Showing
57 changed files
with
2,423 additions
and
678 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,7 @@ import request from 'supertest'; | |
|
||
import createServer from '../createServer.js'; | ||
|
||
describe('A request that targets a directory with a trailing slash', () => { | ||
describe('A request to browse a directory', () => { | ||
let server; | ||
beforeEach(() => { | ||
server = createServer(); | ||
|
@@ -11,7 +11,7 @@ describe('A request that targets a directory with a trailing slash', () => { | |
describe('when the directory exists', () => { | ||
it('returns an HTML page', done => { | ||
request(server) | ||
.get('/[email protected]/umd/') | ||
.get('/browse/[email protected]/umd/') | ||
.end((err, res) => { | ||
expect(res.statusCode).toBe(200); | ||
expect(res.headers['content-type']).toMatch(/\btext\/html\b/); | ||
|
@@ -21,12 +21,12 @@ describe('A request that targets a directory with a trailing slash', () => { | |
}); | ||
|
||
describe('when the directory does not exist', () => { | ||
it('returns a 404 text error', done => { | ||
it('returns a 404 HTML page', done => { | ||
request(server) | ||
.get('/[email protected]/not-here/') | ||
.get('/browse/[email protected]/not-here/') | ||
.end((err, res) => { | ||
expect(res.statusCode).toBe(404); | ||
expect(res.headers['content-type']).toMatch(/\btext\/plain\b/); | ||
expect(res.headers['content-type']).toMatch(/\btext\/html\b/); | ||
done(); | ||
}); | ||
}); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import request from 'supertest'; | ||
|
||
import createServer from '../createServer.js'; | ||
|
||
describe('A request to browse a file', () => { | ||
let server; | ||
beforeEach(() => { | ||
server = createServer(); | ||
}); | ||
|
||
describe('when the file exists', () => { | ||
it('returns an HTML page', done => { | ||
request(server) | ||
.get('/browse/[email protected]/umd/react.production.min.js') | ||
.end((err, res) => { | ||
expect(res.statusCode).toBe(200); | ||
expect(res.headers['content-type']).toMatch(/\btext\/html\b/); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('when the file does not exist', () => { | ||
it('returns a 404 HTML page', done => { | ||
request(server) | ||
.get('/browse/[email protected]/not-here.js') | ||
.end((err, res) => { | ||
expect(res.statusCode).toBe(404); | ||
expect(res.headers['content-type']).toMatch(/\btext\/html\b/); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,4 +27,14 @@ describe('Legacy URLs', () => { | |
done(); | ||
}); | ||
}); | ||
|
||
it('redirect */ to /browse/*/', done => { | ||
request(server) | ||
.get('/[email protected]/umd/') | ||
.end((err, res) => { | ||
expect(res.statusCode).toBe(302); | ||
expect(res.headers.location).toEqual('/browse/[email protected]/umd/'); | ||
done(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,38 +1,36 @@ | ||
import { renderToString, renderToStaticMarkup } from 'react-dom/server'; | ||
|
||
import AutoIndexApp from '../client/autoIndex/App.js'; | ||
|
||
import MainTemplate from './utils/MainTemplate.js'; | ||
import getScripts from './utils/getScripts.js'; | ||
import { createElement, createHTML } from './utils/markupHelpers.js'; | ||
import BrowseApp from '../client/browse/App.js'; | ||
import MainTemplate from '../templates/MainTemplate.js'; | ||
import { getAvailableVersions } from '../utils/npm.js'; | ||
import getScripts from '../utils/getScripts.js'; | ||
import { createElement, createHTML } from '../utils/markup.js'; | ||
|
||
const doctype = '<!DOCTYPE html>'; | ||
const globalURLs = | ||
process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'staging' | ||
? { | ||
'@emotion/core': '/@emotion/[email protected]/dist/core.umd.min.js', | ||
react: '/react@16.7.0/umd/react.production.min.js', | ||
'react-dom': '/react-dom@16.7.0/umd/react-dom.production.min.js' | ||
react: '/react@16.8.6/umd/react.production.min.js', | ||
'react-dom': '/react-dom@16.8.6/umd/react-dom.production.min.js' | ||
} | ||
: { | ||
'@emotion/core': '/@emotion/[email protected]/dist/core.umd.min.js', | ||
react: '/react@16.7.0/umd/react.development.js', | ||
'react-dom': '/react-dom@16.7.0/umd/react-dom.development.js' | ||
react: '/react@16.8.6/umd/react.development.js', | ||
'react-dom': '/react-dom@16.8.6/umd/react-dom.development.js' | ||
}; | ||
|
||
export default async function serveAutoIndexPage(req, res) { | ||
export default async function serveBrowsePage(req, res) { | ||
const availableVersions = await getAvailableVersions(req.packageName); | ||
const data = { | ||
packageName: req.packageName, | ||
packageVersion: req.packageVersion, | ||
availableVersions: availableVersions, | ||
filename: req.filename, | ||
entry: req.entry, | ||
entries: req.entries | ||
target: req.browseTarget | ||
}; | ||
const content = createHTML(renderToString(createElement(AutoIndexApp, data))); | ||
const elements = getScripts('autoIndex', 'iife', globalURLs); | ||
const content = createHTML(renderToString(createElement(BrowseApp, data))); | ||
const elements = getScripts('browse', 'iife', globalURLs); | ||
|
||
const html = | ||
doctype + | ||
|
@@ -49,7 +47,7 @@ export default async function serveAutoIndexPage(req, res) { | |
res | ||
.set({ | ||
'Cache-Control': 'public, max-age=14400', // 4 hours | ||
'Cache-Tag': 'auto-index' | ||
'Cache-Tag': 'browse' | ||
}) | ||
.send(html); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import path from 'path'; | ||
import gunzip from 'gunzip-maybe'; | ||
import tar from 'tar-stream'; | ||
|
||
import bufferStream from '../utils/bufferStream.js'; | ||
import getContentType from '../utils/getContentType.js'; | ||
import getIntegrity from '../utils/getIntegrity.js'; | ||
import { getPackage } from '../utils/npm.js'; | ||
import serveBrowsePage from './serveBrowsePage.js'; | ||
|
||
async function findMatchingEntries(stream, filename) { | ||
// filename = /some/dir/name | ||
return new Promise((accept, reject) => { | ||
const entries = {}; | ||
|
||
stream | ||
.pipe(gunzip()) | ||
.pipe(tar.extract()) | ||
.on('error', reject) | ||
.on('entry', async (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. | ||
path: header.name.replace(/^[^/]+/, ''), | ||
type: header.type | ||
}; | ||
|
||
// Dynamically create "directory" entries for all subdirectories | ||
// in this entry's path. Some tarballs omit directory entries for | ||
// some reason, so this is the "brute force" method. | ||
let dir = path.dirname(entry.path); | ||
while (dir !== '/') { | ||
if (!entries[dir] && path.dirname(dir) === filename) { | ||
entries[dir] = { path: dir, type: 'directory' }; | ||
} | ||
dir = path.dirname(dir); | ||
} | ||
|
||
// Ignore non-files and files that aren't in this directory. | ||
if (entry.type !== 'file' || path.dirname(entry.path) !== filename) { | ||
stream.resume(); | ||
stream.on('end', next); | ||
return; | ||
} | ||
|
||
const content = await bufferStream(stream); | ||
|
||
entry.contentType = getContentType(entry.path); | ||
entry.integrity = getIntegrity(content); | ||
entry.size = content.length; | ||
|
||
entries[entry.path] = entry; | ||
|
||
next(); | ||
}) | ||
.on('finish', () => { | ||
accept(entries); | ||
}); | ||
}); | ||
} | ||
|
||
export default async function serveDirectoryBrowser(req, res) { | ||
const stream = await getPackage(req.packageName, req.packageVersion); | ||
|
||
const filename = req.filename.slice(0, -1) || '/'; | ||
const entries = await findMatchingEntries(stream, filename); | ||
|
||
if (Object.keys(entries).length === 0) { | ||
return res.status(404).send(`Not found: ${req.packageSpec}${req.filename}`); | ||
} | ||
|
||
req.browseTarget = { | ||
path: filename, | ||
type: 'directory', | ||
details: entries | ||
}; | ||
|
||
serveBrowsePage(req, res); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import path from 'path'; | ||
import gunzip from 'gunzip-maybe'; | ||
import tar from 'tar-stream'; | ||
|
||
import bufferStream from '../utils/bufferStream.js'; | ||
import getContentType from '../utils/getContentType.js'; | ||
import getIntegrity from '../utils/getIntegrity.js'; | ||
import { getPackage } from '../utils/npm.js'; | ||
|
||
async function findMatchingEntries(stream, filename) { | ||
// filename = /some/dir/name | ||
return new Promise((accept, reject) => { | ||
const entries = {}; | ||
|
||
entries[filename] = { path: filename, type: 'directory' }; | ||
|
||
stream | ||
.pipe(gunzip()) | ||
.pipe(tar.extract()) | ||
.on('error', reject) | ||
.on('entry', async (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. | ||
path: header.name.replace(/^[^/]+/, ''), | ||
type: header.type | ||
}; | ||
|
||
// Dynamically create "directory" entries for all subdirectories | ||
// in this entry's path. Some tarballs omit directory entries for | ||
// some reason, so this is the "brute force" method. | ||
let dir = path.dirname(entry.path); | ||
while (dir !== '/') { | ||
if (!entries[dir] && dir.startsWith(filename)) { | ||
entries[dir] = { path: dir, type: 'directory' }; | ||
} | ||
dir = path.dirname(dir); | ||
} | ||
|
||
// Ignore non-files and files that don't match the prefix. | ||
if (entry.type !== 'file' || !entry.path.startsWith(filename)) { | ||
stream.resume(); | ||
stream.on('end', next); | ||
return; | ||
} | ||
|
||
const content = await bufferStream(stream); | ||
|
||
entry.contentType = getContentType(entry.path); | ||
entry.integrity = getIntegrity(content); | ||
entry.lastModified = header.mtime.toUTCString(); | ||
entry.size = content.length; | ||
|
||
entries[entry.path] = entry; | ||
|
||
next(); | ||
}) | ||
.on('finish', () => { | ||
accept(entries); | ||
}); | ||
}); | ||
} | ||
|
||
function getMatchingEntries(entry, entries) { | ||
return Object.keys(entries) | ||
.filter(key => entry.path !== key && path.dirname(key) === entry.path) | ||
.map(key => entries[key]); | ||
} | ||
|
||
function getMetadata(entry, entries) { | ||
const metadata = { path: entry.path, type: entry.type }; | ||
|
||
if (entry.type === 'file') { | ||
metadata.contentType = entry.contentType; | ||
metadata.integrity = entry.integrity; | ||
metadata.lastModified = entry.lastModified; | ||
metadata.size = entry.size; | ||
} else if (entry.type === 'directory') { | ||
metadata.files = getMatchingEntries(entry, entries).map(e => | ||
getMetadata(e, entries) | ||
); | ||
} | ||
|
||
return metadata; | ||
} | ||
|
||
export default async function serveDirectoryMetadata(req, res) { | ||
const stream = await getPackage(req.packageName, req.packageVersion); | ||
|
||
const filename = req.filename.slice(0, -1) || '/'; | ||
const entries = await findMatchingEntries(stream, filename); | ||
const metadata = getMetadata(entries[filename], entries); | ||
|
||
res.send(metadata); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,24 @@ | ||
import serveAutoIndexPage from './serveAutoIndexPage.js'; | ||
import serveMetadata from './serveMetadata.js'; | ||
import serveModule from './serveModule.js'; | ||
import serveStaticFile from './serveStaticFile.js'; | ||
import path from 'path'; | ||
import etag from 'etag'; | ||
|
||
/** | ||
* Send the file, JSON metadata, or HTML directory listing. | ||
*/ | ||
export default function serveFile(req, res) { | ||
if (req.query.meta != null) { | ||
return serveMetadata(req, res); | ||
} | ||
import getContentTypeHeader from '../utils/getContentTypeHeader.js'; | ||
|
||
if (req.entry.type === 'directory') { | ||
return serveAutoIndexPage(req, res); | ||
} | ||
export default function serveFile(req, res) { | ||
const tags = ['file']; | ||
|
||
if (req.query.module != null) { | ||
return serveModule(req, res); | ||
const ext = path.extname(req.entry.path).substr(1); | ||
if (ext) { | ||
tags.push(`${ext}-file`); | ||
} | ||
|
||
serveStaticFile(req, res); | ||
res | ||
.set({ | ||
'Content-Type': getContentTypeHeader(req.entry.contentType), | ||
'Content-Length': req.entry.size, | ||
'Cache-Control': 'public, max-age=31536000', // 1 year | ||
'Last-Modified': req.entry.lastModified, | ||
ETag: etag(req.entry.content), | ||
'Cache-Tag': tags.join(', ') | ||
}) | ||
.send(req.entry.content); | ||
} |
Oops, something went wrong.