Server render the main page
Also, add hashes to asset file names and use the "entry manifest" plugin in dev to get auto-reloading.
This commit is contained in:
@ -2,31 +2,18 @@ import React from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import semver from 'semver';
|
||||
|
||||
import MainPage from '../client/MainPage';
|
||||
import MainTemplate from '../client/MainTemplate';
|
||||
import AutoIndexApp from '../client/autoIndex/App';
|
||||
import createHTML from '../client/utils/createHTML';
|
||||
import renderPage from '../utils/renderPage';
|
||||
|
||||
const globalScripts =
|
||||
process.env.NODE_ENV === 'production'
|
||||
? [
|
||||
'/react@16.7.0/umd/react.production.min.js',
|
||||
'/react-dom@16.7.0/umd/react-dom.production.min.js'
|
||||
]
|
||||
: [
|
||||
'/react@16.7.0/umd/react.development.js',
|
||||
'/react-dom@16.7.0/umd/react-dom.development.js'
|
||||
];
|
||||
import getEntryPoints from '../utils/getEntryPoints';
|
||||
import renderTemplate from '../utils/renderTemplate';
|
||||
|
||||
function byVersion(a, b) {
|
||||
return semver.lt(a, b) ? -1 : semver.gt(a, b) ? 1 : 0;
|
||||
}
|
||||
|
||||
export default function serveAutoIndexPage(req, res) {
|
||||
const scripts = globalScripts.concat('/_assets/autoIndex.js');
|
||||
const styles = ['/autoIndex.css'];
|
||||
|
||||
const props = {
|
||||
const data = {
|
||||
packageName: req.packageName,
|
||||
packageVersion: req.packageVersion,
|
||||
availableVersions: Object.keys(req.packageInfo.versions).sort(byVersion),
|
||||
@ -34,17 +21,22 @@ export default function serveAutoIndexPage(req, res) {
|
||||
entry: req.entry,
|
||||
entries: req.entries
|
||||
};
|
||||
|
||||
const content = createHTML(
|
||||
ReactDOMServer.renderToString(React.createElement(AutoIndexApp, props))
|
||||
ReactDOMServer.renderToString(React.createElement(AutoIndexApp, data))
|
||||
);
|
||||
|
||||
const html = renderPage(MainPage, {
|
||||
const entryPoints = getEntryPoints('autoIndex', {
|
||||
es: 'module',
|
||||
system: 'nomodule'
|
||||
});
|
||||
|
||||
const html = renderTemplate(MainTemplate, {
|
||||
title: `UNPKG - ${req.packageName}`,
|
||||
description: `The CDN for ${req.packageName}`,
|
||||
scripts: scripts,
|
||||
styles: styles,
|
||||
data: props,
|
||||
content: content
|
||||
data,
|
||||
content,
|
||||
entryPoints
|
||||
});
|
||||
|
||||
res
|
||||
|
||||
32
modules/actions/serveMainPage.js
Normal file
32
modules/actions/serveMainPage.js
Normal file
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import { StaticRouter } from 'react-router-dom';
|
||||
|
||||
import MainTemplate from '../client/MainTemplate';
|
||||
import MainApp from '../client/main/App';
|
||||
import createHTML from '../client/utils/createHTML';
|
||||
import getEntryPoints from '../utils/getEntryPoints';
|
||||
import renderTemplate from '../utils/renderTemplate';
|
||||
|
||||
export default function serveMainPage(req, res) {
|
||||
const element = React.createElement(
|
||||
StaticRouter,
|
||||
{ location: req.url },
|
||||
React.createElement(MainApp)
|
||||
);
|
||||
const content = createHTML(ReactDOMServer.renderToString(element));
|
||||
|
||||
const entryPoints = getEntryPoints('main', {
|
||||
es: 'module',
|
||||
system: 'nomodule'
|
||||
});
|
||||
|
||||
const html = renderTemplate(MainTemplate, { content, entryPoints });
|
||||
|
||||
res
|
||||
.set({
|
||||
'Cache-Control': 'public, max-age=14400', // 4 hours
|
||||
'Cache-Tag': 'main'
|
||||
})
|
||||
.send(html);
|
||||
}
|
||||
@ -1,4 +1,9 @@
|
||||
{
|
||||
"presets": [["@babel/env", { "loose": true }], "@babel/react"],
|
||||
"plugins": [["@babel/plugin-proposal-class-properties", { "loose": true }]]
|
||||
"presets": [
|
||||
["@babel/env", { "loose": true }],
|
||||
"@babel/react"
|
||||
],
|
||||
"plugins": [
|
||||
["@babel/plugin-proposal-class-properties", { "loose": true }]
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import createHTML from './utils/createHTML';
|
||||
import x from './utils/execScript';
|
||||
|
||||
export default function MainPage({
|
||||
title,
|
||||
description,
|
||||
favicon,
|
||||
scripts,
|
||||
styles,
|
||||
data,
|
||||
content
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
{description && <meta name="description" content={description} />}
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,maximum-scale=1"
|
||||
/>
|
||||
<meta name="timestamp" content={new Date().toISOString()} />
|
||||
{favicon && <link rel="shortcut icon" href={favicon} />}
|
||||
{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={content} />
|
||||
{scripts.map(s => (
|
||||
<script key={s} src={s} />
|
||||
))}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
MainPage.defaultProps = {
|
||||
title: 'UNPKG',
|
||||
description: 'The CDN for everything on npm',
|
||||
favicon: '/favicon.ico',
|
||||
scripts: [],
|
||||
styles: [],
|
||||
data: {},
|
||||
content: createHTML('')
|
||||
};
|
||||
|
||||
const htmlType = PropTypes.shape({
|
||||
__html: PropTypes.string
|
||||
});
|
||||
|
||||
MainPage.propTypes = {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
favicon: PropTypes.string,
|
||||
scripts: PropTypes.arrayOf(PropTypes.string),
|
||||
styles: PropTypes.arrayOf(PropTypes.string),
|
||||
data: PropTypes.any,
|
||||
content: htmlType
|
||||
};
|
||||
89
modules/client/MainTemplate.js
Normal file
89
modules/client/MainTemplate.js
Normal file
@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import createHTML from './utils/createHTML';
|
||||
import x from './utils/execScript';
|
||||
|
||||
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>\')';
|
||||
const fetchShim =
|
||||
'window.fetch || document.write(\'\\x3Cscript src="/whatwg-fetch@3.0.0/dist/fetch.umd.js">\\x3C/script>\')';
|
||||
|
||||
export default function MainTemplate({
|
||||
title,
|
||||
description,
|
||||
favicon,
|
||||
data,
|
||||
content,
|
||||
globalScripts,
|
||||
entryPoints
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
{description && <meta name="description" content={description} />}
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,maximum-scale=1"
|
||||
/>
|
||||
<meta name="timestamp" content={new Date().toISOString()} />
|
||||
{favicon && <link rel="shortcut icon" href={favicon} />}
|
||||
<title>{title}</title>
|
||||
{x(promiseShim)}
|
||||
{x(fetchShim)}
|
||||
{data && x(`window.__DATA__ = ${JSON.stringify(data)}`)}
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" dangerouslySetInnerHTML={content} />
|
||||
|
||||
{globalScripts.map(src => (
|
||||
<script key={src} src={src} />
|
||||
))}
|
||||
|
||||
{entryPoints.module &&
|
||||
x(`
|
||||
import('${entryPoints.module}');
|
||||
window.supportsDynamicImport = true;
|
||||
`)}
|
||||
{entryPoints.nomodule &&
|
||||
x(`
|
||||
if (!window.supportsDynamicImport) {
|
||||
var s = document.createElement('script');
|
||||
s.src = '/systemjs@2.0.0/dist/s.min.js';
|
||||
s.addEventListener('load', function() {
|
||||
System.import('${entryPoints.nomodule}');
|
||||
});
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
`)}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
MainTemplate.defaultProps = {
|
||||
title: 'UNPKG',
|
||||
description: 'The CDN for everything on npm',
|
||||
favicon: '/favicon.ico',
|
||||
content: createHTML(''),
|
||||
globalScripts: []
|
||||
};
|
||||
|
||||
const htmlType = PropTypes.shape({
|
||||
__html: PropTypes.string
|
||||
});
|
||||
|
||||
MainTemplate.propTypes = {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
favicon: PropTypes.string,
|
||||
data: PropTypes.any,
|
||||
content: htmlType,
|
||||
globalScripts: PropTypes.arrayOf(PropTypes.string),
|
||||
entryPoints: PropTypes.shape({
|
||||
module: PropTypes.string,
|
||||
nomodule: PropTypes.string
|
||||
}).isRequired
|
||||
};
|
||||
@ -1,8 +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;
|
||||
}
|
||||
@ -1,10 +1,26 @@
|
||||
// import './autoIndex.css';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Global, css } from '@emotion/core';
|
||||
|
||||
import App from './autoIndex/App';
|
||||
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
||||
const props = window.__DATA__ || {};
|
||||
|
||||
ReactDOM.hydrate(<App {...props} />, document.getElementById('root'));
|
||||
ReactDOM.hydrate(
|
||||
<div>
|
||||
<Global styles={globalStyles} />
|
||||
<App {...props} />
|
||||
</div>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
@ -26,18 +26,7 @@ const styles = {
|
||||
}
|
||||
};
|
||||
|
||||
const entryType = PropTypes.object;
|
||||
|
||||
export default class App extends React.Component {
|
||||
static 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
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
availableVersions: []
|
||||
};
|
||||
@ -92,3 +81,16 @@ export default class App extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@ -126,12 +126,14 @@ export default function DirectoryListing({ filename, entry, entries }) {
|
||||
);
|
||||
}
|
||||
|
||||
const entryType = PropTypes.shape({
|
||||
name: PropTypes.string.isRequired
|
||||
});
|
||||
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
|
||||
};
|
||||
DirectoryListing.propTypes = {
|
||||
filename: PropTypes.string.isRequired,
|
||||
entry: entryType.isRequired,
|
||||
entries: PropTypes.objectOf(entryType).isRequired
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
body {
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
|
||||
Arial, sans-serif;
|
||||
line-height: 1.7;
|
||||
padding: 5px 20px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
body {
|
||||
padding: 40px 20px 120px;
|
||||
}
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: blue;
|
||||
}
|
||||
a:visited {
|
||||
color: rebeccapurple;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid black;
|
||||
border: 0;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
background-color: #eee;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding: 5px;
|
||||
}
|
||||
th {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@ -1,8 +1,82 @@
|
||||
// import './main.css';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import { Global, css } from '@emotion/core';
|
||||
|
||||
import App from './main/App';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
const globalStyles = css`
|
||||
body {
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
Helvetica, Arial, sans-serif;
|
||||
line-height: 1.7;
|
||||
padding: 5px 20px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
body {
|
||||
padding: 40px 20px 120px;
|
||||
}
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: blue;
|
||||
}
|
||||
a:visited {
|
||||
color: rebeccapurple;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid black;
|
||||
border: 0;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
background-color: #eee;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding: 5px;
|
||||
}
|
||||
th {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
`;
|
||||
|
||||
ReactDOM.render(
|
||||
<HashRouter>
|
||||
<div>
|
||||
<Global styles={globalStyles} />
|
||||
<App />
|
||||
</div>
|
||||
</HashRouter>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
@ -1,14 +1,175 @@
|
||||
import React from 'react';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Switch, Route, Link, withRouter } from 'react-router-dom';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
|
||||
import Layout from './Layout';
|
||||
import WindowSize from './WindowSize';
|
||||
import About from './About';
|
||||
import Stats from './Stats';
|
||||
import Home from './Home';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<HashRouter>
|
||||
<Layout />
|
||||
</HashRouter>
|
||||
);
|
||||
const styles = {
|
||||
title: {
|
||||
margin: 0,
|
||||
textTransform: 'uppercase',
|
||||
textAlign: 'center',
|
||||
fontSize: '5em'
|
||||
},
|
||||
nav: {
|
||||
margin: '0 0 3em'
|
||||
},
|
||||
navList: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
navListItem: {
|
||||
flexBasis: 'auto',
|
||||
listStyleType: 'none',
|
||||
display: 'inline-block',
|
||||
fontSize: '1.1em',
|
||||
margin: '0 10px'
|
||||
},
|
||||
navLink: {
|
||||
textDecoration: 'none',
|
||||
color: 'black'
|
||||
},
|
||||
navUnderline: {
|
||||
height: 4,
|
||||
backgroundColor: 'black',
|
||||
position: 'absolute',
|
||||
left: 0
|
||||
}
|
||||
};
|
||||
|
||||
class Layout extends React.Component {
|
||||
state = {
|
||||
underlineLeft: 0,
|
||||
underlineWidth: 0,
|
||||
useSpring: false,
|
||||
stats: null
|
||||
};
|
||||
|
||||
adjustUnderline = (useSpring = false) => {
|
||||
let itemIndex;
|
||||
switch (this.props.location.pathname) {
|
||||
case '/stats':
|
||||
itemIndex = 1;
|
||||
break;
|
||||
case '/about':
|
||||
itemIndex = 2;
|
||||
break;
|
||||
case '/':
|
||||
default:
|
||||
itemIndex = 0;
|
||||
}
|
||||
|
||||
const itemNodes = this.listNode.querySelectorAll('li');
|
||||
const currentNode = itemNodes[itemIndex];
|
||||
|
||||
this.setState({
|
||||
underlineLeft: currentNode.offsetLeft,
|
||||
underlineWidth: currentNode.offsetWidth,
|
||||
useSpring
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.adjustUnderline();
|
||||
|
||||
fetch('/api/stats?period=last-month')
|
||||
.then(res => res.json())
|
||||
.then(stats => this.setState({ stats }));
|
||||
|
||||
if (window.localStorage) {
|
||||
const savedStats = window.localStorage.savedStats;
|
||||
|
||||
if (savedStats) this.setState({ stats: JSON.parse(savedStats) });
|
||||
|
||||
window.onbeforeunload = () => {
|
||||
localStorage.savedStats = JSON.stringify(this.state.stats);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.location.pathname !== this.props.location.pathname) {
|
||||
this.adjustUnderline(true);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { underlineLeft, underlineWidth, useSpring } = this.state;
|
||||
|
||||
const style = {
|
||||
left: useSpring
|
||||
? spring(underlineLeft, { stiffness: 220 })
|
||||
: underlineLeft,
|
||||
width: useSpring ? spring(underlineWidth) : underlineWidth
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
<WindowSize onChange={this.adjustUnderline} />
|
||||
|
||||
<div className="wrapper">
|
||||
<header>
|
||||
<h1 style={styles.title}>unpkg</h1>
|
||||
<nav style={styles.nav}>
|
||||
<ol style={styles.navList} ref={node => (this.listNode = node)}>
|
||||
<li style={styles.navListItem}>
|
||||
<Link to="/" style={styles.navLink}>
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
<li style={styles.navListItem}>
|
||||
<Link to="/stats" style={styles.navLink}>
|
||||
Stats
|
||||
</Link>
|
||||
</li>
|
||||
<li style={styles.navListItem}>
|
||||
<Link to="/about" style={styles.navLink}>
|
||||
About
|
||||
</Link>
|
||||
</li>
|
||||
</ol>
|
||||
<Motion
|
||||
defaultStyle={{ left: underlineLeft, width: underlineWidth }}
|
||||
style={style}
|
||||
children={style => (
|
||||
<div
|
||||
style={{
|
||||
...styles.navUnderline,
|
||||
WebkitTransform: `translate3d(${style.left}px,0,0)`,
|
||||
transform: `translate3d(${style.left}px,0,0)`,
|
||||
width: style.width
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<Switch>
|
||||
<Route
|
||||
path="/stats"
|
||||
render={() => <Stats data={this.state.stats} />}
|
||||
/>
|
||||
<Route path="/about" component={About} />
|
||||
<Route path="/" component={Home} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Layout.propTypes = {
|
||||
location: PropTypes.object,
|
||||
children: PropTypes.node
|
||||
};
|
||||
}
|
||||
|
||||
export default withRouter(Layout);
|
||||
|
||||
@ -1,172 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Switch, Route, Link, withRouter } from 'react-router-dom';
|
||||
import { Motion, spring } from 'react-motion';
|
||||
|
||||
import WindowSize from './WindowSize';
|
||||
import About from './About';
|
||||
import Stats from './Stats';
|
||||
import Home from './Home';
|
||||
|
||||
const styles = {
|
||||
title: {
|
||||
margin: 0,
|
||||
textTransform: 'uppercase',
|
||||
textAlign: 'center',
|
||||
fontSize: '5em'
|
||||
},
|
||||
nav: {
|
||||
margin: '0 0 3em'
|
||||
},
|
||||
navList: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
navListItem: {
|
||||
flexBasis: 'auto',
|
||||
listStyleType: 'none',
|
||||
display: 'inline-block',
|
||||
fontSize: '1.1em',
|
||||
margin: '0 10px'
|
||||
},
|
||||
navLink: {
|
||||
textDecoration: 'none',
|
||||
color: 'black'
|
||||
},
|
||||
navUnderline: {
|
||||
height: 4,
|
||||
backgroundColor: 'black',
|
||||
position: 'absolute',
|
||||
left: 0
|
||||
}
|
||||
};
|
||||
|
||||
class Layout extends React.Component {
|
||||
static propTypes = {
|
||||
location: PropTypes.object,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
state = {
|
||||
underlineLeft: 0,
|
||||
underlineWidth: 0,
|
||||
useSpring: false,
|
||||
stats: null
|
||||
};
|
||||
|
||||
adjustUnderline = (useSpring = false) => {
|
||||
let itemIndex;
|
||||
switch (this.props.location.pathname) {
|
||||
case '/stats':
|
||||
itemIndex = 1;
|
||||
break;
|
||||
case '/about':
|
||||
itemIndex = 2;
|
||||
break;
|
||||
case '/':
|
||||
default:
|
||||
itemIndex = 0;
|
||||
}
|
||||
|
||||
const itemNodes = this.listNode.querySelectorAll('li');
|
||||
const currentNode = itemNodes[itemIndex];
|
||||
|
||||
this.setState({
|
||||
underlineLeft: currentNode.offsetLeft,
|
||||
underlineWidth: currentNode.offsetWidth,
|
||||
useSpring
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.adjustUnderline();
|
||||
|
||||
fetch('/api/stats?period=last-month')
|
||||
.then(res => res.json())
|
||||
.then(stats => this.setState({ stats }));
|
||||
|
||||
if (window.localStorage) {
|
||||
const savedStats = window.localStorage.savedStats;
|
||||
|
||||
if (savedStats) this.setState({ stats: JSON.parse(savedStats) });
|
||||
|
||||
window.onbeforeunload = () => {
|
||||
localStorage.savedStats = JSON.stringify(this.state.stats);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.location.pathname !== this.props.location.pathname) {
|
||||
this.adjustUnderline(true);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { underlineLeft, underlineWidth, useSpring } = this.state;
|
||||
|
||||
const style = {
|
||||
left: useSpring
|
||||
? spring(underlineLeft, { stiffness: 220 })
|
||||
: underlineLeft,
|
||||
width: useSpring ? spring(underlineWidth) : underlineWidth
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="layout">
|
||||
<WindowSize onChange={this.adjustUnderline} />
|
||||
<div className="wrapper">
|
||||
<header>
|
||||
<h1 style={styles.title}>unpkg</h1>
|
||||
<nav style={styles.nav}>
|
||||
<ol style={styles.navList} ref={node => (this.listNode = node)}>
|
||||
<li style={styles.navListItem}>
|
||||
<Link to="/" style={styles.navLink}>
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
<li style={styles.navListItem}>
|
||||
<Link to="/stats" style={styles.navLink}>
|
||||
Stats
|
||||
</Link>
|
||||
</li>
|
||||
<li style={styles.navListItem}>
|
||||
<Link to="/about" style={styles.navLink}>
|
||||
About
|
||||
</Link>
|
||||
</li>
|
||||
</ol>
|
||||
<Motion
|
||||
defaultStyle={{ left: underlineLeft, width: underlineWidth }}
|
||||
style={style}
|
||||
children={style => (
|
||||
<div
|
||||
style={{
|
||||
...styles.navUnderline,
|
||||
WebkitTransform: `translate3d(${style.left}px,0,0)`,
|
||||
transform: `translate3d(${style.left}px,0,0)`,
|
||||
width: style.width
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<Switch>
|
||||
<Route
|
||||
path="/stats"
|
||||
render={() => <Stats data={this.state.stats} />}
|
||||
/>
|
||||
<Route path="/about" component={About} />
|
||||
<Route path="/" component={Home} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(Layout);
|
||||
@ -35,10 +35,6 @@ function sumKeyValues(hash, keys) {
|
||||
// }
|
||||
|
||||
export default class Stats extends React.Component {
|
||||
static propTypes = {
|
||||
data: PropTypes.object
|
||||
};
|
||||
|
||||
state = {
|
||||
// minPackageRequests: 1000000,
|
||||
minCountryRequests: 1000000
|
||||
@ -333,3 +329,9 @@ export default class Stats extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Stats.propTypes = {
|
||||
data: PropTypes.object
|
||||
};
|
||||
}
|
||||
|
||||
@ -6,10 +6,6 @@ import { addEvent, removeEvent } from '../utils/dom';
|
||||
const resizeEvent = 'resize';
|
||||
|
||||
export default class WindowSize extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func
|
||||
};
|
||||
|
||||
handleWindowResize = () => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange({
|
||||
@ -31,3 +27,9 @@ export default class WindowSize extends React.Component {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
WindowSize.propTypes = {
|
||||
onChange: PropTypes.func
|
||||
};
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { https } from 'firebase-functions';
|
||||
|
||||
// import serveAuth from './serveAuth';
|
||||
import serveAutoIndexPage from './serveAutoIndexPage';
|
||||
import serveMainPage from './serveMainPage';
|
||||
import serveNpmPackageFile from './serveNpmPackageFile';
|
||||
import servePublicKey from './servePublicKey';
|
||||
import serveStats from './serveStats';
|
||||
@ -9,6 +10,7 @@ import serveStats from './serveStats';
|
||||
export default {
|
||||
// serveAuth: https.onRequest(serveAuth),
|
||||
serveAutoIndexPage: https.onRequest(serveAutoIndexPage),
|
||||
serveMainPage: https.onRequest(serveMainPage),
|
||||
serveNpmPackageFile: https.onRequest(serveNpmPackageFile),
|
||||
servePublicKey: https.onRequest(servePublicKey),
|
||||
serveStats: https.onRequest(serveStats)
|
||||
|
||||
10
modules/functions/serveMainPage.js
Normal file
10
modules/functions/serveMainPage.js
Normal file
@ -0,0 +1,10 @@
|
||||
import express from 'express';
|
||||
|
||||
import serveMainPage from '../actions/serveMainPage';
|
||||
|
||||
const app = express();
|
||||
|
||||
app.disable('x-powered-by');
|
||||
app.use(serveMainPage);
|
||||
|
||||
export default app;
|
||||
19
modules/utils/getEntryPoints.js
Normal file
19
modules/utils/getEntryPoints.js
Normal file
@ -0,0 +1,19 @@
|
||||
import invariant from 'invariant';
|
||||
|
||||
// Virtual module id; see rollup.config.js
|
||||
import entryManifest from 'entry-manifest';
|
||||
|
||||
export default function getEntryPoints(entryName, formatKeys) {
|
||||
const manifest = entryManifest[entryName];
|
||||
|
||||
invariant(manifest, 'Invalid entry name: %s', entryName);
|
||||
|
||||
return manifest.reduce((memo, entryPoint) => {
|
||||
if (entryPoint.format in formatKeys) {
|
||||
const key = formatKeys[entryPoint.format];
|
||||
memo[key] = entryPoint.url;
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, {});
|
||||
}
|
||||
@ -3,9 +3,9 @@ import ReactDOMServer from 'react-dom/server';
|
||||
|
||||
const doctype = '<!DOCTYPE html>';
|
||||
|
||||
export default function renderPage(page, props) {
|
||||
export default function renderTemplate(component, props) {
|
||||
return (
|
||||
doctype +
|
||||
ReactDOMServer.renderToStaticMarkup(React.createElement(page, props))
|
||||
ReactDOMServer.renderToStaticMarkup(React.createElement(component, props))
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user