diff --git a/modules/__tests__/directoryIndex-test.js b/modules/__tests__/browseDirectory-test.js similarity index 70% rename from modules/__tests__/directoryIndex-test.js rename to modules/__tests__/browseDirectory-test.js index 1d753f2..c540374 100644 --- a/modules/__tests__/directoryIndex-test.js +++ b/modules/__tests__/browseDirectory-test.js @@ -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('/react@16.8.0/umd/') + .get('/browse/react@16.8.0/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('/react@16.8.0/not-here/') + .get('/browse/react@16.8.0/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(); }); }); diff --git a/modules/__tests__/browseFile-test.js b/modules/__tests__/browseFile-test.js new file mode 100644 index 0000000..b6c88cc --- /dev/null +++ b/modules/__tests__/browseFile-test.js @@ -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/react@16.8.0/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/react@16.8.0/not-here.js') + .end((err, res) => { + expect(res.statusCode).toBe(404); + expect(res.headers['content-type']).toMatch(/\btext\/html\b/); + done(); + }); + }); + }); +}); diff --git a/modules/__tests__/legacyURLs-test.js b/modules/__tests__/legacyURLs-test.js index 90658fd..8342887 100644 --- a/modules/__tests__/legacyURLs-test.js +++ b/modules/__tests__/legacyURLs-test.js @@ -27,4 +27,14 @@ describe('Legacy URLs', () => { done(); }); }); + + it('redirect */ to /browse/*/', done => { + request(server) + .get('/react@16.8.0/umd/') + .end((err, res) => { + expect(res.statusCode).toBe(302); + expect(res.headers.location).toEqual('/browse/react@16.8.0/umd/'); + done(); + }); + }); }); diff --git a/modules/actions/serveAutoIndexPage.js b/modules/actions/serveBrowsePage.js similarity index 58% rename from modules/actions/serveAutoIndexPage.js rename to modules/actions/serveBrowsePage.js index e5e7b8f..de720c0 100644 --- a/modules/actions/serveAutoIndexPage.js +++ b/modules/actions/serveBrowsePage.js @@ -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 = ''; const globalURLs = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'staging' ? { '@emotion/core': '/@emotion/core@10.0.6/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/core@10.0.6/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); } diff --git a/modules/actions/serveDirectoryBrowser.js b/modules/actions/serveDirectoryBrowser.js new file mode 100644 index 0000000..557f41a --- /dev/null +++ b/modules/actions/serveDirectoryBrowser.js @@ -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); +} diff --git a/modules/actions/serveDirectoryMetadata.js b/modules/actions/serveDirectoryMetadata.js new file mode 100644 index 0000000..b1a589a --- /dev/null +++ b/modules/actions/serveDirectoryMetadata.js @@ -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); +} diff --git a/modules/actions/serveFile.js b/modules/actions/serveFile.js index 3d0c52c..7551b00 100644 --- a/modules/actions/serveFile.js +++ b/modules/actions/serveFile.js @@ -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'; + +import getContentTypeHeader from '../utils/getContentTypeHeader.js'; -/** - * Send the file, JSON metadata, or HTML directory listing. - */ export default function serveFile(req, res) { - if (req.query.meta != null) { - return serveMetadata(req, res); + const tags = ['file']; + + const ext = path.extname(req.entry.path).substr(1); + if (ext) { + tags.push(`${ext}-file`); } - if (req.entry.type === 'directory') { - return serveAutoIndexPage(req, res); - } - - if (req.query.module != null) { - return serveModule(req, res); - } - - 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); } diff --git a/modules/actions/serveFileBrowser.js b/modules/actions/serveFileBrowser.js new file mode 100644 index 0000000..a63a033 --- /dev/null +++ b/modules/actions/serveFileBrowser.js @@ -0,0 +1,84 @@ +import gunzip from 'gunzip-maybe'; +import tar from 'tar-stream'; + +import bufferStream from '../utils/bufferStream.js'; +import createDataURI from '../utils/createDataURI.js'; +import getContentType from '../utils/getContentType.js'; +import getIntegrity from '../utils/getIntegrity.js'; +import { getPackage } from '../utils/npm.js'; +import getHighlights from '../utils/getHighlights.js'; +import getLanguageName from '../utils/getLanguageName.js'; + +import serveBrowsePage from './serveBrowsePage.js'; + +async function findEntry(stream, filename) { + // filename = /some/file/name.js + return new Promise((accept, reject) => { + let foundEntry = null; + + 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 + }; + + // Ignore non-files and files that don't match the name. + if (entry.type !== 'file' || entry.path !== filename) { + stream.resume(); + stream.on('end', next); + return; + } + + entry.content = await bufferStream(stream); + foundEntry = entry; + + next(); + }) + .on('finish', () => { + accept(foundEntry); + }); + }); +} + +export default async function serveFileBrowser(req, res) { + const stream = await getPackage(req.packageName, req.packageVersion); + const entry = await findEntry(stream, req.filename); + + if (!entry) { + return res.status(404).send(`Not found: ${req.packageSpec}${req.filename}`); + } + + const details = { + contentType: getContentType(entry.path), + integrity: getIntegrity(entry.content), + language: getLanguageName(entry.path), + size: entry.content.length + }; + + if (/^image\//.test(details.contentType)) { + details.uri = createDataURI(details.contentType, entry.content); + details.highlights = null; + } else { + details.uri = null; + details.highlights = getHighlights( + entry.content.toString('utf8'), + entry.path + ); + } + + req.browseTarget = { + path: req.filename, + type: 'file', + details + }; + + serveBrowsePage(req, res); +} diff --git a/modules/actions/serveFileMetadata.js b/modules/actions/serveFileMetadata.js new file mode 100644 index 0000000..f54c1bc --- /dev/null +++ b/modules/actions/serveFileMetadata.js @@ -0,0 +1,61 @@ +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 findEntry(stream, filename) { + // filename = /some/file/name.js + return new Promise((accept, reject) => { + let foundEntry = null; + + 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 + }; + + // Ignore non-files and files that don't match the name. + if (entry.type !== 'file' || 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.lastModified = header.mtime.toUTCString(); + entry.size = content.length; + + foundEntry = entry; + + next(); + }) + .on('finish', () => { + accept(foundEntry); + }); + }); +} + +export default async function serveFileMetadata(req, res) { + const stream = await getPackage(req.packageName, req.packageVersion); + const entry = await findEntry(stream, req.filename); + + if (!entry) { + // TODO: 404 + } + + res.send(entry); +} diff --git a/modules/actions/serveMainPage.js b/modules/actions/serveMainPage.js index a292195..783bec5 100644 --- a/modules/actions/serveMainPage.js +++ b/modules/actions/serveMainPage.js @@ -1,23 +1,22 @@ import { renderToString, renderToStaticMarkup } from 'react-dom/server'; import MainApp from '../client/main/App.js'; - -import MainTemplate from './utils/MainTemplate.js'; -import getScripts from './utils/getScripts.js'; -import { createElement, createHTML } from './utils/markupHelpers.js'; +import MainTemplate from '../templates/MainTemplate.js'; +import getScripts from '../utils/getScripts.js'; +import { createElement, createHTML } from '../utils/markup.js'; const doctype = ''; const globalURLs = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'staging' ? { '@emotion/core': '/@emotion/core@10.0.6/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/core@10.0.6/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 function serveMainPage(req, res) { diff --git a/modules/actions/serveMetadata.js b/modules/actions/serveMetadata.js deleted file mode 100644 index 414d287..0000000 --- a/modules/actions/serveMetadata.js +++ /dev/null @@ -1,42 +0,0 @@ -import path from 'path'; - -function getMatchingEntries(entry, entries) { - const dirname = entry.name || '.'; - - return Object.keys(entries) - .filter(name => entry.name !== name && path.dirname(name) === dirname) - .map(name => entries[name]); -} - -const leadingSlashes = /^\/*/; - -function getMetadata(entry, entries) { - const metadata = { - path: entry.name.replace(leadingSlashes, '/'), - 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 function serveMetadata(req, res) { - const metadata = getMetadata(req.entry, req.entries); - - res - .set({ - 'Cache-Control': 'public, max-age=31536000', // 1 year - 'Cache-Tag': 'meta' - }) - .send(metadata); -} diff --git a/modules/actions/serveModule.js b/modules/actions/serveModule.js index 10c9039..819a298 100644 --- a/modules/actions/serveModule.js +++ b/modules/actions/serveModule.js @@ -1,7 +1,7 @@ import serveHTMLModule from './serveHTMLModule.js'; import serveJavaScriptModule from './serveJavaScriptModule.js'; -export default function serveModule(req, res) { +export default async function serveModule(req, res) { if (req.entry.contentType === 'application/javascript') { return serveJavaScriptModule(req, res); } diff --git a/modules/actions/serveStaticFile.js b/modules/actions/serveStaticFile.js deleted file mode 100644 index a7ddcdc..0000000 --- a/modules/actions/serveStaticFile.js +++ /dev/null @@ -1,24 +0,0 @@ -import path from 'path'; -import etag from 'etag'; - -import getContentTypeHeader from '../utils/getContentTypeHeader.js'; - -export default function serveStaticFile(req, res) { - const tags = ['file']; - - const ext = path.extname(req.entry.name).substr(1); - if (ext) { - tags.push(`${ext}-file`); - } - - res - .set({ - 'Content-Length': req.entry.size, - 'Content-Type': getContentTypeHeader(req.entry.contentType), - '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); -} diff --git a/modules/actions/serveStats.js b/modules/actions/serveStats.js index f773a30..e18936a 100644 --- a/modules/actions/serveStats.js +++ b/modules/actions/serveStats.js @@ -1,6 +1,6 @@ import { subDays, startOfDay } from 'date-fns'; -import getStats from './utils/getStats.js'; +import getStats from '../utils/getStats.js'; export default function serveStats(req, res) { let since, until; diff --git a/modules/client/.eslintrc b/modules/client/.eslintrc index b1e8fbe..0f0d029 100644 --- a/modules/client/.eslintrc +++ b/modules/client/.eslintrc @@ -2,12 +2,12 @@ "env": { "browser": true }, - "plugins": [ - "react" - ], + "plugins": ["react", "react-hooks"], "rules": { "react/jsx-uses-react": "error", - "react/jsx-uses-vars": "error" + "react/jsx-uses-vars": "error", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn" }, "settings": { "react": { diff --git a/modules/client/autoIndex/App.js b/modules/client/autoIndex/App.js deleted file mode 100644 index 6459d1a..0000000 --- a/modules/client/autoIndex/App.js +++ /dev/null @@ -1,91 +0,0 @@ -/** @jsx jsx */ -import PropTypes from 'prop-types'; -import { Global, css, jsx } from '@emotion/core'; - -import DirectoryListing from './DirectoryListing.js'; - -const globalStyles = css` - body { - font-size: 14px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, - Helvetica, Arial, sans-serif; - line-height: 1.7; - padding: 0px 10px 5px; - color: #000000; - } -`; - -export default function App({ - packageName, - packageVersion, - availableVersions = [], - filename, - entry, - entries -}) { - function handleChange(event) { - window.location.href = window.location.href.replace( - '@' + packageVersion, - '@' + event.target.value - ); - } - - return ( -
- - -
-

- Index of /{packageName}@{packageVersion} - {filename} -

- -
- Version:{' '} - -
-
- -
- - - -
- -
- {packageName}@{packageVersion} -
-
- ); -} - -if (process.env.NODE_ENV !== 'production') { - const entryType = PropTypes.object; - - App.propTypes = { - packageName: PropTypes.string.isRequired, - packageVersion: PropTypes.string.isRequired, - availableVersions: PropTypes.arrayOf(PropTypes.string), - filename: PropTypes.string.isRequired, - entry: entryType.isRequired, - entries: PropTypes.objectOf(entryType).isRequired - }; -} diff --git a/modules/client/autoIndex/DirectoryListing.js b/modules/client/autoIndex/DirectoryListing.js deleted file mode 100644 index 1558ae1..0000000 --- a/modules/client/autoIndex/DirectoryListing.js +++ /dev/null @@ -1,142 +0,0 @@ -/** @jsx jsx */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { jsx } from '@emotion/core'; -import formatBytes from 'pretty-bytes'; -import sortBy from 'sort-by'; - -function getDirname(name) { - return ( - name - .split('/') - .slice(0, -1) - .join('/') || '.' - ); -} - -function getMatchingEntries(entry, entries) { - const dirname = entry.name || '.'; - - return Object.keys(entries) - .filter(name => entry.name !== name && getDirname(name) === dirname) - .map(name => entries[name]); -} - -function getRelativeName(base, name) { - return base.length ? name.substr(base.length + 1) : name; -} - -const styles = { - tableHead: { - textAlign: 'left', - padding: '0.5em 1em' - }, - tableCell: { - padding: '0.5em 1em' - }, - evenRow: { - backgroundColor: '#eee' - } -}; - -export default function DirectoryListing({ filename, entry, entries }) { - const rows = []; - - if (filename !== '/') { - rows.push( - - - - .. - - - - - - - - - - ); - } - - const matchingEntries = getMatchingEntries(entry, entries); - - matchingEntries - .filter(({ type }) => type === 'directory') - .sort(sortBy('name')) - .forEach(({ name }) => { - const relName = getRelativeName(entry.name, name); - const href = relName + '/'; - - rows.push( - - - - {href} - - - - - - - - - - ); - }); - - matchingEntries - .filter(({ type }) => type === 'file') - .sort(sortBy('name')) - .forEach(({ name, size, contentType, lastModified }) => { - const relName = getRelativeName(entry.name, name); - - rows.push( - - - - {relName} - - - {contentType} - {formatBytes(size)} - {lastModified} - - ); - }); - - return ( -
- - - - - - - - - - - {rows.map((row, index) => - React.cloneElement(row, { - style: index % 2 ? undefined : styles.evenRow - }) - )} - -
NameTypeSizeLast Modified
-
- ); -} - -if (process.env.NODE_ENV !== 'production') { - const entryType = PropTypes.shape({ - name: PropTypes.string.isRequired - }); - - DirectoryListing.propTypes = { - filename: PropTypes.string.isRequired, - entry: entryType.isRequired, - entries: PropTypes.objectOf(entryType).isRequired - }; -} diff --git a/modules/client/autoIndex.js b/modules/client/browse.js similarity index 81% rename from modules/client/autoIndex.js rename to modules/client/browse.js index d706adb..3e5bd86 100644 --- a/modules/client/autoIndex.js +++ b/modules/client/browse.js @@ -1,7 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import App from './autoIndex/App.js'; +import App from './browse/App.js'; const props = window.__DATA__ || {}; diff --git a/modules/client/browse/App.js b/modules/client/browse/App.js new file mode 100644 index 0000000..04541c2 --- /dev/null +++ b/modules/client/browse/App.js @@ -0,0 +1,356 @@ +/** @jsx jsx */ +import { Global, css, jsx } from '@emotion/core'; +import { Fragment } from 'react'; +import PropTypes from 'prop-types'; + +import { fontSans, fontMono } from '../utils/style.js'; + +import { PackageInfoProvider } from './PackageInfo.js'; +import DirectoryViewer from './DirectoryViewer.js'; +import FileViewer from './FileViewer.js'; +import { TwitterIcon, GitHubIcon } from './Icons.js'; + +import SelectDownArrow from './images/SelectDownArrow.png'; + +const globalStyles = css` + html { + box-sizing: border-box; + } + *, + *:before, + *:after { + box-sizing: inherit; + } + + html, + body, + #root { + height: 100%; + margin: 0; + } + + body { + ${fontSans} + font-size: 16px; + line-height: 1.5; + background: white; + color: black; + } + + code { + ${fontMono} + } + + th, + td { + padding: 0; + } + + select { + font-size: inherit; + } + + #root { + display: flex; + flex-direction: column; + } +`; + +// Adapted from https://github.com/highlightjs/highlight.js/blob/master/src/styles/atom-one-light.css +const lightCodeStyles = css` + .code-listing { + background: #fbfdff; + color: #383a42; + } + .code-comment, + .code-quote { + color: #a0a1a7; + font-style: italic; + } + .code-doctag, + .code-keyword, + .code-link, + .code-formula { + color: #a626a4; + } + .code-section, + .code-name, + .code-selector-tag, + .code-deletion, + .code-subst { + color: #e45649; + } + .code-literal { + color: #0184bb; + } + .code-string, + .code-regexp, + .code-addition, + .code-attribute, + .code-meta-string { + color: #50a14f; + } + .code-built_in, + .code-class .code-title { + color: #c18401; + } + .code-attr, + .code-variable, + .code-template-variable, + .code-type, + .code-selector-class, + .code-selector-attr, + .code-selector-pseudo, + .code-number { + color: #986801; + } + .code-symbol, + .code-bullet, + .code-meta, + .code-selector-id, + .code-title { + color: #4078f2; + } + .code-emphasis { + font-style: italic; + } + .code-strong { + font-weight: bold; + } +`; + +const linkStyle = { + color: '#0076ff', + textDecoration: 'none', + ':hover': { + textDecoration: 'underline' + } +}; + +export default function App({ + packageName, + packageVersion, + availableVersions = [], + filename, + target +}) { + function handleChange(event) { + window.location.href = window.location.href.replace( + '@' + packageVersion, + '@' + event.target.value + ); + } + + const breadcrumbs = []; + + if (filename === '/') { + breadcrumbs.push(packageName); + } else { + let url = `/browse/${packageName}@${packageVersion}`; + + breadcrumbs.push( + + {packageName} + + ); + + const segments = filename + .replace(/^\/+/, '') + .replace(/\/+$/, '') + .split('/'); + + const lastSegment = segments.pop(); + + segments.forEach(segment => { + url += `/${segment}`; + breadcrumbs.push( + + {segment} + + ); + }); + + breadcrumbs.push(lastSegment); + } + + // TODO: Provide a user pref to go full width? + const maxContentWidth = 940; + + return ( + + + + + +
+
+
+

+ + UNPKG + +

+ {/* + + */} +
+ +
+

+ +

+
+ {' '} + +
+
+
+ +
+ {target.type === 'directory' ? ( + + ) : target.type === 'file' ? ( + + ) : null} +
+
+ + +
+
+ ); +} + +if (process.env.NODE_ENV !== 'production') { + const targetType = PropTypes.shape({ + path: PropTypes.string.isRequired, + type: PropTypes.oneOf(['directory', 'file']).isRequired, + details: PropTypes.object.isRequired + }); + + App.propTypes = { + packageName: PropTypes.string.isRequired, + packageVersion: PropTypes.string.isRequired, + availableVersions: PropTypes.arrayOf(PropTypes.string), + filename: PropTypes.string.isRequired, + target: targetType.isRequired + }; +} diff --git a/modules/client/browse/DirectoryViewer.js b/modules/client/browse/DirectoryViewer.js new file mode 100644 index 0000000..6b66b3c --- /dev/null +++ b/modules/client/browse/DirectoryViewer.js @@ -0,0 +1,182 @@ +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import PropTypes from 'prop-types'; +import VisuallyHidden from '@reach/visually-hidden'; +import sortBy from 'sort-by'; + +import { formatBytes } from '../utils/format.js'; + +import { DirectoryIcon, CodeFileIcon } from './Icons.js'; + +const linkStyle = { + color: '#0076ff', + textDecoration: 'none', + ':hover': { + textDecoration: 'underline' + } +}; + +const tableCellStyle = { + paddingTop: 6, + paddingRight: 3, + paddingBottom: 6, + paddingLeft: 3, + borderTop: '1px solid #eaecef' +}; + +const iconCellStyle = { + ...tableCellStyle, + color: '#424242', + width: 17, + paddingRight: 2, + paddingLeft: 10, + '@media (max-width: 700px)': { + paddingLeft: 20 + } +}; + +const typeCellStyle = { + ...tableCellStyle, + textAlign: 'right', + paddingRight: 10, + '@media (max-width: 700px)': { + paddingRight: 20 + } +}; + +function getRelName(path, base) { + return path.substr(base.length > 1 ? base.length + 1 : 1); +} + +export default function DirectoryViewer({ path, details: entries }) { + const rows = []; + + if (path !== '/') { + rows.push( + + + + + .. + + + + + + ); + } + + const { subdirs, files } = Object.keys(entries).reduce( + (memo, key) => { + const { subdirs, files } = memo; + const entry = entries[key]; + + if (entry.type === 'directory') { + subdirs.push(entry); + } else if (entry.type === 'file') { + files.push(entry); + } + + return memo; + }, + { subdirs: [], files: [] } + ); + + subdirs.sort(sortBy('path')).forEach(({ path: dirname }) => { + const relName = getRelName(dirname, path); + const href = relName + '/'; + + rows.push( + + + + + + + {relName} + + + - + - + + ); + }); + + files + .sort(sortBy('path')) + .forEach(({ path: filename, size, contentType }) => { + const relName = getRelName(filename, path); + const href = relName; + + rows.push( + + + + + + + {relName} + + + {formatBytes(size)} + {contentType} + + ); + }); + + return ( +
+ + + + + + + + + + {rows} +
+ Icon + + Name + + Size + + Content Type +
+
+ ); +} + +if (process.env.NODE_ENV !== 'production') { + DirectoryViewer.propTypes = { + path: PropTypes.string.isRequired, + details: PropTypes.objectOf( + PropTypes.shape({ + path: PropTypes.string.isRequired, + type: PropTypes.oneOf(['directory', 'file']).isRequired, + contentType: PropTypes.string, // file only + integrity: PropTypes.string, // file only + size: PropTypes.number // file only + }) + ).isRequired + }; +} diff --git a/modules/client/browse/FileViewer.js b/modules/client/browse/FileViewer.js new file mode 100644 index 0000000..8f60288 --- /dev/null +++ b/modules/client/browse/FileViewer.js @@ -0,0 +1,212 @@ +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import PropTypes from 'prop-types'; + +import { formatBytes } from '../utils/format.js'; +import { createHTML } from '../utils/markup.js'; + +import { usePackageInfo } from './PackageInfo.js'; + +function getBasename(path) { + const segments = path.split('/'); + return segments[segments.length - 1]; +} + +function ImageViewer({ path, uri }) { + return ( +
+ +
+ ); +} + +function CodeListing({ highlights }) { + const lines = highlights.slice(0); + const hasTrailingNewline = lines.length && lines[lines.length - 1] === ''; + if (hasTrailingNewline) { + lines.pop(); + } + + return ( +
+ + + {lines.map((line, index) => { + const lineNumber = index + 1; + + return ( + + + + + ); + })} + {!hasTrailingNewline && ( + + + + + )} + +
+ {lineNumber} + + +
+ \ + + No newline at end of file +
+
+ ); +} + +function BinaryViewer() { + return ( +
+

No preview available.

+
+ ); +} + +export default function FileViewer({ path, details }) { + const { packageName, packageVersion } = usePackageInfo(); + const { highlights, uri, language, size } = details; + + const segments = path.split('/'); + const filename = segments[segments.length - 1]; + + return ( +
+
+ {formatBytes(size)} {language}{' '} + + View Raw + +
+ + {highlights ? ( + + ) : uri ? ( + + ) : ( + + )} +
+ ); +} + +if (process.env.NODE_ENV !== 'production') { + FileViewer.propTypes = { + path: PropTypes.string.isRequired, + details: PropTypes.shape({ + contentType: PropTypes.string.isRequired, + highlights: PropTypes.arrayOf(PropTypes.string), // code + uri: PropTypes.string, // images + integrity: PropTypes.string.isRequired, + language: PropTypes.string.isRequired, + size: PropTypes.number.isRequired + }).isRequired + }; +} diff --git a/modules/client/browse/Icons.js b/modules/client/browse/Icons.js new file mode 100644 index 0000000..664b3d7 --- /dev/null +++ b/modules/client/browse/Icons.js @@ -0,0 +1,24 @@ +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import { GoFileDirectory, GoFile } from 'react-icons/go'; +import { FaTwitter, FaGithub } from 'react-icons/fa'; + +function createIcon(Type, { css, ...rest }) { + return ; +} + +export function DirectoryIcon(props) { + return createIcon(GoFileDirectory, props); +} + +export function CodeFileIcon(props) { + return createIcon(GoFile, props); +} + +export function TwitterIcon(props) { + return createIcon(FaTwitter, props); +} + +export function GitHubIcon(props) { + return createIcon(FaGithub, props); +} diff --git a/modules/client/browse/PackageInfo.js b/modules/client/browse/PackageInfo.js new file mode 100644 index 0000000..36e049d --- /dev/null +++ b/modules/client/browse/PackageInfo.js @@ -0,0 +1,11 @@ +import React, { createContext, useContext } from 'react'; + +const Context = createContext(); + +export function PackageInfoProvider({ children, ...rest }) { + return ; +} + +export function usePackageInfo() { + return useContext(Context); +} diff --git a/modules/client/browse/images/DownArrow.png b/modules/client/browse/images/DownArrow.png new file mode 100644 index 0000000..3212bc9 Binary files /dev/null and b/modules/client/browse/images/DownArrow.png differ diff --git a/modules/client/browse/images/SelectDownArrow.png b/modules/client/browse/images/SelectDownArrow.png new file mode 100644 index 0000000..64913ae Binary files /dev/null and b/modules/client/browse/images/SelectDownArrow.png differ diff --git a/modules/client/main/App.js b/modules/client/main/App.js index c6ceabb..2063707 100644 --- a/modules/client/main/App.js +++ b/modules/client/main/App.js @@ -1,40 +1,45 @@ /** @jsx jsx */ -import React from 'react'; -import PropTypes from 'prop-types'; import { Global, css, jsx } from '@emotion/core'; +import { Fragment, useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; import formatBytes from 'pretty-bytes'; import formatDate from 'date-fns/format'; import parseDate from 'date-fns/parse'; -import formatNumber from '../utils/formatNumber.js'; -import formatPercent from '../utils/formatPercent.js'; +import { formatNumber, formatPercent } from '../utils/format.js'; +import { fontSans, fontMono } from '../utils/style.js'; -import cloudflareLogo from './CloudflareLogo.png'; -import angularLogo from './AngularLogo.png'; -import googleCloudLogo from './GoogleCloudLogo.png'; +import { TwitterIcon, GitHubIcon } from './Icons.js'; +import CloudflareLogo from './images/CloudflareLogo.png'; +import AngularLogo from './images/AngularLogo.png'; const globalStyles = css` + html { + box-sizing: border-box; + } + *, + *:before, + *:after { + box-sizing: inherit; + } + + html, + body, + #root { + height: 100%; + margin: 0; + } + body { - font-size: 14px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, - Helvetica, Arial, sans-serif; - line-height: 1.7; - padding: 5px 20px; + ${fontSans} + font-size: 16px; + line-height: 1.5; + background: white; color: black; } - @media (min-width: 800px) { - body { - padding: 40px 20px 120px; - } - } - - a:link { - color: blue; - text-decoration: none; - } - a:visited { - color: rebeccapurple; + code { + ${fontMono} } dd, @@ -42,23 +47,18 @@ const globalStyles = css` margin-left: 0; padding-left: 25px; } + + #root { + display: flex; + flex-direction: column; + } `; -const styles = { - heading: { - margin: '0.8em 0', - textTransform: 'uppercase', - textAlign: 'center', - fontSize: '5em' - }, - subheading: { - fontSize: '1.6em' - }, - example: { - textAlign: 'center', - backgroundColor: '#eee', - margin: '2em 0', - padding: '5px 0' +const linkStyle = { + color: '#0076ff', + textDecoration: 'none', + ':hover': { + textDecoration: 'underline' } }; @@ -90,59 +90,73 @@ function Stats({ data }) { ); } -export default class App extends React.Component { - constructor(props) { - super(props); +export default function App() { + const [stats, setStats] = useState( + typeof window === 'object' && + window.localStorage && + window.localStorage.savedStats + ? JSON.parse(window.localStorage.savedStats) + : null + ); + const hasStats = !!(stats && !stats.error); + const stringStats = JSON.stringify(stats); - this.state = { stats: null }; + useEffect(() => { + window.localStorage.savedStats = stringStats; + }, [stringStats]); - if (typeof window === 'object' && window.localStorage) { - const savedStats = window.localStorage.savedStats; - - if (savedStats) { - this.state.stats = JSON.parse(savedStats); - } - - window.onbeforeunload = () => { - window.localStorage.savedStats = JSON.stringify(this.state.stats); - }; - } - } - - componentDidMount() { - // Refresh latest stats. + useEffect(() => { fetch('/api/stats?period=last-month') .then(res => res.json()) - .then(stats => this.setState({ stats })); - } + .then(setStats); + }, []); - render() { - const { stats } = this.state; - const hasStats = !!(stats && !stats.error); - - return ( -
+ return ( + +
-

unpkg

+

+ unpkg +

- unpkg is a fast, global{' '} - - content delivery network - {' '} - for everything on npm. Use it - to quickly and easily load any file from any package using a URL - like: + unpkg is a fast, global content delivery network for everything on{' '} + + npm + + . Use it to quickly and easily load any file from any package using + a URL like:

-
unpkg.com/:package@:version/:file
+
+ unpkg.com/:package@:version/:file +
{hasStats && }
-

+

Examples

@@ -150,12 +164,20 @@ export default class App extends React.Component {
  • - + unpkg.com/react@16.7.0/umd/react.production.min.js
  • - + unpkg.com/react-dom@16.7.0/umd/react-dom.production.min.js
  • @@ -163,20 +185,41 @@ export default class App extends React.Component {

    You may also use a{' '} - semver range or a{' '} - tag instead of a - fixed version number, or omit the version/tag entirely to use the{' '} - latest tag. + + semver range + {' '} + or a{' '} + + tag + {' '} + instead of a fixed version number, or omit the version/tag entirely to + use the latest tag.

    • - + unpkg.com/react@^16/umd/react.production.min.js
    • - + unpkg.com/react/umd/react.production.min.js
    • @@ -190,10 +233,14 @@ export default class App extends React.Component { @@ -204,14 +251,26 @@ export default class App extends React.Component { -

      +

      Query Parameters

      @@ -229,7 +288,11 @@ export default class App extends React.Component {
      Expands all{' '} - + “bare” import specifiers {' '} in JavaScript modules to unpkg URLs. This feature is{' '} @@ -237,7 +300,7 @@ export default class App extends React.Component {
      -

      +

      Cache Behavior

      @@ -255,8 +318,14 @@ export default class App extends React.Component { URLs that do not specify a package version number redirect to one that does. This is the latest version when no version is specified, or the maxSatisfying version when a{' '} - semver version is - given. Redirects are cached for 10 minutes at the CDN, 1 minute in + + semver version + {' '} + is given. Redirects are cached for 10 minutes at the CDN, 1 minute in browsers.

      @@ -267,15 +336,18 @@ export default class App extends React.Component { latest version and redirect them.

      -

      +

      Workflow

      For npm package authors, unpkg relieves the burden of publishing your code to a CDN in addition to the npm registry. All you need to do is - include your UMD build in - your npm package (not your repo, that's different!). + include your{' '} + + UMD + {' '} + build in your npm package (not your repo, that's different!).

      You can do this easily using the following setup:

      @@ -287,7 +359,11 @@ export default class App extends React.Component {
    • Add the umd directory to your{' '} - + files array {' '} in package.json @@ -303,24 +379,50 @@ export default class App extends React.Component { a version available on unpkg as well.

      -

      +

      About

      - unpkg is an open source project - built and maintained by{' '} - Michael Jackson. unpkg is - not affiliated with or supported by npm, Inc. in any way. Please do - not contact npm for help with unpkg. Instead, please reach out to{' '} - @unpkg with any questions or - concerns. + unpkg is an{' '} + + open source + {' '} + project built and maintained by{' '} + + Michael Jackson + + . unpkg is not affiliated with or supported by npm, Inc. in any way. + Please do not contact npm for help with unpkg. Instead, please reach + out to{' '} + + @unpkg + {' '} + with any questions or concerns.

      The unpkg CDN is powered by{' '} - Cloudflare, one of the - world's largest and fastest cloud network platforms.{' '} + + Cloudflare + + , one of the world's largest and fastest cloud network platforms.{' '} {hasStats && ( In the past month, Cloudflare served over{' '} @@ -339,19 +441,27 @@ export default class App extends React.Component { }} > - - + +

The origin servers for unpkg are powered by{' '} - Google Cloud and made possible - by a generous donation from the{' '} - Angular web framework, one of the - world's most popular libraries for building incredible user - experiences on both desktop and mobile. + + Google Cloud + {' '} + and made possible by a generous donation from the{' '} + + Angular web framework + + , one of the world's most popular libraries for building + incredible user experiences on both desktop and mobile.

+
- - - ); - } + + + + ); } if (process.env.NODE_ENV !== 'production') { diff --git a/modules/client/main/GoogleCloudLogo.png b/modules/client/main/GoogleCloudLogo.png deleted file mode 100644 index 7e23dfa..0000000 Binary files a/modules/client/main/GoogleCloudLogo.png and /dev/null differ diff --git a/modules/client/main/Icons.js b/modules/client/main/Icons.js new file mode 100644 index 0000000..e2e15c6 --- /dev/null +++ b/modules/client/main/Icons.js @@ -0,0 +1,15 @@ +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import { FaTwitter, FaGithub } from 'react-icons/fa'; + +function createIcon(Type, { css, ...rest }) { + return ; +} + +export function TwitterIcon(props) { + return createIcon(FaTwitter, props); +} + +export function GitHubIcon(props) { + return createIcon(FaGithub, props); +} diff --git a/modules/client/main/AngularLogo.png b/modules/client/main/images/AngularLogo.png similarity index 100% rename from modules/client/main/AngularLogo.png rename to modules/client/main/images/AngularLogo.png diff --git a/modules/client/main/CloudflareLogo.png b/modules/client/main/images/CloudflareLogo.png similarity index 100% rename from modules/client/main/CloudflareLogo.png rename to modules/client/main/images/CloudflareLogo.png diff --git a/modules/client/utils/format.js b/modules/client/utils/format.js new file mode 100644 index 0000000..177db70 --- /dev/null +++ b/modules/client/utils/format.js @@ -0,0 +1,18 @@ +import formatBytes from 'pretty-bytes'; + +export { formatBytes }; + +export function formatNumber(n) { + const digits = String(n).split(''); + const groups = []; + + while (digits.length) { + groups.unshift(digits.splice(-3).join('')); + } + + return groups.join(','); +} + +export function formatPercent(n, decimals = 1) { + return (n * 100).toPrecision(decimals + 2); +} diff --git a/modules/client/utils/formatNumber.js b/modules/client/utils/formatNumber.js deleted file mode 100644 index 46fcd41..0000000 --- a/modules/client/utils/formatNumber.js +++ /dev/null @@ -1,10 +0,0 @@ -export default function formatNumber(n) { - const digits = String(n).split(''); - const groups = []; - - while (digits.length) { - groups.unshift(digits.splice(-3).join('')); - } - - return groups.join(','); -} diff --git a/modules/client/utils/formatPercent.js b/modules/client/utils/formatPercent.js deleted file mode 100644 index e281005..0000000 --- a/modules/client/utils/formatPercent.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function formatPercent(n, decimals = 1) { - return (n * 100).toPrecision(decimals + 2); -} diff --git a/modules/client/utils/markup.js b/modules/client/utils/markup.js new file mode 100644 index 0000000..6cc875d --- /dev/null +++ b/modules/client/utils/markup.js @@ -0,0 +1,3 @@ +export function createHTML(content) { + return { __html: content }; +} diff --git a/modules/client/utils/style.js b/modules/client/utils/style.js new file mode 100644 index 0000000..ed2ddf6 --- /dev/null +++ b/modules/client/utils/style.js @@ -0,0 +1,24 @@ +export const fontSans = ` +font-family: -apple-system, + BlinkMacSystemFont, + "Segoe UI", + "Roboto", + "Oxygen", + "Ubuntu", + "Cantarell", + "Fira Sans", + "Droid Sans", + "Helvetica Neue", + sans-serif; +`; + +export const fontMono = ` +font-family: Menlo, + Monaco, + Lucida Console, + Liberation Mono, + DejaVu Sans Mono, + Bitstream Vera Sans Mono, + Courier New, + monospace; +`; diff --git a/modules/createServer.js b/modules/createServer.js index 4080e89..f65f38d 100644 --- a/modules/createServer.js +++ b/modules/createServer.js @@ -1,49 +1,148 @@ import express from 'express'; +import serveDirectoryBrowser from './actions/serveDirectoryBrowser.js'; +import serveDirectoryMetadata from './actions/serveDirectoryMetadata.js'; +import serveFileBrowser from './actions/serveFileBrowser.js'; +import serveFileMetadata from './actions/serveFileMetadata.js'; import serveFile from './actions/serveFile.js'; import serveMainPage from './actions/serveMainPage.js'; +import serveModule from './actions/serveModule.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 findEntry from './middleware/findEntry.js'; import logger from './middleware/logger.js'; import redirectLegacyURLs from './middleware/redirectLegacyURLs.js'; import staticFiles from './middleware/staticFiles.js'; +import validateFilename from './middleware/validateFilename.js'; import validatePackageURL from './middleware/validatePackageURL.js'; import validatePackageName from './middleware/validatePackageName.js'; import validateQuery from './middleware/validateQuery.js'; +import validateVersion from './middleware/validateVersion.js'; -export default function createServer() { +function createApp(callback) { 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 - ); - + callback(app); return app; } + +export default function createServer() { + return createApp(app => { + app.disable('x-powered-by'); + app.enable('trust proxy'); + app.enable('strict routing'); + + 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.use( + '/browse', + createApp(app => { + app.enable('strict routing'); + + app.get( + '*/', + validatePackageURL, + validatePackageName, + validateQuery, + validateVersion, + serveDirectoryBrowser + ); + + app.get( + '*', + validatePackageURL, + validatePackageName, + validateQuery, + validateVersion, + serveFileBrowser + ); + }) + ); + + // We need to route in this weird way because Express + // doesn't have a way to route based on query params. + const metadataApp = createApp(app => { + app.enable('strict routing'); + + app.get( + '*/', + validatePackageURL, + validatePackageName, + validateQuery, + validateVersion, + validateFilename, + serveDirectoryMetadata + ); + + app.get( + '*', + validatePackageURL, + validatePackageName, + validateQuery, + validateVersion, + validateFilename, + serveFileMetadata + ); + }); + + app.use((req, res, next) => { + if (req.query.meta != null) { + metadataApp(req, res); + } else { + next(); + } + }); + + const moduleApp = createApp(app => { + app.enable('strict routing'); + + app.get( + '*', + validatePackageURL, + validatePackageName, + validateQuery, + validateVersion, + validateFilename, + findEntry, + serveModule + ); + }); + + app.use((req, res, next) => { + if (req.query.module != null) { + moduleApp(req, res); + } else { + next(); + } + }); + + // Send old */ requests to the new /browse UI. + app.get('*/', (req, res) => { + res.redirect(302, '/browse' + req.url); + }); + + app.get( + '*', + validatePackageURL, + validatePackageName, + validateQuery, + validateVersion, + validateFilename, + findEntry, + serveFile + ); + }); +} diff --git a/modules/middleware/findFile.js b/modules/middleware/findEntry.js similarity index 63% rename from modules/middleware/findFile.js rename to modules/middleware/findEntry.js index 150f427..930d287 100644 --- a/modules/middleware/findFile.js +++ b/modules/middleware/findEntry.js @@ -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(); } diff --git a/modules/middleware/fetchPackage.js b/modules/middleware/validateFilename.js similarity index 63% rename from modules/middleware/fetchPackage.js rename to modules/middleware/validateFilename.js index 333d612..4b08384 100644 --- a/modules/middleware/fetchPackage.js +++ b/modules/middleware/validateFilename.js @@ -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); } diff --git a/modules/middleware/validateVersion.js b/modules/middleware/validateVersion.js new file mode 100644 index 0000000..6c78047 --- /dev/null +++ b/modules/middleware/validateVersion.js @@ -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(); +} diff --git a/modules/actions/utils/MainTemplate.js b/modules/templates/MainTemplate.js similarity index 92% rename from modules/actions/utils/MainTemplate.js rename to modules/templates/MainTemplate.js index 8edc5df..3f09d60 100644 --- a/modules/actions/utils/MainTemplate.js +++ b/modules/templates/MainTemplate.js @@ -1,10 +1,11 @@ import PropTypes from 'prop-types'; +import encodeJSONForScript from '../utils/encodeJSONForScript.js'; import { createElement as e, createHTML as h, createScript as x -} from './markupHelpers.js'; +} from '../utils/markup.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>\')'; @@ -47,7 +48,7 @@ gtag('config', 'UA-140352188-1');`), e('title', null, title), x(promiseShim), x(fetchShim), - data && x(`window.__DATA__ = ${JSON.stringify(data)}`) + data && x(`window.__DATA__ = ${encodeJSONForScript(data)}`) ), e( 'body', diff --git a/modules/utils/__tests__/getContentType-test.js b/modules/utils/__tests__/getContentType-test.js index e878aa6..f9a4c67 100644 --- a/modules/utils/__tests__/getContentType-test.js +++ b/modules/utils/__tests__/getContentType-test.js @@ -1,39 +1,47 @@ 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'); - expect(getContentType('CHANGES')).toBe('text/plain'); - expect(getContentType('LICENSE')).toBe('text/plain'); - expect(getContentType('Makefile')).toBe('text/plain'); - expect(getContentType('PATENTS')).toBe('text/plain'); - expect(getContentType('README')).toBe('text/plain'); -}); +describe('getContentType', () => { + it('returns text/plain for LICENSE|README|CHANGES|AUTHORS|Makefile', () => { + expect(getContentType('AUTHORS')).toBe('text/plain'); + expect(getContentType('CHANGES')).toBe('text/plain'); + expect(getContentType('LICENSE')).toBe('text/plain'); + expect(getContentType('Makefile')).toBe('text/plain'); + expect(getContentType('PATENTS')).toBe('text/plain'); + expect(getContentType('README')).toBe('text/plain'); + }); -it('gets a content type of text/plain for .*rc files', () => { - expect(getContentType('.eslintrc')).toBe('text/plain'); - expect(getContentType('.babelrc')).toBe('text/plain'); - expect(getContentType('.anythingrc')).toBe('text/plain'); -}); + it('returns text/plain for .*rc files', () => { + expect(getContentType('.eslintrc')).toBe('text/plain'); + expect(getContentType('.babelrc')).toBe('text/plain'); + expect(getContentType('.anythingrc')).toBe('text/plain'); + }); -it('gets a content type of text/plain for .git* files', () => { - expect(getContentType('.gitignore')).toBe('text/plain'); - expect(getContentType('.gitanything')).toBe('text/plain'); -}); + it('returns text/plain for .git* files', () => { + expect(getContentType('.gitignore')).toBe('text/plain'); + expect(getContentType('.gitanything')).toBe('text/plain'); + }); -it('gets a content type of text/plain for .*ignore files', () => { - expect(getContentType('.eslintignore')).toBe('text/plain'); - expect(getContentType('.anythingignore')).toBe('text/plain'); -}); + it('returns text/plain for .*ignore files', () => { + expect(getContentType('.eslintignore')).toBe('text/plain'); + expect(getContentType('.anythingignore')).toBe('text/plain'); + }); -it('gets a content type of text/plain for .ts files', () => { - expect(getContentType('app.ts')).toBe('text/plain'); - expect(getContentType('app.d.ts')).toBe('text/plain'); -}); + it('returns text/plain for .ts(x) files', () => { + expect(getContentType('app.ts')).toBe('text/plain'); + expect(getContentType('app.d.ts')).toBe('text/plain'); + expect(getContentType('app.tsx')).toBe('text/plain'); + }); -it('gets a content type of text/plain for .flow files', () => { - expect(getContentType('app.js.flow')).toBe('text/plain'); -}); + it('returns text/plain for .flow files', () => { + expect(getContentType('app.js.flow')).toBe('text/plain'); + }); -it('gets a content type of text/plain for .lock files', () => { - expect(getContentType('yarn.lock')).toBe('text/plain'); + it('returns text/plain for .lock files', () => { + expect(getContentType('yarn.lock')).toBe('text/plain'); + }); + + it('returns application/json for .map files', () => { + expect(getContentType('react.js.map')).toBe('application/json'); + expect(getContentType('react.json.map')).toBe('application/json'); + }); }); diff --git a/modules/utils/__tests__/getLanguageName-test.js b/modules/utils/__tests__/getLanguageName-test.js new file mode 100644 index 0000000..5a0ffc1 --- /dev/null +++ b/modules/utils/__tests__/getLanguageName-test.js @@ -0,0 +1,84 @@ +import getLanguageName from '../getLanguageName.js'; + +describe('getLanguageName', () => { + // Hard-coded overrides + + it('detects Flow files', () => { + expect(getLanguageName('react.flow')).toBe('Flow'); + }); + + it('detects source maps', () => { + expect(getLanguageName('react.map')).toBe('Source Map (JSON)'); + expect(getLanguageName('react.js.map')).toBe('Source Map (JSON)'); + expect(getLanguageName('react.json.map')).toBe('Source Map (JSON)'); + }); + + it('detects TypeScript files', () => { + expect(getLanguageName('react.d.ts')).toBe('TypeScript'); + expect(getLanguageName('react.tsx')).toBe('TypeScript'); + }); + + // Content-Type lookups + + it('detects JavaScript files', () => { + expect(getLanguageName('react.js')).toBe('JavaScript'); + }); + + it('detects JSON files', () => { + expect(getLanguageName('react.json')).toBe('JSON'); + }); + + it('detects binary files', () => { + expect(getLanguageName('ionicons.bin')).toBe('Binary'); + }); + + it('detects EOT files', () => { + expect(getLanguageName('ionicons.eot')).toBe('Embedded OpenType'); + }); + + it('detects SVG files', () => { + expect(getLanguageName('react.svg')).toBe('SVG'); + }); + + it('detects TTF files', () => { + expect(getLanguageName('ionicons.ttf')).toBe('TrueType Font'); + }); + + it('detects WOFF files', () => { + expect(getLanguageName('ionicons.woff')).toBe('WOFF'); + }); + + it('detects WOFF2 files', () => { + expect(getLanguageName('ionicons.woff2')).toBe('WOFF2'); + }); + + it('detects CSS files', () => { + expect(getLanguageName('react.css')).toBe('CSS'); + }); + + it('detects HTML files', () => { + expect(getLanguageName('react.html')).toBe('HTML'); + }); + + it('detects JSX files', () => { + expect(getLanguageName('react.jsx')).toBe('JSX'); + }); + + it('detects Markdown files', () => { + expect(getLanguageName('README.md')).toBe('Markdown'); + }); + + it('detects plain text files', () => { + expect(getLanguageName('README')).toBe('Plain Text'); + expect(getLanguageName('LICENSE')).toBe('Plain Text'); + }); + + it('detects SCSS files', () => { + expect(getLanguageName('some.scss')).toBe('SCSS'); + }); + + it('detects YAML files', () => { + expect(getLanguageName('config.yml')).toBe('YAML'); + expect(getLanguageName('config.yaml')).toBe('YAML'); + }); +}); diff --git a/modules/actions/utils/cloudflare.js b/modules/utils/cloudflare.js similarity index 100% rename from modules/actions/utils/cloudflare.js rename to modules/utils/cloudflare.js diff --git a/modules/utils/createDataURI.js b/modules/utils/createDataURI.js new file mode 100644 index 0000000..a8a3b98 --- /dev/null +++ b/modules/utils/createDataURI.js @@ -0,0 +1,3 @@ +export default function createDataURI(contentType, content) { + return `data:${contentType};base64,${content.toString('base64')}`; +} diff --git a/modules/utils/encodeJSONForScript.js b/modules/utils/encodeJSONForScript.js new file mode 100644 index 0000000..a30cb55 --- /dev/null +++ b/modules/utils/encodeJSONForScript.js @@ -0,0 +1,8 @@ +import jsesc from 'jsesc'; + +/** + * Encodes some data as JSON that may safely be included in HTML. + */ +export default function encodeJSONForScript(data) { + return jsesc(data, { json: true, isScriptContext: true }); +} diff --git a/modules/utils/getContentType.js b/modules/utils/getContentType.js index 1ab0b04..61188f9 100644 --- a/modules/utils/getContentType.js +++ b/modules/utils/getContentType.js @@ -1,3 +1,4 @@ +import path from 'path'; import mime from 'mime'; mime.define( @@ -19,5 +20,9 @@ mime.define( const textFiles = /\/?(\.[a-z]*rc|\.git[a-z]*|\.[a-z]*ignore|\.lock)$/i; export default function getContentType(file) { - return textFiles.test(file) ? 'text/plain' : mime.getType(file); + const name = path.basename(file); + + return textFiles.test(name) + ? 'text/plain' + : mime.getType(name) || 'text/plain'; } diff --git a/modules/utils/getHighlights.js b/modules/utils/getHighlights.js new file mode 100644 index 0000000..cac6165 --- /dev/null +++ b/modules/utils/getHighlights.js @@ -0,0 +1,82 @@ +import path from 'path'; +import hljs from 'highlight.js'; + +import getContentType from './getContentType.js'; + +function escapeHTML(code) { + return code + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +// These should probably be added to highlight.js auto-detection. +const extLanguages = { + map: 'json', + mjs: 'javascript', + tsbuildinfo: 'json', + tsx: 'typescript', + vue: 'html' +}; + +function getLanguage(file) { + // Try to guess the language based on the file extension. + const ext = path.extname(file).substr(1); + + if (ext) { + return extLanguages[ext] || ext; + } + + const contentType = getContentType(file); + + if (contentType === 'text/plain') { + return 'text'; + } + + return null; +} + +function getLines(code) { + return code + .split('\n') + .map((line, index, array) => + index === array.length - 1 ? line : line + '\n' + ); +} + +/** + * Returns an array of HTML strings that highlight the given source code. + */ +export default function getHighlights(code, file) { + const language = getLanguage(file); + + if (!language) { + return null; + } + + if (language === 'text') { + return getLines(code).map(escapeHTML); + } + + try { + let continuation = false; + const hi = getLines(code).map(line => { + const result = hljs.highlight(language, line, false, continuation); + continuation = result.top; + return result; + }); + + return hi.map(result => + result.value.replace( + //g, + '' + ) + ); + } catch (error) { + // Probably an "unknown language" error. + // console.error(error); + return null; + } +} diff --git a/modules/utils/getLanguageName.js b/modules/utils/getLanguageName.js new file mode 100644 index 0000000..ec92892 --- /dev/null +++ b/modules/utils/getLanguageName.js @@ -0,0 +1,36 @@ +import getContentType from './getContentType.js'; + +const contentTypeNames = { + 'application/javascript': 'JavaScript', + 'application/json': 'JSON', + 'application/octet-stream': 'Binary', + 'application/vnd.ms-fontobject': 'Embedded OpenType', + 'application/xml': 'XML', + 'image/svg+xml': 'SVG', + 'font/ttf': 'TrueType Font', + 'font/woff': 'WOFF', + 'font/woff2': 'WOFF2', + 'text/css': 'CSS', + 'text/html': 'HTML', + 'text/jsx': 'JSX', + 'text/markdown': 'Markdown', + 'text/plain': 'Plain Text', + 'text/x-scss': 'SCSS', + 'text/yaml': 'YAML' +}; + +/** + * Gets a human-friendly name for whatever is in the given file. + */ +export default function getLanguageName(file) { + // Content-Type is text/plain, but we can be more descriptive. + if (/\.flow$/.test(file)) return 'Flow'; + if (/\.(d\.ts|tsx)$/.test(file)) return 'TypeScript'; + + // Content-Type is application/json, but we can be more descriptive. + if (/\.map$/.test(file)) return 'Source Map (JSON)'; + + const contentType = getContentType(file); + + return contentTypeNames[contentType] || contentType; +} diff --git a/modules/actions/utils/getScripts.js b/modules/utils/getScripts.js similarity index 93% rename from modules/actions/utils/getScripts.js rename to modules/utils/getScripts.js index ae02dee..545700a 100644 --- a/modules/actions/utils/getScripts.js +++ b/modules/utils/getScripts.js @@ -2,7 +2,7 @@ // eslint-disable-next-line import/no-unresolved import entryManifest from 'entry-manifest'; -import { createElement, createScript } from './markupHelpers.js'; +import { createElement, createScript } from './markup.js'; function getEntryPoint(name, format) { let entryPoints; diff --git a/modules/actions/utils/getStats.js b/modules/utils/getStats.js similarity index 100% rename from modules/actions/utils/getStats.js rename to modules/utils/getStats.js diff --git a/modules/actions/utils/markupHelpers.js b/modules/utils/markup.js similarity index 100% rename from modules/actions/utils/markupHelpers.js rename to modules/utils/markup.js diff --git a/modules/utils/parsePackageURL.js b/modules/utils/parsePackageURL.js index 6f20575..72c3628 100644 --- a/modules/utils/parsePackageURL.js +++ b/modules/utils/parsePackageURL.js @@ -19,7 +19,7 @@ export default function parsePackageURL(originalURL) { const packageName = match[1]; const packageVersion = match[2] || 'latest'; - const filename = match[3] || ''; + const filename = (match[3] || '').replace(/\/\/+/g, '/'); return { // If the URL is /@scope/name@version/file.js?main=browser: diff --git a/package.json b/package.json index d5eaae5..616ea78 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "build": "rollup -c", "clean": "git clean -e '!/.env' -fdX .", "lint": "eslint modules", + "serve": "nodemon -w server.js server.js", "test": "jest", "watch": "rollup -c -w" }, @@ -17,21 +18,25 @@ "@babel/plugin-syntax-export-namespace-from": "^7.2.0", "@babel/plugin-syntax-import-meta": "^7.2.0", "@emotion/core": "^10.0.6", + "@reach/visually-hidden": "^0.1.4", "cheerio": "^1.0.0-rc.2", "cors": "^2.8.5", "date-fns": "^1.30.1", "etag": "^1.8.1", "express": "^4.16.4", "gunzip-maybe": "^1.4.1", + "highlight.js": "^9.15.8", "isomorphic-fetch": "^2.2.1", + "jsesc": "^2.5.2", "lru-cache": "^5.1.1", "mime": "^2.4.0", "morgan": "^1.9.1", "ndjson": "^1.5.0", "pretty-bytes": "^5.1.0", "prop-types": "^15.6.2", - "react": "^16.7.0", - "react-dom": "^16.7.0", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-icons": "^3.7.0", "semver": "^5.6.0", "sort-by": "^1.2.0", "sri-toolbox": "^0.2.0", @@ -54,7 +59,9 @@ "eslint-import-resolver-node": "^0.3.2", "eslint-plugin-import": "^2.8.0", "eslint-plugin-react": "^7.14.2", + "eslint-plugin-react-hooks": "^1.6.1", "jest": "^22.4.4", + "nodemon": "^1.19.1", "rollup": "^1.1.0", "rollup-plugin-babel": "^4.2.0", "rollup-plugin-commonjs": "^9.2.0", diff --git a/plugins/entryManifest.js b/plugins/entryManifest.js index 4f454e7..31ff35f 100644 --- a/plugins/entryManifest.js +++ b/plugins/entryManifest.js @@ -33,7 +33,7 @@ function entryManifest() { Object.keys(bundle).forEach(fileName => { const info = bundle[fileName]; - // We're only interested in entry points. + // We're interested only in entry points. if (!info.isEntry) return; const globalImports = info.imports.filter( diff --git a/rollup.config.js b/rollup.config.js index 649b6d5..2c4ea23 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -18,9 +18,9 @@ const dev = env === 'development'; const manifest = entryManifest(); -const client = ['main', 'autoIndex'].map(entryName => { +const client = ['browse', 'main'].map(entryName => { return { - external: ['react', 'react-dom', '@emotion/core'], + external: ['@emotion/core', 'react', 'react-dom', 'highlight.js'], input: `modules/client/${entryName}.js`, output: { format: 'iife', diff --git a/unpkg.sketch b/unpkg.sketch new file mode 100644 index 0000000..9b4809f Binary files /dev/null and b/unpkg.sketch differ diff --git a/yarn.lock b/yarn.lock index dfb1638..64cc4f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -904,6 +904,11 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.2.tgz#63985d3d8b02530e0869962f4da09142ee8e200e" integrity sha512-n/VQ4mbfr81aqkx/XmVicOLjviMuy02eenSdJY33SVA7S2J42EU0P1H0mOogfYedb3wXA0d/LVtBrgTSm04WEA== +"@reach/visually-hidden@^0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@reach/visually-hidden/-/visually-hidden-0.1.4.tgz#0dc4ecedf523004337214187db70a46183bd945b" + integrity sha512-QHbzXjflSlCvDd6vJwdwx16mSB+vUCCQMiU/wK/CgVNPibtpEiIbisyxkpZc55DyDFNUIqP91rSUsNae+ogGDQ== + "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" @@ -1009,6 +1014,13 @@ ajv@^6.5.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-align@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" + integrity sha1-w2rsy6VjuJzrVW82kPCx2eNUf38= + dependencies: + string-width "^2.0.0" + ansi-escapes@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" @@ -1148,6 +1160,11 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== + async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" @@ -1446,6 +1463,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +binary-extensions@^1.0.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== + bl@^1.0.0: version "1.2.2" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" @@ -1475,6 +1497,19 @@ boolbase@~1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= +boxen@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" + integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw== + dependencies: + ansi-align "^2.0.0" + camelcase "^4.0.0" + chalk "^2.0.1" + cli-boxes "^1.0.0" + string-width "^2.0.0" + term-size "^1.2.0" + widest-line "^2.0.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1492,7 +1527,7 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" -braces@^2.3.1: +braces@^2.3.1, braces@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== @@ -1627,11 +1662,16 @@ callsites@^2.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= -camelcase@^4.1.0: +camelcase@^4.0.0, camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + caniuse-lite@^1.0.30000967: version "1.0.30000969" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000969.tgz#7664f571f2072657bde70b00a1fc1ba41f1942a9" @@ -1644,6 +1684,11 @@ capture-exit@^1.2.0: dependencies: rsvp "^3.3.3" +capture-stack-trace@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d" + integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -1686,6 +1731,25 @@ cheerio@^1.0.0-rc.2: lodash "^4.15.0" parse5 "^3.0.1" +chokidar@^2.1.5: + version "2.1.6" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.6.tgz#b6cad653a929e244ce8a834244164d241fa954c5" + integrity sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + chownr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" @@ -1711,6 +1775,11 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +cli-boxes@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" + integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM= + cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" @@ -1818,6 +1887,18 @@ concat-stream@^1.6.0: readable-stream "^2.2.2" typedarray "^0.0.6" +configstore@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f" + integrity sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw== + dependencies: + dot-prop "^4.1.0" + graceful-fs "^4.1.2" + make-dir "^1.0.0" + unique-string "^1.0.0" + write-file-atomic "^2.0.0" + xdg-basedir "^3.0.0" + console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" @@ -1915,6 +1996,13 @@ cosmiconfig@^5.2.0: js-yaml "^3.13.1" parse-json "^4.0.0" +create-error-class@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" + integrity sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y= + dependencies: + capture-stack-trace "^1.0.0" + cross-spawn@^5.0.1, cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -2159,11 +2247,23 @@ domutils@^1.5.1: dom-serializer "0" domelementtype "1" +dot-prop@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" + integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ== + dependencies: + is-obj "^1.0.0" + dotenv@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064" integrity sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w== +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= + duplexify@^3.5.0, duplexify@^3.6.0: version "3.7.1" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" @@ -2299,6 +2399,11 @@ eslint-plugin-import@^2.8.0: read-pkg-up "^2.0.0" resolve "^1.10.0" +eslint-plugin-react-hooks@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.6.1.tgz#3c66a5515ea3e0a221ffc5d4e75c971c217b1a4c" + integrity sha512-wHhmGJyVuijnYIJXZJHDUF2WM+rJYTjulUTqF9k61d3BTk8etydz+M4dXUVH7M76ZRS85rqBTCx0Es/lLsrjnA== + eslint-plugin-react@^7.14.2: version "7.14.2" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.2.tgz#94c193cc77a899ac0ecbb2766fbef88685b7ecc1" @@ -2775,7 +2880,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^1.2.3: +fsevents@^1.2.3, fsevents@^1.2.7: version "1.2.9" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== @@ -2844,6 +2949,14 @@ glob-parent@^2.0.0: dependencies: is-glob "^2.0.0" +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: version "7.1.4" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" @@ -2856,6 +2969,13 @@ glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +global-dirs@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" + integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= + dependencies: + ini "^1.3.4" + globals@^11.0.1, globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -2901,6 +3021,23 @@ google-closure-compiler@20181125.0.1: google-closure-compiler-linux "^20181125.0.1" google-closure-compiler-osx "^20181125.0.1" +got@^6.7.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" + integrity sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA= + dependencies: + create-error-class "^3.0.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + is-redirect "^1.0.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + unzip-response "^2.0.1" + url-parse-lax "^1.0.0" + graceful-fs@^4.1.11, graceful-fs@^4.1.2: version "4.1.15" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" @@ -3012,6 +3149,11 @@ has@^1.0.1, has@^1.0.3: dependencies: function-bind "^1.1.1" +highlight.js@^9.15.8: + version "9.15.8" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.8.tgz#f344fda123f36f1a65490e932cf90569e4999971" + integrity sha512-RrapkKQWwE+wKdF73VsOa2RQdIoO3mxwJ4P8mhbI6KYJUraUHRKM5w5zQQKXNk0xNL4UVRdulV9SBJcmzJNzVA== + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -3071,6 +3213,11 @@ iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13: dependencies: safer-buffer ">= 2.1.2 < 3" +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= + ignore-walk@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" @@ -3091,6 +3238,11 @@ import-fresh@^2.0.0: caller-path "^2.0.0" resolve-from "^3.0.0" +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= + import-local@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" @@ -3117,7 +3269,7 @@ inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -ini@~1.3.0: +ini@^1.3.4, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== @@ -3178,6 +3330,13 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= + dependencies: + binary-extensions "^1.0.0" + is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -3271,6 +3430,11 @@ is-extglob@^1.0.0: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" integrity sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA= +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + is-finite@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" @@ -3302,16 +3466,43 @@ is-glob@^2.0.0, is-glob@^2.0.1: dependencies: is-extglob "^1.0.0" +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + is-gzip@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-gzip/-/is-gzip-1.0.0.tgz#6ca8b07b99c77998025900e555ced8ed80879a83" integrity sha1-bKiwe5nHeZgCWQDlVc7Y7YCHmoM= +is-installed-globally@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" + integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA= + dependencies: + global-dirs "^0.1.0" + is-path-inside "^1.0.0" + is-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= +is-npm@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" + integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ= + is-number@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" @@ -3331,6 +3522,18 @@ is-number@^4.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= + dependencies: + path-is-inside "^1.0.1" + is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -3353,6 +3556,11 @@ is-promise@^2.1.0: resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= +is-redirect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ= + is-regex@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" @@ -3365,7 +3573,12 @@ is-resolvable@^1.0.0: resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== -is-stream@^1.0.1, is-stream@^1.1.0: +is-retry-allowed@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" + integrity sha1-EaBgVotnM5REAz0BJaYaINVk+zQ= + +is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= @@ -3851,7 +4064,7 @@ jsesc@^1.3.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= -jsesc@^2.5.1: +jsesc@^2.5.1, jsesc@^2.5.2: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== @@ -3957,6 +4170,13 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== +latest-version@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" + integrity sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU= + dependencies: + package-json "^4.0.0" + lcid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" @@ -4028,6 +4248,11 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +lowercase-keys@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -4373,6 +4598,22 @@ node-releases@^1.1.19: dependencies: semver "^5.3.0" +nodemon@^1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.1.tgz#576f0aad0f863aabf8c48517f6192ff987cd5071" + integrity sha512-/DXLzd/GhiaDXXbGId5BzxP1GlsqtMGM9zTmkWrgXtSqjKmGSbLicM/oAy4FR0YWm14jCHRwnR31AHS2dYFHrg== + dependencies: + chokidar "^2.1.5" + debug "^3.1.0" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.6" + semver "^5.5.0" + supports-color "^5.2.0" + touch "^3.1.0" + undefsafe "^2.0.2" + update-notifier "^2.5.0" + nopt@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" @@ -4381,6 +4622,13 @@ nopt@^4.0.1: abbrev "1" osenv "^0.1.4" +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= + dependencies: + abbrev "1" + normalize-package-data@^2.3.2: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -4398,6 +4646,11 @@ normalize-path@^2.0.1, normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + npm-bundled@^1.0.1: version "1.0.6" resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" @@ -4641,6 +4894,16 @@ p-try@^1.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= +package-json@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" + integrity sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0= + dependencies: + got "^6.7.1" + registry-auth-token "^3.0.1" + registry-url "^3.0.3" + semver "^5.1.0" + pako@~0.2.0: version "0.2.9" resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" @@ -4693,6 +4956,11 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" @@ -4710,7 +4978,7 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-is-inside@^1.0.2: +path-is-inside@^1.0.1, path-is-inside@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= @@ -4809,6 +5077,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prepend-http@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= + preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" @@ -4869,6 +5142,11 @@ psl@^1.1.24, psl@^1.1.28: resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184" integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw== +pstree.remy@^1.1.6: + version "1.1.7" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.7.tgz#c76963a28047ed61542dc361aa26ee55a7fa15f3" + integrity sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A== + pump@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" @@ -4930,7 +5208,7 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" -rc@^1.2.7: +rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -4940,7 +5218,7 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@^16.7.0: +react-dom@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA== @@ -4950,12 +5228,19 @@ react-dom@^16.7.0: prop-types "^15.6.2" scheduler "^0.13.6" +react-icons@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-3.7.0.tgz#64fe46231fabfeea27895edeae6c3b78114b8c8f" + integrity sha512-7MyPwjIhuyW0D2N3s4DEd0hGPGFf0sK+IIRKhc1FvSpZNVmnUoGvHbmAwzGJU+3my+fvihVWgwU5SDtlAri56Q== + dependencies: + camelcase "^5.0.0" + react-is@^16.8.1: version "16.8.6" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== -react@^16.7.0: +react@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw== @@ -4999,7 +5284,7 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6: +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -5021,6 +5306,15 @@ readable-stream@^3.1.1: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + realpath-native@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" @@ -5094,6 +5388,21 @@ regexpu-core@^4.5.4: unicode-match-property-ecmascript "^1.0.4" unicode-match-property-value-ecmascript "^1.1.0" +registry-auth-token@^3.0.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e" + integrity sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A== + dependencies: + rc "^1.1.6" + safe-buffer "^5.0.1" + +registry-url@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" + integrity sha1-PU74cPc93h138M+aOBQyRE4XSUI= + dependencies: + rc "^1.0.1" + regjsgen@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd" @@ -5393,7 +5702,14 @@ scheduler@^0.13.6: loose-envify "^1.1.0" object-assign "^4.1.1" -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: +semver-diff@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" + integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY= + dependencies: + semver "^5.0.3" + +"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== @@ -5783,7 +6099,7 @@ supports-color@^3.1.2: dependencies: has-flag "^1.0.0" -supports-color@^5.3.0: +supports-color@^5.2.0, supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -5859,6 +6175,13 @@ tempy@^0.3.0: type-fest "^0.3.1" unique-string "^1.0.0" +term-size@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" + integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk= + dependencies: + execa "^0.7.0" + test-exclude@^4.2.1: version "4.2.3" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.3.tgz#a9a5e64474e4398339245a0a769ad7c2f4a97c20" @@ -5893,6 +6216,11 @@ through@^2.3.6: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +timed-out@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -5950,6 +6278,13 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + tough-cookie@^2.3.3, tough-cookie@^2.3.4: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -6023,6 +6358,13 @@ uglify-js@^3.1.4: commander "~2.20.0" source-map "~0.6.1" +undefsafe@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.2.tgz#225f6b9e0337663e0d8e7cfd686fc2836ccace76" + integrity sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY= + dependencies: + debug "^2.2.0" + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -6076,6 +6418,32 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +unzip-response@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" + integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c= + +upath@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" + integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== + +update-notifier@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6" + integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw== + dependencies: + boxen "^1.2.1" + chalk "^2.0.1" + configstore "^3.0.0" + import-lazy "^2.1.0" + is-ci "^1.0.10" + is-installed-globally "^0.1.0" + is-npm "^1.0.0" + latest-version "^3.0.0" + semver-diff "^2.0.0" + xdg-basedir "^3.0.0" + uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" @@ -6088,6 +6456,13 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +url-parse-lax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" + integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM= + dependencies: + prepend-http "^1.0.1" + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -6252,6 +6627,13 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2" +widest-line@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc" + integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA== + dependencies: + string-width "^2.1.1" + wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" @@ -6275,7 +6657,7 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -write-file-atomic@^2.1.0: +write-file-atomic@^2.0.0, write-file-atomic@^2.1.0: version "2.4.3" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== @@ -6298,6 +6680,11 @@ ws@^5.2.0: dependencies: async-limiter "~1.0.0" +xdg-basedir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" + integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"