From 34baab07aba70efb945e24530fbacac0a4694b21 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 24 Jul 2019 17:55:13 -0700 Subject: [PATCH] New "browse" UI Also, separated out browse, ?meta, and ?module request handlers. Fixes #82 --- ...yIndex-test.js => browseDirectory-test.js} | 10 +- modules/__tests__/browseFile-test.js | 34 ++ modules/__tests__/legacyURLs-test.js | 10 + ...rveAutoIndexPage.js => serveBrowsePage.js} | 28 +- modules/actions/serveDirectoryBrowser.js | 81 ++++ modules/actions/serveDirectoryMetadata.js | 97 ++++ modules/actions/serveFile.js | 37 +- modules/actions/serveFileBrowser.js | 84 ++++ modules/actions/serveFileMetadata.js | 61 +++ modules/actions/serveMainPage.js | 15 +- modules/actions/serveMetadata.js | 42 -- modules/actions/serveModule.js | 2 +- modules/actions/serveStaticFile.js | 24 - modules/actions/serveStats.js | 2 +- modules/client/.eslintrc | 8 +- modules/client/autoIndex/App.js | 91 ---- modules/client/autoIndex/DirectoryListing.js | 142 ------ modules/client/{autoIndex.js => browse.js} | 2 +- modules/client/browse/App.js | 356 +++++++++++++++ modules/client/browse/DirectoryViewer.js | 182 ++++++++ modules/client/browse/FileViewer.js | 212 +++++++++ modules/client/browse/Icons.js | 24 + modules/client/browse/PackageInfo.js | 11 + modules/client/browse/images/DownArrow.png | Bin 0 -> 307 bytes .../client/browse/images/SelectDownArrow.png | Bin 0 -> 343 bytes modules/client/main/App.js | 406 +++++++++++------ modules/client/main/GoogleCloudLogo.png | Bin 26055 -> 0 bytes modules/client/main/Icons.js | 15 + .../client/main/{ => images}/AngularLogo.png | Bin .../main/{ => images}/CloudflareLogo.png | Bin modules/client/utils/format.js | 18 + modules/client/utils/formatNumber.js | 10 - modules/client/utils/formatPercent.js | 3 - modules/client/utils/markup.js | 3 + modules/client/utils/style.js | 24 + modules/createServer.js | 163 +++++-- .../middleware/{findFile.js => findEntry.js} | 97 ++-- .../{fetchPackage.js => validateFilename.js} | 45 +- modules/middleware/validateVersion.js | 49 +++ .../utils => templates}/MainTemplate.js | 5 +- .../utils/__tests__/getContentType-test.js | 68 +-- .../utils/__tests__/getLanguageName-test.js | 84 ++++ modules/{actions => }/utils/cloudflare.js | 0 modules/utils/createDataURI.js | 3 + modules/utils/encodeJSONForScript.js | 8 + modules/utils/getContentType.js | 7 +- modules/utils/getHighlights.js | 82 ++++ modules/utils/getLanguageName.js | 36 ++ modules/{actions => }/utils/getScripts.js | 2 +- modules/{actions => }/utils/getStats.js | 0 .../markupHelpers.js => utils/markup.js} | 0 modules/utils/parsePackageURL.js | 2 +- package.json | 11 +- plugins/entryManifest.js | 2 +- rollup.config.js | 4 +- unpkg.sketch | Bin 0 -> 26823 bytes yarn.lock | 415 +++++++++++++++++- 57 files changed, 2431 insertions(+), 686 deletions(-) rename modules/__tests__/{directoryIndex-test.js => browseDirectory-test.js} (70%) create mode 100644 modules/__tests__/browseFile-test.js rename modules/actions/{serveAutoIndexPage.js => serveBrowsePage.js} (58%) create mode 100644 modules/actions/serveDirectoryBrowser.js create mode 100644 modules/actions/serveDirectoryMetadata.js create mode 100644 modules/actions/serveFileBrowser.js create mode 100644 modules/actions/serveFileMetadata.js delete mode 100644 modules/actions/serveMetadata.js delete mode 100644 modules/actions/serveStaticFile.js delete mode 100644 modules/client/autoIndex/App.js delete mode 100644 modules/client/autoIndex/DirectoryListing.js rename modules/client/{autoIndex.js => browse.js} (81%) create mode 100644 modules/client/browse/App.js create mode 100644 modules/client/browse/DirectoryViewer.js create mode 100644 modules/client/browse/FileViewer.js create mode 100644 modules/client/browse/Icons.js create mode 100644 modules/client/browse/PackageInfo.js create mode 100644 modules/client/browse/images/DownArrow.png create mode 100644 modules/client/browse/images/SelectDownArrow.png delete mode 100644 modules/client/main/GoogleCloudLogo.png create mode 100644 modules/client/main/Icons.js rename modules/client/main/{ => images}/AngularLogo.png (100%) rename modules/client/main/{ => images}/CloudflareLogo.png (100%) create mode 100644 modules/client/utils/format.js delete mode 100644 modules/client/utils/formatNumber.js delete mode 100644 modules/client/utils/formatPercent.js create mode 100644 modules/client/utils/markup.js create mode 100644 modules/client/utils/style.js rename modules/middleware/{findFile.js => findEntry.js} (63%) rename modules/middleware/{fetchPackage.js => validateFilename.js} (63%) create mode 100644 modules/middleware/validateVersion.js rename modules/{actions/utils => templates}/MainTemplate.js (92%) create mode 100644 modules/utils/__tests__/getLanguageName-test.js rename modules/{actions => }/utils/cloudflare.js (100%) create mode 100644 modules/utils/createDataURI.js create mode 100644 modules/utils/encodeJSONForScript.js create mode 100644 modules/utils/getHighlights.js create mode 100644 modules/utils/getLanguageName.js rename modules/{actions => }/utils/getScripts.js (93%) rename modules/{actions => }/utils/getStats.js (100%) rename modules/{actions/utils/markupHelpers.js => utils/markup.js} (100%) create mode 100644 unpkg.sketch 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} +
+
+ +
+
+

© {new Date().getFullYear()} UNPKG

+

+ + + + + + +

+
+
+
+
+ ); +} + +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 0000000000000000000000000000000000000000..3212bc94745b67a7aa5f38d19c239cce8c8930ef GIT binary patch literal 307 zcmV-30nGl1P)Px#?MXyIR45g#P&*33KoE5|N#!Xl#nwKgN^g-Lis(hePOl-Fgn$P~r@f@K^a#Pc4k-y=KKDV{IBMD-hxF@q!&eT__dhw8Dj>wEbGWQAA{uI zHmRyAR}^L9y6%Bfk>fZ!xD}8Q|6m!4*S2kMDUZp?K@e=veFUv5>rzY~?6Lb)X!Qhw zpz{oI0z8B!dD2d*s?H^?Jpnz$aokPQG{Gc2P_0RR;!YP}h@0ITt#+*mNHI;5cGPjy z8ⅅUo}lL-is-}bpsNFg<;q)%koSdV-HYV$Kw5O{slZgM15=oHK70i002ovPDHLk FV1m=#fBOIc literal 0 HcmV?d00001 diff --git a/modules/client/browse/images/SelectDownArrow.png b/modules/client/browse/images/SelectDownArrow.png new file mode 100644 index 0000000000000000000000000000000000000000..64913ae1162b5b66a83db4ef2120fb3c3c578993 GIT binary patch literal 343 zcmV-d0jU0oP)Px$5lKWrR45f=WB>v+E&WCiO)9u`?P5KnimstO!=JwgNYxBd&%(^)$o%fri-$sD zl6U?y{`X*HU|=Q6AV!9742+CPx2|5?#0=8#?)8hyVqy{?wiyt!fy8hDMn;COOpMH3 zx36883{nF&0K|Cv`sE=Z5pfL$Mg|oi7rP;hj9(f4GpxOR?P4KF4+tr(42-{ls_wBd z37TBJdf*GP4h#c8lG@s)5~k<5iNIS pKi9x&nV7ZjTs;qABWb1z695D&O&377NxJ|5002ovPDHLkV1mw3kPrX> literal 0 HcmV?d00001 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 7e23dfac198639ba2b5d31ebd5871f216dfc230f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26055 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 j8u; +} + +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 0000000000000000000000000000000000000000..9b4809f338a8281c93ad383cd1d5423c55185387 GIT binary patch literal 26823 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 rj%NR1+5U&@e^d0o$o|d|ag;Yl&p_*VF$2q;leE literal 0 HcmV?d00001 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"