Server render auto-index pages

Also, load the autoIndex bundle on the client so we can use React
instead of using an inline script.
This commit is contained in:
Michael Jackson 2018-07-31 10:07:27 -07:00
parent 168ccf5aac
commit 135da0fdc5
62 changed files with 761 additions and 589 deletions

View File

@ -1,8 +0,0 @@
import "./main.css";
import React from "react";
import ReactDOM from "react-dom";
import App from "./main/App";
ReactDOM.render(<App />, document.getElementById("root"));

View File

@ -1,11 +0,0 @@
import "./About.css";
import React from "react";
import html from "./About.md";
function About() {
return <div className="wrapper" dangerouslySetInnerHTML={{ __html: html }} />;
}
export default About;

View File

@ -1,11 +0,0 @@
import React from "react";
import { HashRouter } from "react-router-dom";
import Layout from "./Layout";
const App = () => (
<HashRouter>
<Layout />
</HashRouter>
);
export default App;

View File

@ -1,11 +0,0 @@
import "./Home.css";
import React from "react";
import html from "./Home.md";
function Home() {
return <div className="wrapper" dangerouslySetInnerHTML={{ __html: html }} />;
}
export default Home;

View File

@ -1,10 +0,0 @@
const formatNumber = n => {
const digits = String(n).split("");
const groups = [];
while (digits.length) groups.unshift(digits.splice(-3).join(""));
return groups.join(",");
};
export default formatNumber;

View File

@ -1,4 +0,0 @@
const formatPercent = (n, fixed = 1) =>
String((n.toPrecision(2) * 100).toFixed(fixed));
export default formatPercent;

View File

@ -1,3 +0,0 @@
const parseNumber = s => parseInt(s.replace(/,/g, ""), 10) || 0;
export default parseNumber;

View File

@ -12,7 +12,7 @@ services:
server:
build: .
command: node_modules/.bin/nodemon --ignore client server.js
command: node_modules/.bin/nodemon --ignore server/client server.js
env_file: .env
environment:
- CACHE_URL=redis://data:6379
@ -30,7 +30,7 @@ services:
worker:
build: .
command: node_modules/.bin/nodemon --ignore client server/ingestLogs.js
command: node_modules/.bin/nodemon --ignore server/client server/ingestLogs.js
env_file: .env
environment:
- DATA_URL=redis://data:6379

View File

@ -7,12 +7,16 @@
"test": "jest"
},
"dependencies": {
"babel-core": "^6.26.0",
"babel-core": "^6.26.3",
"babel-plugin-syntax-export-extensions": "^6.13.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.14",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"babel-register": "^6.26.0",
"body-parser": "^1.18.2",
"cors": "^2.8.1",
"countries-list": "^1.3.2",
"csso": "^3.1.1",
"date-fns": "^1.28.1",
"etag": "^1.8.0",
"express": "^4.15.2",
@ -44,15 +48,12 @@
"devDependencies": {
"babel-eslint": "^8.0.3",
"babel-loader": "^7.1.2",
"babel-plugin-transform-react-remove-prop-types": "^0.4.13",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"css-loader": "0.26.1",
"errorhandler": "^1.5.0",
"eslint": "^4.13.1",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-react": "^7.5.1",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "0.10.0",
"html-loader": "^0.5.1",
"jest": "^22.4.4",

View File

@ -6,6 +6,8 @@ const createServer = require("./server/createServer");
const createDevServer = require("./server/createDevServer");
const config = require("./server/config");
require("./server/clientRuntime");
if (process.env.SENTRY_DSN) {
raven
.config(process.env.SENTRY_DSN, {

View File

@ -0,0 +1,57 @@
const React = require("react");
const ReactDOMServer = require("react-dom/server");
const semver = require("semver");
const MainPage = require("../client/MainPage");
const AutoIndexApp = require("../client/autoIndex/App");
const renderPage = require("../utils/renderPage");
const globalScripts =
process.env.NODE_ENV === "production"
? [
"/react@16.4.1/umd/react.production.min.js",
"/react-dom@16.4.1/umd/react-dom.production.min.js",
"/react-router-dom@4.3.1/umd/react-router-dom.min.js"
]
: [
"/react@16.4.1/umd/react.development.js",
"/react-dom@16.4.1/umd/react-dom.development.js",
"/react-router-dom@4.3.1/umd/react-router-dom.js"
];
function byVersion(a, b) {
return semver.lt(a, b) ? -1 : semver.gt(a, b) ? 1 : 0;
}
function serveAutoIndexPage(req, res) {
const scripts = globalScripts.concat(req.assets.getScripts("autoIndex"));
const styles = req.assets.getStyles("autoIndex");
const props = {
packageName: req.packageName,
packageVersion: req.packageVersion,
availableVersions: Object.keys(req.packageInfo.versions).sort(byVersion),
filename: req.filename,
entry: req.entry,
entries: req.entries
};
const content = ReactDOMServer.renderToString(
React.createElement(AutoIndexApp, props)
);
const html = renderPage(MainPage, {
scripts: scripts,
styles: styles,
data: props,
content: content
});
res
.set({
"Cache-Control": "public,max-age=60", // 1 minute
"Cache-Tag": "auto-index"
})
.send(html);
}
module.exports = serveAutoIndexPage;

View File

@ -1,155 +1,7 @@
const fs = require("fs");
const path = require("path");
const etag = require("etag");
const babel = require("babel-core");
const IndexPage = require("../components/IndexPage");
const unpkgRewrite = require("../plugins/unpkgRewrite");
const addLeadingSlash = require("../utils/addLeadingSlash");
const renderPage = require("../utils/renderPage");
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]);
}
function getMetadata(entry, entries) {
const metadata = {
path: addLeadingSlash(entry.name),
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;
}
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);
}
function rewriteBareModuleIdentifiers(code, packageConfig) {
const dependencies = Object.assign(
{},
packageConfig.peerDependencies,
packageConfig.dependencies
);
const options = {
// Ignore .babelrc and package.json babel config
// because we haven't installed dependencies so
// we can't load plugins; see #84
babelrc: false,
plugins: [unpkgRewrite(dependencies)]
};
return babel.transform(code, options).code;
}
function getContentTypeHeader(type) {
return type === "application/javascript" ? type + "; charset=utf-8" : type;
}
function serveJavaScriptModule(req, res) {
if (req.entry.contentType !== "application/javascript") {
return res
.status(403)
.type("text")
.send("?module mode is available only for JavaScript files");
}
try {
const code = rewriteBareModuleIdentifiers(
req.entry.content.toString("utf8"),
req.packageConfig
);
res
.set({
"Content-Length": Buffer.byteLength(code),
"Content-Type": getContentTypeHeader(req.entry.contentType),
"Cache-Control": "public,max-age=31536000", // 1 year
ETag: etag(code),
"Cache-Tag": "file,js-file,js-module"
})
.send(code);
} catch (error) {
console.error(error);
const errorName = error.constructor.name;
const errorMessage = error.message.replace(
/^.*?\/unpkg-.+?\//,
`/${req.packageSpec}/`
);
const codeFrame = error.codeFrame;
const debugInfo = `${errorName}: ${errorMessage}\n\n${codeFrame}`;
res
.status(500)
.type("text")
.send(
`Cannot generate module for ${req.packageSpec}${
req.filename
}\n\n${debugInfo}`
);
}
}
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);
}
function serveIndex(req, res) {
const html = renderPage(IndexPage, {
packageInfo: req.packageInfo,
version: req.packageVersion,
filename: req.filename,
entries: req.entries,
entry: req.entry
});
res
.set({
"Cache-Control": "public,max-age=60", // 1 minute
"Cache-Tag": "index"
})
.send(html);
}
const serveAutoIndexPage = require("./serveAutoIndexPage");
const serveJavaScriptModule = require("./serveJavaScriptModule");
const serveStaticFile = require("./serveStaticFile");
const serveMetadata = require("./serveMetadata");
/**
* Send the file, JSON metadata, or HTML directory listing.
@ -160,7 +12,7 @@ function serveFile(req, res) {
}
if (req.entry.type === "directory") {
return serveIndex(req, res);
return serveAutoIndexPage(req, res);
}
if (req.query.module != null) {

View File

@ -0,0 +1,70 @@
const etag = require("etag");
const babel = require("babel-core");
const getContentTypeHeader = require("../utils/getContentTypeHeader");
const unpkgRewrite = require("../plugins/unpkgRewrite");
function rewriteBareModuleIdentifiers(code, packageConfig) {
const dependencies = Object.assign(
{},
packageConfig.peerDependencies,
packageConfig.dependencies
);
const options = {
// Ignore .babelrc and package.json babel config
// because we haven't installed dependencies so
// we can't load plugins; see #84
babelrc: false,
plugins: [unpkgRewrite(dependencies)]
};
return babel.transform(code, options).code;
}
function serveJavaScriptModule(req, res) {
if (req.entry.contentType !== "application/javascript") {
return res
.status(403)
.type("text")
.send("?module mode is available only for JavaScript files");
}
try {
const code = rewriteBareModuleIdentifiers(
req.entry.content.toString("utf8"),
req.packageConfig
);
res
.set({
"Content-Length": Buffer.byteLength(code),
"Content-Type": getContentTypeHeader(req.entry.contentType),
"Cache-Control": "public,max-age=31536000", // 1 year
ETag: etag(code),
"Cache-Tag": "file,js-file,js-module"
})
.send(code);
} catch (error) {
console.error(error);
const errorName = error.constructor.name;
const errorMessage = error.message.replace(
/^.*?\/unpkg-.+?\//,
`/${req.packageSpec}/`
);
const codeFrame = error.codeFrame;
const debugInfo = `${errorName}: ${errorMessage}\n\n${codeFrame}`;
res
.status(500)
.type("text")
.send(
`Cannot generate module for ${req.packageSpec}${
req.filename
}\n\n${debugInfo}`
);
}
}
module.exports = serveJavaScriptModule;

View File

@ -1,13 +1,29 @@
const MainPage = require("../components/MainPage");
const MainPage = require("../client/MainPage");
const renderPage = require("../utils/renderPage");
const globalScripts =
process.env.NODE_ENV === "production"
? [
"/react@16.4.1/umd/react.production.min.js",
"/react-dom@16.4.1/umd/react-dom.production.min.js",
"/react-router-dom@4.3.1/umd/react-router-dom.min.js"
]
: [
"/react@16.4.1/umd/react.development.js",
"/react-dom@16.4.1/umd/react-dom.development.js",
"/react-router-dom@4.3.1/umd/react-router-dom.js"
];
function serveMainPage(req, res) {
res.send(
renderPage(MainPage, {
scripts: req.assets.getScripts("main"),
styles: req.assets.getStyles("main")
})
);
const scripts = globalScripts.concat(req.assets.getScripts("main"));
const styles = req.assets.getStyles("main");
const html = renderPage(MainPage, {
scripts: scripts,
styles: styles
});
res.send(html);
}
module.exports = serveMainPage;

View File

@ -0,0 +1,44 @@
const path = require("path");
const addLeadingSlash = require("../utils/addLeadingSlash");
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]);
}
function getMetadata(entry, entries) {
const metadata = {
path: addLeadingSlash(entry.name),
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;
}
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);
}
module.exports = serveMetadata;

View File

@ -0,0 +1,26 @@
const path = require("path");
const etag = require("etag");
const getContentTypeHeader = require("../utils/getContentTypeHeader");
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);
}
module.exports = serveStaticFile;

56
server/client/MainPage.js Normal file
View File

@ -0,0 +1,56 @@
const React = require("react");
const PropTypes = require("prop-types");
const h = require("./utils/createHTML");
const x = require("./utils/execScript");
function MainPage({ title, description, scripts, styles, data, content }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="description" content={description} />
<meta httpEquiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1"
/>
<meta name="timestamp" content={new Date().toISOString()} />
<link rel="shortcut icon" href="/favicon.ico" />
{styles.map(s => <link key={s} rel="stylesheet" href={s} />)}
{x(
"window.Promise || document.write('\\x3Cscript src=\"/_polyfills/es6-promise.min.js\">\\x3C/script>\\x3Cscript>ES6Promise.polyfill()\\x3C/script>')"
)}
{x(
"window.fetch || document.write('\\x3Cscript src=\"/_polyfills/fetch.min.js\">\\x3C/script>')"
)}
{x(`window.__DATA__ = ${JSON.stringify(data)}`)}
<title>{title}</title>
</head>
<body>
<div id="root" dangerouslySetInnerHTML={h(content)} />
{scripts.map(s => <script key={s} src={s} />)}
</body>
</html>
);
}
MainPage.propTypes = {
title: PropTypes.string,
description: PropTypes.string,
scripts: PropTypes.arrayOf(PropTypes.string),
styles: PropTypes.arrayOf(PropTypes.string),
data: PropTypes.any,
content: PropTypes.string
};
MainPage.defaultProps = {
title: "UNPKG",
description: "The CDN for everything on npm",
scripts: [],
styles: [],
data: {},
content: ""
};
module.exports = MainPage;

View File

@ -0,0 +1,8 @@
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;
}

View File

@ -0,0 +1,10 @@
require("./autoIndex.css");
const React = require("react");
const ReactDOM = require("react-dom");
const App = require("./autoIndex/App");
const props = window.__DATA__ || {};
ReactDOM.hydrate(<App {...props} />, document.getElementById("root"));

View File

@ -0,0 +1,23 @@
.app {
max-width: 900px;
margin: 0 auto;
}
.app-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.app-version-selector {
line-height: 2.25em;
float: right;
}
.app-version-selector select {
font-size: 1em;
}
.app-address {
text-align: right;
}

View File

@ -0,0 +1,77 @@
require("./App.css");
const React = require("react");
const DirectoryListing = require("./DirectoryListing");
class App extends React.Component {
static defaultProps = {
availableVersions: []
};
handleChange = event => {
window.location.href = window.location.href.replace(
"@" + this.props.packageVersion,
"@" + event.target.value
);
};
render() {
return (
<div className="app">
<header className="app-header">
<h1>
Index of /{this.props.packageName}@{this.props.packageVersion}
{this.props.filename}
</h1>
<div className="app-version-selector">
Version:{" "}
<select
id="version"
defaultValue={this.props.packageVersion}
onChange={this.handleChange}
>
{this.props.availableVersions.map(v => (
<option key={v} value={v}>
{v}
</option>
))}
</select>
</div>
</header>
<hr />
<DirectoryListing
filename={this.props.filename}
entry={this.props.entry}
entries={this.props.entries}
/>
<hr />
<address className="app-address">
{this.props.packageName}@{this.props.packageVersion}
</address>
</div>
);
}
}
if (process.env.NODE_ENV === "development") {
const PropTypes = require("prop-types");
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
};
}
module.exports = App;

View File

@ -0,0 +1,17 @@
.directory-listing table {
width: 100%;
border-collapse: collapse;
font: 0.85em Monaco, monospace;
}
.directory-listing tr.even {
background-color: #eee;
}
.directory-listing th {
text-align: left;
}
.directory-listing th,
.directory-listing td {
padding: 0.5em 1em;
}

View File

@ -0,0 +1,126 @@
require("./DirectoryListing.css");
const React = require("react");
const formatBytes = require("pretty-bytes");
const sortBy = require("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;
}
function DirectoryListing({ filename, entry, entries }) {
const rows = [];
if (filename !== "/") {
rows.push(
<tr key="..">
<td>
<a title="Parent directory" href="../">
..
</a>
</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
);
}
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(
<tr key={name}>
<td>
<a title={relName} href={href}>
{href}
</a>
</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
);
});
matchingEntries
.filter(({ type }) => type === "file")
.sort(sortBy("name"))
.forEach(({ name, size, contentType, lastModified }) => {
const relName = getRelativeName(entry.name, name);
rows.push(
<tr key={name}>
<td>
<a title={relName} href={relName}>
{relName}
</a>
</td>
<td>{contentType}</td>
<td>{formatBytes(size)}</td>
<td>{lastModified}</td>
</tr>
);
});
return (
<div className="directory-listing">
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Size</th>
<th>Last Modified</th>
</tr>
</thead>
<tbody>
{rows.map((row, index) =>
React.cloneElement(row, {
className: index % 2 ? "odd" : "even"
})
)}
</tbody>
</table>
</div>
);
}
if (process.env.NODE_ENV === "development") {
const PropTypes = require("prop-types");
const entryType = PropTypes.shape({
name: PropTypes.string.isRequired
});
DirectoryListing.propTypes = {
filename: PropTypes.string.isRequired,
entry: entryType.isRequired,
entries: PropTypes.objectOf(entryType).isRequired
};
}
module.exports = DirectoryListing;

8
server/client/main.js Normal file
View File

@ -0,0 +1,8 @@
require("./main.css");
const React = require("react");
const ReactDOM = require("react-dom");
const App = require("./main/App");
ReactDOM.render(<App />, document.getElementById("root"));

View File

@ -0,0 +1,12 @@
require("./About.css");
const React = require("react");
const h = require("../utils/createHTML");
const markup = require("./About.md");
function About() {
return <div className="wrapper" dangerouslySetInnerHTML={h(markup)} />;
}
module.exports = About;

14
server/client/main/App.js Normal file
View File

@ -0,0 +1,14 @@
const React = require("react");
const { HashRouter } = require("react-router-dom");
const Layout = require("./Layout");
function App() {
return (
<HashRouter>
<Layout />
</HashRouter>
);
}
module.exports = App;

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,12 @@
require("./Home.css");
const React = require("react");
const h = require("../utils/createHTML");
const markup = require("./Home.md");
function Home() {
return <div className="wrapper" dangerouslySetInnerHTML={h(markup)} />;
}
module.exports = Home;

View File

@ -1,14 +1,14 @@
import "./Layout.css";
require("./Layout.css");
import React from "react";
import PropTypes from "prop-types";
import { Motion, spring } from "react-motion";
import { Switch, Route, Link, withRouter } from "react-router-dom";
const React = require("react");
const PropTypes = require("prop-types");
const { Switch, Route, Link, withRouter } = require("react-router-dom");
const { Motion, spring } = require("react-motion");
import WindowSize from "./WindowSize";
import About from "./About";
import Stats from "./Stats";
import Home from "./Home";
const WindowSize = require("./WindowSize");
const About = require("./About");
const Stats = require("./Stats");
const Home = require("./Home");
class Layout extends React.Component {
static propTypes = {
@ -132,4 +132,4 @@ class Layout extends React.Component {
}
}
export default withRouter(Layout);
module.exports = withRouter(Layout);

View File

@ -1,14 +1,14 @@
import "./Stats.css";
require("./Stats.css");
import React 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 { continents, countries } from "countries-list";
const React = require("react");
const PropTypes = require("prop-types");
const formatBytes = require("pretty-bytes");
const formatDate = require("date-fns/format");
const parseDate = require("date-fns/parse");
const { continents, countries } = require("countries-list");
import formatNumber from "../utils/formatNumber";
import formatPercent from "../utils/formatPercent";
const formatNumber = require("../utils/formatNumber");
const formatPercent = require("../utils/formatPercent");
function getCountriesByContinent(continent) {
return Object.keys(countries).filter(
@ -320,4 +320,4 @@ class Stats extends React.Component {
}
}
export default Stats;
module.exports = Stats;

View File

@ -1,8 +1,8 @@
import React from "react";
import PropTypes from "prop-types";
const React = require("react");
const PropTypes = require("prop-types");
import addEvent from "../utils/addEvent";
import removeEvent from "../utils/removeEvent";
const addEvent = require("../utils/addEvent");
const removeEvent = require("../utils/removeEvent");
const resizeEvent = "resize";
@ -32,4 +32,4 @@ class WindowSize extends React.Component {
}
}
export default WindowSize;
module.exports = WindowSize;

View File

@ -6,4 +6,4 @@ function addEvent(node, type, handler) {
}
}
export default addEvent;
module.exports = addEvent;

View File

@ -0,0 +1,5 @@
function createHTML(code) {
return { __html: code };
}
module.exports = createHTML;

View File

@ -0,0 +1,9 @@
const React = require("react");
const h = require("./createHTML");
function execScript(code) {
return <script dangerouslySetInnerHTML={h(code)} />;
}
module.exports = execScript;

View File

@ -0,0 +1,12 @@
function formatNumber(n) {
const digits = String(n).split("");
const groups = [];
while (digits.length) {
groups.unshift(digits.splice(-3).join(""));
}
return groups.join(",");
}
module.exports = formatNumber;

View File

@ -0,0 +1,5 @@
function formatPercent(n, fixed = 1) {
return String((n.toPrecision(2) * 100).toFixed(fixed));
}
module.exports = formatPercent;

View File

@ -0,0 +1,5 @@
function parseNumber(s) {
return parseInt(s.replace(/,/g, ""), 10) || 0;
}
module.exports = parseNumber;

View File

@ -6,4 +6,4 @@ function removeEvent(node, type, handler) {
}
}
export default removeEvent;
module.exports = removeEvent;

9
server/clientRuntime.js Normal file
View File

@ -0,0 +1,9 @@
// Use babel to compile JSX on the fly.
require("babel-register")({
only: /server\/client/
});
// Ignore require("*.css") calls.
require.extensions[".css"] = function() {
return {};
};

View File

@ -1,102 +0,0 @@
const path = require("path");
const formatBytes = require("pretty-bytes");
const sortBy = require("sort-by");
const cloneElement = require("./utils/cloneElement");
const e = require("./utils/createElement");
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]);
}
function getRelativeName(base, name) {
return base.length ? name.substr(base.length + 1) : name;
}
function DirectoryListing({ filename, entry, entries }) {
const rows = [];
if (filename !== "/") {
rows.push(
e(
"tr",
{ key: ".." },
e("td", null, e("a", { title: "Parent directory", href: "../" }, "..")),
e("td", null, "-"),
e("td", null, "-"),
e("td", null, "-")
)
);
}
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(
e(
"tr",
{ key: name },
e("td", null, e("a", { title: relName, href }, href)),
e("td", null, "-"),
e("td", null, "-"),
e("td", null, "-")
)
);
});
matchingEntries
.filter(({ type }) => type === "file")
.sort(sortBy("name"))
.forEach(({ name, size, contentType, lastModified }) => {
const relName = getRelativeName(entry.name, name);
rows.push(
e(
"tr",
{ key: name },
e("td", null, e("a", { title: relName, href: relName }, relName)),
e("td", null, contentType),
e("td", null, formatBytes(size)),
e("td", null, lastModified)
)
);
});
return e(
"table",
null,
e(
"thead",
null,
e(
"tr",
null,
e("th", null, "Name"),
e("th", null, "Type"),
e("th", null, "Size"),
e("th", null, "Last Modified")
)
),
e(
"tbody",
null,
rows.map((row, index) =>
cloneElement(row, {
className: index % 2 ? "odd" : "even"
})
)
)
);
}
module.exports = DirectoryListing;

View File

@ -1,40 +0,0 @@
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;
}
table {
width: 100%;
border-collapse: collapse;
font: 0.85em Monaco, monospace;
}
tr.even {
background-color: #eee;
}
th {
text-align: left;
}
th,
td {
padding: 0.25em 0.5em;
}
.content-wrapper {
max-width: 900px;
margin: 0 auto;
}
.version-wrapper {
line-height: 2.25em;
float: right;
}
#version {
font-size: 1em;
}
address {
text-align: right;
}

View File

@ -1,59 +0,0 @@
const semver = require("semver");
const DirectoryListing = require("./DirectoryListing");
const readCSS = require("./utils/readCSS");
const e = require("./utils/createElement");
const s = require("./utils/createStyle");
const x = require("./utils/createScript");
const IndexPageStyle = readCSS(__dirname, "IndexPage.css");
const IndexPageScript = `
var s = document.getElementById('version'), v = s.value;
s.onchange = function () {
window.location.href = window.location.href.replace('@' + v, '@' + s.value);
};
`;
function byVersion(a, b) {
return semver.lt(a, b) ? -1 : semver.gt(a, b) ? 1 : 0;
}
function IndexPage({ packageInfo, version, filename, entry, entries }) {
const versions = Object.keys(packageInfo.versions).sort(byVersion);
const options = versions.map(v =>
e("option", { key: v, value: v }, `${packageInfo.name}@${v}`)
);
return e(
"html",
null,
e(
"head",
null,
e("meta", { charSet: "utf-8" }),
e("title", null, `Index of ${filename}`),
s(IndexPageStyle)
),
e(
"body",
null,
e(
"div",
{ className: "content-wrapper" },
e(
"div",
{ className: "version-wrapper" },
e("select", { id: "version", defaultValue: version }, options)
),
e("h1", null, `Index of ${filename}`),
x(IndexPageScript),
e("hr"),
e(DirectoryListing, { filename, entry, entries }),
e("hr"),
e("address", null, `${packageInfo.name}@${version}`)
)
)
);
}
module.exports = IndexPage;

View File

@ -1,61 +0,0 @@
const PropTypes = require("prop-types");
const e = require("./utils/createElement");
const x = require("./utils/createScript");
function MainPage({ title, description, scripts, styles, content }) {
return e(
"html",
{ lang: "en" },
e(
"head",
null,
e("meta", { charSet: "utf-8" }),
e("title", null, title),
e("meta", { httpEquiv: "X-UA-Compatible", content: "IE=edge,chrome=1" }),
e("meta", { name: "description", content: description }),
e("meta", {
name: "viewport",
content: "width=device-width,initial-scale=1,maximum-scale=1"
}),
e("meta", { name: "timestamp", content: new Date().toISOString() }),
e("link", { rel: "shortcut icon", href: "/favicon.ico" }),
x(
"window.Promise || document.write('\\x3Cscript src=\"/_polyfills/es6-promise.min.js\">\\x3C/script>\\x3Cscript>ES6Promise.polyfill()\\x3C/script>')"
),
x(
"window.fetch || document.write('\\x3Cscript src=\"/_polyfills/fetch.min.js\">\\x3C/script>')"
),
e("script", { src: "/react@16.4.1/umd/react.production.min.js" }),
e("script", { src: "/react-dom@16.4.1/umd/react-dom.production.min.js" }),
e("script", {
src: "/react-router-dom@4.3.1/umd/react-router-dom.min.js"
}),
styles.map(s => e("link", { key: s, rel: "stylesheet", href: s }))
),
e(
"body",
null,
e("div", { id: "root", dangerouslySetInnerHTML: { __html: content } }),
scripts.map(s => e("script", { key: s, src: s }))
)
);
}
MainPage.propTypes = {
title: PropTypes.string,
description: PropTypes.string,
scripts: PropTypes.arrayOf(PropTypes.string),
styles: PropTypes.arrayOf(PropTypes.string),
content: PropTypes.string
};
MainPage.defaultProps = {
title: "UNPKG",
description: "The CDN for everything on npm",
scripts: [],
styles: [],
content: ""
};
module.exports = MainPage;

View File

@ -1,2 +0,0 @@
const React = require("react");
module.exports = React.cloneElement;

View File

@ -1,2 +0,0 @@
const React = require("react");
module.exports = React.createElement;

View File

@ -1,7 +0,0 @@
const createElement = require("./createElement");
function createScript(code) {
return createElement("script", { dangerouslySetInnerHTML: { __html: code } });
}
module.exports = createScript;

View File

@ -1,7 +0,0 @@
const createElement = require("./createElement");
function createStyle(code) {
return createElement("style", { dangerouslySetInnerHTML: { __html: code } });
}
module.exports = createStyle;

View File

@ -1,9 +0,0 @@
const fs = require("fs");
const path = require("path");
const csso = require("csso");
function readCSS(...args) {
return csso.minify(fs.readFileSync(path.resolve(...args), "utf8")).css;
}
module.exports = readCSS;

View File

@ -0,0 +1,5 @@
function getContentTypeHeader(type) {
return type === "application/javascript" ? type + "; charset=utf-8" : type;
}
module.exports = getContentTypeHeader;

View File

@ -4,11 +4,8 @@ const ReactDOMServer = require("react-dom/server");
const doctype = "<!DOCTYPE html>";
function renderPage(page, props) {
const html = ReactDOMServer.renderToStaticMarkup(
React.createElement(page, props)
);
return doctype + html;
const element = React.createElement(page, props);
return doctype + ReactDOMServer.renderToStaticMarkup(element);
}
module.exports = renderPage;

View File

@ -1,9 +1,11 @@
const path = require("path");
const webpack = require("webpack");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
entry: {
main: path.resolve(__dirname, "client/main.js")
main: path.resolve(__dirname, "./server/client/main.js"),
autoIndex: path.resolve(__dirname, "./server/client/autoIndex.js")
},
externals: {
@ -21,7 +23,13 @@ module.exports = {
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, use: "babel-loader" },
{ test: /\.css$/, use: ["style-loader", "css-loader"] },
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
},
{ test: /\.md$/, use: ["html-loader", "markdown-loader"] },
{ test: /\.png$/, use: "file-loader" }
]
@ -32,7 +40,8 @@ module.exports = {
"process.env.NODE_ENV": JSON.stringify(
process.env.NODE_ENV || "development"
)
})
}),
new ExtractTextPlugin("[name]-[hash:8].css")
],
devtool:

110
yarn.lock
View File

@ -132,7 +132,7 @@ ajv@^4.9.1:
co "^4.6.0"
json-stable-stringify "^1.0.1"
ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0:
ajv@^5.0.0, ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0:
version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
dependencies:
@ -363,6 +363,12 @@ async@^2.1.2, async@^2.1.4:
dependencies:
lodash "^4.14.0"
async@^2.4.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
dependencies:
lodash "^4.17.10"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@ -426,6 +432,30 @@ babel-core@^6.0.0, babel-core@^6.26.0:
slash "^1.0.0"
source-map "^0.5.6"
babel-core@^6.26.3:
version "6.26.3"
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207"
dependencies:
babel-code-frame "^6.26.0"
babel-generator "^6.26.0"
babel-helpers "^6.24.1"
babel-messages "^6.23.0"
babel-register "^6.26.0"
babel-runtime "^6.26.0"
babel-template "^6.26.0"
babel-traverse "^6.26.0"
babel-types "^6.26.0"
babylon "^6.18.0"
convert-source-map "^1.5.1"
debug "^2.6.9"
json5 "^0.5.1"
lodash "^4.17.4"
minimatch "^3.0.4"
path-is-absolute "^1.0.1"
private "^0.1.8"
slash "^1.0.0"
source-map "^0.5.7"
babel-eslint@^8.0.3:
version "8.2.1"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.1.tgz#136888f3c109edc65376c23ebf494f36a3e03951"
@ -780,8 +810,8 @@ babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015
babel-template "^6.24.1"
babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a"
version "6.26.2"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3"
dependencies:
babel-plugin-transform-strict-mode "^6.24.1"
babel-runtime "^6.26.0"
@ -913,9 +943,9 @@ babel-plugin-transform-react-jsx@^6.24.1:
babel-plugin-syntax-jsx "^6.8.0"
babel-runtime "^6.22.0"
babel-plugin-transform-react-remove-prop-types@^0.4.13:
version "0.4.13"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.13.tgz#331cfc05099a808238311d78319c27460d481189"
babel-plugin-transform-react-remove-prop-types@^0.4.14:
version "0.4.14"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.14.tgz#d3bf0ab39bd491c6670e71abd8642d4e6caae919"
babel-plugin-transform-regenerator@^6.22.0:
version "6.26.0"
@ -930,9 +960,9 @@ babel-plugin-transform-strict-mode@^6.24.1:
babel-runtime "^6.22.0"
babel-types "^6.24.1"
babel-preset-env@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.1.tgz#a18b564cc9b9afdf4aae57ae3c1b0d99188e6f48"
babel-preset-env@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a"
dependencies:
babel-plugin-check-es2015-constants "^6.22.0"
babel-plugin-syntax-trailing-function-commas "^6.22.0"
@ -961,7 +991,7 @@ babel-preset-env@^1.6.1:
babel-plugin-transform-es2015-unicode-regex "^6.22.0"
babel-plugin-transform-exponentiation-operator "^6.22.0"
babel-plugin-transform-regenerator "^6.22.0"
browserslist "^2.1.2"
browserslist "^3.2.6"
invariant "^2.2.2"
semver "^5.3.0"
@ -1317,12 +1347,12 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
caniuse-db "^1.0.30000639"
electron-to-chromium "^1.2.7"
browserslist@^2.1.2:
version "2.11.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.11.3.tgz#fe36167aed1bbcde4827ebfe71347a2cc70b99b2"
browserslist@^3.2.6:
version "3.2.8"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6"
dependencies:
caniuse-lite "^1.0.30000792"
electron-to-chromium "^1.3.30"
caniuse-lite "^1.0.30000844"
electron-to-chromium "^1.3.47"
bser@^2.0.0:
version "2.0.0"
@ -1456,9 +1486,9 @@ caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
version "1.0.30000808"
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000808.tgz#30dfd83009d5704f02dffb37725068ed12a366bb"
caniuse-lite@^1.0.30000792:
version "1.0.30000808"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000808.tgz#7d759b5518529ea08b6705a19e70dbf401628ffc"
caniuse-lite@^1.0.30000844:
version "1.0.30000865"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000865.tgz#70026616e8afe6e1442f8bb4e1092987d81a2f25"
capture-exit@^1.2.0:
version "1.2.0"
@ -1799,7 +1829,7 @@ content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
convert-source-map@^1.4.0, convert-source-map@^1.5.0:
convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
@ -1956,13 +1986,6 @@ css-selector-tokenizer@^0.7.0:
fastparse "^1.1.1"
regexpu-core "^1.0.0"
css-tree@1.0.0-alpha.27:
version "1.0.0-alpha.27"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.27.tgz#f211526909c7dc940843d83b9376ed98ddb8de47"
dependencies:
mdn-data "^1.0.0"
source-map "^0.5.3"
cssesc@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4"
@ -2004,12 +2027,6 @@ cssesc@^0.1.0:
postcss-value-parser "^3.2.3"
postcss-zindex "^2.0.1"
csso@^3.1.1:
version "3.5.0"
resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.0.tgz#acdbba5719e2c87bc801eadc032764b2e4b9d4e7"
dependencies:
css-tree "1.0.0-alpha.27"
csso@~2.3.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85"
@ -2297,10 +2314,14 @@ ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30:
electron-to-chromium@^1.2.7:
version "1.3.33"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.33.tgz#bf00703d62a7c65238136578c352d6c5c042a545"
electron-to-chromium@^1.3.47:
version "1.3.52"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.52.tgz#d2d9f1270ba4a3b967b831c40ef71fb4d9ab5ce0"
elliptic@^6.0.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
@ -2787,6 +2808,15 @@ extglob@^2.0.2, extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
extract-text-webpack-plugin@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.2.tgz#5f043eaa02f9750a9258b78c0a6e0dc1408fb2f7"
dependencies:
async "^2.4.1"
loader-utils "^1.1.0"
schema-utils "^0.3.0"
webpack-sources "^1.0.1"
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@ -4558,7 +4588,7 @@ lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
lodash@^4.13.1:
lodash@^4.13.1, lodash@^4.17.10:
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
@ -4666,10 +4696,6 @@ md5@^2.2.1:
crypt "~0.0.1"
is-buffer "~1.1.1"
mdn-data@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.0.tgz#a7056319da95a2d0881267d7263075042eb061e2"
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@ -5780,7 +5806,7 @@ pretty-format@^22.4.0, pretty-format@^22.4.3:
ansi-regex "^3.0.0"
ansi-styles "^3.2.0"
private@^0.1.6, private@^0.1.7, private@~0.1.5:
private@^0.1.6, private@^0.1.7, private@^0.1.8, private@~0.1.5:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@ -6443,6 +6469,12 @@ sax@^1.2.4, sax@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
schema-utils@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf"
dependencies:
ajv "^5.0.0"
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"