Better dev server integration
This commit is contained in:
parent
a22e0fa801
commit
d6f2bc089a
.gitignorewebpack.config.jsyarn.lock
client
package.jsonpublic/_polyfills
server.jsserver
actions
components
createDevCompiler.jscreateDevServer.jscreateRouter.jscreateServer.jsmiddleware
assetsManifest.jscheckBlacklist.jsdevAssets.jsfetchFile.jsparseURL.jsserveFile.jsstaticAssets.js
utils
utils
|
@ -1,7 +1,8 @@
|
|||
.DS_Store
|
||||
.env
|
||||
/node_modules
|
||||
/build
|
||||
/public/_assets
|
||||
/server/stats.json
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
{
|
||||
"presets": ["env", "react", "stage-2"],
|
||||
"plugins": []
|
||||
"presets": ["env", "stage-2", "react"]
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="description" content="A fast, global content delivery network for everything on npm">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
|
||||
<link rel="shortcut icon" href="/favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react"
|
||||
import ReactDOM from "react-dom"
|
||||
import App from "./App"
|
||||
import "./index.css"
|
||||
import "./main.css"
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById("app"))
|
18
package.json
18
package.json
|
@ -1,16 +1,17 @@
|
|||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "heroku local -f Procfile.dev -p 8081",
|
||||
"start": "webpack-dev-server --inline",
|
||||
"build": "NODE_ENV=production webpack -p",
|
||||
"start": "heroku local -f Procfile.dev",
|
||||
"build": "NODE_ENV=production webpack -p --json > server/stats.json",
|
||||
"lint": "eslint client && eslint server",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-plugin-syntax-export-extensions": "^6.13.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"body-parser": "^1.18.2",
|
||||
"cors": "^2.8.1",
|
||||
"countries-list": "^1.3.2",
|
||||
|
@ -44,25 +45,22 @@
|
|||
"whatwg-url": "^6.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.0.3",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"copy-webpack-plugin": "^4.3.0",
|
||||
"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",
|
||||
"file-loader": "0.10.0",
|
||||
"html-loader": "^0.5.1",
|
||||
"html-webpack-plugin": "2.24.0",
|
||||
"jest": "18.1.0",
|
||||
"markdown-loader": "^2.0.1",
|
||||
"style-loader": "0.13.1",
|
||||
"supertest": "^3.0.0",
|
||||
"url-loader": "0.5.7",
|
||||
"webpack": "^3.10.0",
|
||||
"webpack-dev-server": "^2.9.7",
|
||||
"webpack-dev-server": "^2.11.1",
|
||||
"whatwg-fetch": "2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -73,9 +71,5 @@
|
|||
"/node_modules/",
|
||||
"__tests__/utils"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 100,
|
||||
"semi": false
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
48
server.js
48
server.js
|
@ -1,34 +1,26 @@
|
|||
const http = require("http")
|
||||
const throng = require("throng")
|
||||
const createServer = require("./server/createServer")
|
||||
const path = require("path");
|
||||
const throng = require("throng");
|
||||
const createServer = require("./server/createServer");
|
||||
const createDevServer = require("./server/createDevServer");
|
||||
|
||||
const port = parseInt(process.env.PORT, 10) || 5000
|
||||
const port = parseInt(process.env.PORT, 10) || 5000;
|
||||
|
||||
function startServer(id) {
|
||||
const server = http.createServer(createServer())
|
||||
const server =
|
||||
process.env.NODE_ENV === "production"
|
||||
? createServer(
|
||||
path.resolve(__dirname, "public"),
|
||||
path.resolve(__dirname, "server/stats.json")
|
||||
)
|
||||
: createDevServer(
|
||||
path.resolve(__dirname, "public"),
|
||||
require("./webpack.config"),
|
||||
`http://localhost:${port}`
|
||||
);
|
||||
|
||||
// Heroku dynos automatically timeout after 30s. Set our
|
||||
// own timeout here to force sockets to close before that.
|
||||
// https://devcenter.heroku.com/articles/request-timeout
|
||||
server.setTimeout(25000, function(socket) {
|
||||
const message = `Timeout of 25 seconds exceeded`
|
||||
|
||||
socket.end(
|
||||
[
|
||||
"HTTP/1.1 503 Service Unavailable",
|
||||
"Date: " + new Date().toGMTString(),
|
||||
"Content-Length: " + Buffer.byteLength(message),
|
||||
"Content-Type: text/plain",
|
||||
"Connection: close",
|
||||
"",
|
||||
message
|
||||
].join("\r\n")
|
||||
)
|
||||
})
|
||||
|
||||
server.listen(port, function() {
|
||||
console.log("Server #%s listening on port %s, Ctrl+C to stop", id, port)
|
||||
})
|
||||
server.listen(port, () => {
|
||||
console.log("Server #%s listening on port %s, Ctrl+C to stop", id, port);
|
||||
});
|
||||
}
|
||||
|
||||
throng({
|
||||
|
@ -36,4 +28,4 @@ throng({
|
|||
lifetime: Infinity,
|
||||
grace: 25000,
|
||||
start: startServer
|
||||
})
|
||||
});
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const etag = require("etag");
|
||||
const babel = require("babel-core");
|
||||
|
||||
const IndexPage = require("../components/IndexPage");
|
||||
const renderPage = require("../utils/renderPage");
|
||||
const getMetadata = require("../utils/getMetadata");
|
||||
const getFileContentType = require("../utils/getFileContentType");
|
||||
const unpkgRewrite = require("../utils/unpkgRewriteBabelPlugin");
|
||||
const getEntries = require("../utils/getEntries");
|
||||
|
||||
/**
|
||||
* Automatically generate HTML pages that show package contents.
|
||||
*/
|
||||
const AutoIndex = !process.env.DISABLE_INDEX;
|
||||
|
||||
/**
|
||||
* Maximum recursion depth for meta listings.
|
||||
*/
|
||||
const MaximumDepth = 128;
|
||||
|
||||
function rewriteBareModuleIdentifiers(file, packageConfig, callback) {
|
||||
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)]
|
||||
};
|
||||
|
||||
babel.transformFile(file, options, (error, result) => {
|
||||
callback(error, result && result.code);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the file, JSON metadata, or HTML directory listing.
|
||||
*/
|
||||
function serveFile(req, res) {
|
||||
if (req.query.meta != null) {
|
||||
// Serve JSON metadata.
|
||||
getMetadata(
|
||||
req.packageDir,
|
||||
req.filename,
|
||||
req.stats,
|
||||
MaximumDepth,
|
||||
(error, metadata) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
|
||||
res
|
||||
.status(500)
|
||||
.type("text")
|
||||
.send(
|
||||
`Cannot generate metadata for ${req.packageSpec}${req.filename}`
|
||||
);
|
||||
} else {
|
||||
// Cache metadata for 1 year.
|
||||
res
|
||||
.set({
|
||||
"Cache-Control": "public, max-age=31536000",
|
||||
"Cache-Tag": "meta"
|
||||
})
|
||||
.send(metadata);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else if (req.stats.isFile()) {
|
||||
// Serve a file.
|
||||
const file = path.join(req.packageDir, req.filename);
|
||||
|
||||
let contentType = getFileContentType(file);
|
||||
|
||||
if (contentType === "text/html") contentType = "text/plain"; // We can't serve HTML because bad people :(
|
||||
|
||||
if (contentType === "application/javascript" && req.query.module != null) {
|
||||
// Serve a JavaScript module.
|
||||
rewriteBareModuleIdentifiers(file, req.packageConfig, (error, code) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
|
||||
const debugInfo =
|
||||
error.constructor.name +
|
||||
": " +
|
||||
error.message.replace(/^.*?\/unpkg-.+?\//, `/${req.packageSpec}/`) +
|
||||
"\n\n" +
|
||||
error.codeFrame;
|
||||
|
||||
res
|
||||
.status(500)
|
||||
.type("text")
|
||||
.send(
|
||||
`Cannot generate module for ${req.packageSpec}${
|
||||
req.filename
|
||||
}\n\n${debugInfo}`
|
||||
);
|
||||
} else {
|
||||
// Cache modules for 1 year.
|
||||
res
|
||||
.set({
|
||||
"Content-Type": `${contentType}; charset=utf-8`,
|
||||
"Content-Length": Buffer.byteLength(code),
|
||||
"Cache-Control": "public, max-age=31536000",
|
||||
"Cache-Tag": "file,js-file,js-module"
|
||||
})
|
||||
.send(code);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Serve some other static file.
|
||||
const tags = ["file"];
|
||||
|
||||
const ext = path.extname(req.filename).substr(1);
|
||||
if (ext) {
|
||||
tags.push(`${ext}-file`);
|
||||
}
|
||||
|
||||
if (contentType === "application/javascript") {
|
||||
contentType += "; charset=utf-8";
|
||||
}
|
||||
|
||||
// Cache files for 1 year.
|
||||
res.set({
|
||||
"Content-Type": contentType,
|
||||
"Content-Length": req.stats.size,
|
||||
"Cache-Control": "public, max-age=31536000",
|
||||
"Last-Modified": req.stats.mtime.toUTCString(),
|
||||
ETag: etag(req.stats),
|
||||
"Cache-Tag": tags.join(",")
|
||||
});
|
||||
|
||||
const stream = fs.createReadStream(file);
|
||||
|
||||
stream.on("error", error => {
|
||||
console.error(`Cannot send file ${req.packageSpec}${req.filename}`);
|
||||
console.error(error);
|
||||
res.sendStatus(500);
|
||||
});
|
||||
|
||||
stream.pipe(res);
|
||||
}
|
||||
} else if (AutoIndex && req.stats.isDirectory()) {
|
||||
// Serve an HTML directory listing.
|
||||
getEntries(path.join(req.packageDir, req.filename)).then(
|
||||
entries => {
|
||||
const html = renderPage(IndexPage, {
|
||||
packageInfo: req.packageInfo,
|
||||
version: req.packageVersion,
|
||||
dir: req.filename,
|
||||
entries
|
||||
});
|
||||
|
||||
// Cache HTML directory listings for 1 minute.
|
||||
res
|
||||
.set({
|
||||
"Cache-Control": "public, max-age=60",
|
||||
"Cache-Tag": "index"
|
||||
})
|
||||
.send(html);
|
||||
},
|
||||
error => {
|
||||
console.error(error);
|
||||
|
||||
res
|
||||
.status(500)
|
||||
.type("text")
|
||||
.send(`Cannot read entries for ${req.packageSpec}${req.filename}`);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
res
|
||||
.status(403)
|
||||
.type("text")
|
||||
.send(`Cannot serve ${req.packageSpec}${req.filename}; it's not a file`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = serveFile;
|
|
@ -1,15 +1,15 @@
|
|||
const React = require("react")
|
||||
const prettyBytes = require("pretty-bytes")
|
||||
const getFileContentType = require("../utils/getFileContentType")
|
||||
const React = require("react");
|
||||
const prettyBytes = require("pretty-bytes");
|
||||
const getFileContentType = require("../utils/getFileContentType");
|
||||
|
||||
const e = React.createElement
|
||||
const e = React.createElement;
|
||||
|
||||
const formatTime = time => new Date(time).toISOString()
|
||||
const formatTime = time => new Date(time).toISOString();
|
||||
|
||||
const DirectoryListing = ({ dir, entries }) => {
|
||||
function DirectoryListing({ dir, entries }) {
|
||||
const rows = entries.map(({ file, stats }, index) => {
|
||||
const isDir = stats.isDirectory()
|
||||
const href = file + (isDir ? "/" : "")
|
||||
const isDir = stats.isDirectory();
|
||||
const href = file + (isDir ? "/" : "");
|
||||
|
||||
return e(
|
||||
"tr",
|
||||
|
@ -18,10 +18,10 @@ const DirectoryListing = ({ dir, entries }) => {
|
|||
e("td", null, isDir ? "-" : getFileContentType(file)),
|
||||
e("td", null, isDir ? "-" : prettyBytes(stats.size)),
|
||||
e("td", null, isDir ? "-" : formatTime(stats.mtime))
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (dir !== "/")
|
||||
if (dir !== "/") {
|
||||
rows.unshift(
|
||||
e(
|
||||
"tr",
|
||||
|
@ -31,7 +31,8 @@ const DirectoryListing = ({ dir, entries }) => {
|
|||
e("td", null, "-"),
|
||||
e("td", null, "-")
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return e(
|
||||
"table",
|
||||
|
@ -49,7 +50,7 @@ const DirectoryListing = ({ dir, entries }) => {
|
|||
)
|
||||
),
|
||||
e("tbody", null, rows)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = DirectoryListing
|
||||
module.exports = DirectoryListing;
|
|
@ -1,23 +1,25 @@
|
|||
const React = require("react")
|
||||
const semver = require("semver")
|
||||
const DirectoryListing = require("./DirectoryListing")
|
||||
const readCSS = require("../utils/readCSS")
|
||||
const React = require("react");
|
||||
const semver = require("semver");
|
||||
const DirectoryListing = require("./DirectoryListing");
|
||||
const readCSS = require("../utils/readCSS");
|
||||
|
||||
const e = React.createElement
|
||||
const e = React.createElement;
|
||||
|
||||
const IndexPageStyle = readCSS(__dirname, "IndexPage.css")
|
||||
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)
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
const byVersion = (a, b) => (semver.lt(a, b) ? -1 : semver.gt(a, b) ? 1 : 0)
|
||||
const byVersion = (a, b) => (semver.lt(a, b) ? -1 : semver.gt(a, b) ? 1 : 0);
|
||||
|
||||
const IndexPage = ({ packageInfo, version, dir, entries }) => {
|
||||
const versions = Object.keys(packageInfo.versions).sort(byVersion)
|
||||
const options = versions.map(v => e("option", { key: v, value: v }, `${packageInfo.name}@${v}`))
|
||||
function IndexPage({ packageInfo, version, dir, 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",
|
||||
|
@ -48,7 +50,7 @@ const IndexPage = ({ packageInfo, version, dir, entries }) => {
|
|||
e("address", null, `${packageInfo.name}@${version}`)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = IndexPage
|
||||
module.exports = IndexPage;
|
|
@ -0,0 +1,76 @@
|
|||
const React = require("react");
|
||||
const PropTypes = require("prop-types");
|
||||
|
||||
const e = React.createElement;
|
||||
|
||||
function MainPage({
|
||||
title,
|
||||
description,
|
||||
scripts,
|
||||
styles,
|
||||
webpackManifest,
|
||||
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" }),
|
||||
e("script", {
|
||||
dangerouslySetInnerHTML: {
|
||||
__html:
|
||||
"window.Promise || document.write('\\x3Cscript src=\"/_polyfills/es6-promise.min.js\">\\x3C/script>\\x3Cscript>ES6Promise.polyfill()\\x3C/script>')"
|
||||
}
|
||||
}),
|
||||
e("script", {
|
||||
dangerouslySetInnerHTML: {
|
||||
__html:
|
||||
"window.fetch || document.write('\\x3Cscript src=\"/_polyfills/fetch.min.js\">\\x3C/script>')"
|
||||
}
|
||||
}),
|
||||
e("script", {
|
||||
dangerouslySetInnerHTML: {
|
||||
__html: "window.webpackManifest = " + JSON.stringify(webpackManifest)
|
||||
}
|
||||
}),
|
||||
styles.map(s => e("link", { key: s, rel: "stylesheet", href: s }))
|
||||
),
|
||||
e(
|
||||
"body",
|
||||
null,
|
||||
e("div", { id: "app", 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),
|
||||
webpackManifest: PropTypes.object,
|
||||
content: PropTypes.string
|
||||
};
|
||||
|
||||
MainPage.defaultProps = {
|
||||
title: "UNPKG",
|
||||
description: "The CDN for everything on npm",
|
||||
scripts: [],
|
||||
styles: [],
|
||||
webpackManifest: {},
|
||||
content: ""
|
||||
};
|
||||
|
||||
module.exports = MainPage;
|
|
@ -0,0 +1,42 @@
|
|||
const webpack = require("webpack");
|
||||
|
||||
/**
|
||||
* Returns a modified copy of the given webpackEntry object with
|
||||
* the moduleId in front of all other assets.
|
||||
*/
|
||||
function prependModuleId(webpackEntry, moduleId) {
|
||||
if (typeof webpackEntry === "string") {
|
||||
return [moduleId, webpackEntry];
|
||||
}
|
||||
|
||||
if (Array.isArray(webpackEntry)) {
|
||||
return [moduleId, ...webpackEntry];
|
||||
}
|
||||
|
||||
if (webpackEntry && typeof webpackEntry === "object") {
|
||||
const entry = { ...webpackEntry };
|
||||
|
||||
for (const chunkName in entry) {
|
||||
if (entry.hasOwnProperty(chunkName)) {
|
||||
entry[chunkName] = prependModuleId(entry[chunkName], moduleId);
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
throw new Error("Invalid webpack entry object");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a webpack compiler that automatically inlines the
|
||||
* webpack dev runtime in all entry points.
|
||||
*/
|
||||
function createDevCompiler(webpackConfig, webpackRuntimeModuleId) {
|
||||
return webpack({
|
||||
...webpackConfig,
|
||||
entry: prependModuleId(webpackConfig.entry, webpackRuntimeModuleId)
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = createDevCompiler;
|
|
@ -0,0 +1,49 @@
|
|||
const express = require("express");
|
||||
const morgan = require("morgan");
|
||||
const WebpackDevServer = require("webpack-dev-server");
|
||||
const devErrorHandler = require("errorhandler");
|
||||
|
||||
const devAssets = require("./middleware/devAssets");
|
||||
const createDevCompiler = require("./createDevCompiler");
|
||||
const createRouter = require("./createRouter");
|
||||
|
||||
function createDevServer(publicDir, webpackConfig, devOrigin) {
|
||||
const compiler = createDevCompiler(
|
||||
webpackConfig,
|
||||
`webpack-dev-server/client?${devOrigin}`
|
||||
);
|
||||
|
||||
const server = new WebpackDevServer(compiler, {
|
||||
// webpack-dev-middleware options
|
||||
publicPath: webpackConfig.output.publicPath,
|
||||
quiet: false,
|
||||
noInfo: false,
|
||||
stats: {
|
||||
// https://webpack.js.org/configuration/stats/
|
||||
assets: true,
|
||||
colors: true,
|
||||
version: true,
|
||||
hash: true,
|
||||
timings: true,
|
||||
chunks: false
|
||||
},
|
||||
|
||||
// webpack-dev-server options
|
||||
contentBase: false,
|
||||
before(app) {
|
||||
// This runs before webpack-dev-middleware
|
||||
app.disable("x-powered-by");
|
||||
app.use(morgan("dev"));
|
||||
}
|
||||
});
|
||||
|
||||
// This runs after webpack-dev-middleware
|
||||
server.use(devErrorHandler());
|
||||
server.use(express.static(publicDir));
|
||||
server.use(devAssets(compiler));
|
||||
server.use(createRouter());
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
module.exports = createDevServer;
|
|
@ -0,0 +1,78 @@
|
|||
const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
const cors = require("cors");
|
||||
|
||||
const renderPage = require("./utils/renderPage");
|
||||
const requireAuth = require("./middleware/requireAuth");
|
||||
const MainPage = require("./components/MainPage");
|
||||
|
||||
function route(setup) {
|
||||
const app = express.Router();
|
||||
setup(app);
|
||||
return app;
|
||||
}
|
||||
|
||||
function createRouter() {
|
||||
const app = express.Router();
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.send(
|
||||
renderPage(MainPage, {
|
||||
scripts: req.bundle.getScripts("main"),
|
||||
styles: req.bundle.getStyles("main")
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
app.use(require("./middleware/userToken"));
|
||||
|
||||
app.get("/_publicKey", require("./actions/showPublicKey"));
|
||||
|
||||
app.use(
|
||||
"/_auth",
|
||||
route(app => {
|
||||
app.post("/", require("./actions/createAuth"));
|
||||
app.get("/", require("./actions/showAuth"));
|
||||
})
|
||||
);
|
||||
|
||||
app.use(
|
||||
"/_blacklist",
|
||||
route(app => {
|
||||
app.post(
|
||||
"/",
|
||||
requireAuth("blacklist.add"),
|
||||
require("./actions/addToBlacklist")
|
||||
);
|
||||
app.get(
|
||||
"/",
|
||||
requireAuth("blacklist.read"),
|
||||
require("./actions/showBlacklist")
|
||||
);
|
||||
app.delete(
|
||||
"*",
|
||||
requireAuth("blacklist.remove"),
|
||||
require("./middleware/validatePackageURL"),
|
||||
require("./actions/removeFromBlacklist")
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
app.get("/_stats", require("./actions/showStats"));
|
||||
}
|
||||
|
||||
app.get(
|
||||
"*",
|
||||
require("./middleware/parseURL"),
|
||||
require("./middleware/checkBlacklist"),
|
||||
require("./middleware/fetchFile"),
|
||||
require("./actions/serveFile")
|
||||
);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
module.exports = createRouter;
|
|
@ -1,97 +1,67 @@
|
|||
const express = require("express")
|
||||
const bodyParser = require("body-parser")
|
||||
const morgan = require("morgan")
|
||||
const cors = require("cors")
|
||||
const http = require("http");
|
||||
const express = require("express");
|
||||
const morgan = require("morgan");
|
||||
|
||||
const checkBlacklist = require("./middleware/checkBlacklist")
|
||||
const fetchFile = require("./middleware/fetchFile")
|
||||
const parseURL = require("./middleware/parseURL")
|
||||
const requireAuth = require("./middleware/requireAuth")
|
||||
const serveFile = require("./middleware/serveFile")
|
||||
const userToken = require("./middleware/userToken")
|
||||
const validatePackageURL = require("./middleware/validatePackageURL")
|
||||
const staticAssets = require("./middleware/staticAssets");
|
||||
const createRouter = require("./createRouter");
|
||||
|
||||
morgan.token("fwd", req => {
|
||||
return req.get("x-forwarded-for").replace(/\s/g, "")
|
||||
})
|
||||
return req.get("x-forwarded-for").replace(/\s/g, "");
|
||||
});
|
||||
|
||||
function errorHandler(err, req, res, next) {
|
||||
console.error(err.stack)
|
||||
console.error(err.stack);
|
||||
|
||||
res
|
||||
.status(500)
|
||||
.type("text")
|
||||
.send("Internal Server Error")
|
||||
.send("Internal Server Error");
|
||||
|
||||
next(err)
|
||||
next(err);
|
||||
}
|
||||
|
||||
function createRouter(setup) {
|
||||
const app = express.Router()
|
||||
setup(app)
|
||||
return app
|
||||
}
|
||||
function createServer(publicDir, statsFile) {
|
||||
const app = express();
|
||||
|
||||
function createServer() {
|
||||
const app = express()
|
||||
|
||||
app.disable("x-powered-by")
|
||||
app.disable("x-powered-by");
|
||||
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
app.use(
|
||||
morgan(
|
||||
process.env.NODE_ENV === "production"
|
||||
? // Modified version of the Heroku router's log format
|
||||
// https://devcenter.heroku.com/articles/http-routing#heroku-router-log-format
|
||||
'method=:method path=":url" host=:req[host] request_id=:req[x-request-id] cf_ray=:req[cf-ray] fwd=:fwd status=:status bytes=:res[content-length]'
|
||||
: "dev"
|
||||
// Modified version of the Heroku router's log format
|
||||
// https://devcenter.heroku.com/articles/http-routing#heroku-router-log-format
|
||||
'method=:method path=":url" host=:req[host] request_id=:req[x-request-id] cf_ray=:req[cf-ray] fwd=:fwd status=:status bytes=:res[content-length]'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
app.use(errorHandler)
|
||||
app.use(errorHandler);
|
||||
app.use(express.static(publicDir, { maxAge: "365d" }));
|
||||
app.use(staticAssets(statsFile));
|
||||
app.use(createRouter());
|
||||
|
||||
app.use(
|
||||
express.static("build", {
|
||||
maxAge: "365d"
|
||||
})
|
||||
)
|
||||
const server = http.createServer(app);
|
||||
|
||||
app.use(cors())
|
||||
app.use(bodyParser.json())
|
||||
app.use(userToken)
|
||||
// Heroku dynos automatically timeout after 30s. Set our
|
||||
// own timeout here to force sockets to close before that.
|
||||
// https://devcenter.heroku.com/articles/request-timeout
|
||||
server.setTimeout(25000, socket => {
|
||||
const message = `Timeout of 25 seconds exceeded`;
|
||||
|
||||
app.get("/_publicKey", require("./actions/showPublicKey"))
|
||||
socket.end(
|
||||
[
|
||||
"HTTP/1.1 503 Service Unavailable",
|
||||
"Date: " + new Date().toGMTString(),
|
||||
"Content-Length: " + Buffer.byteLength(message),
|
||||
"Content-Type: text/plain",
|
||||
"Connection: close",
|
||||
"",
|
||||
message
|
||||
].join("\r\n")
|
||||
);
|
||||
});
|
||||
|
||||
app.use(
|
||||
"/_auth",
|
||||
createRouter(app => {
|
||||
app.post("/", require("./actions/createAuth"))
|
||||
app.get("/", require("./actions/showAuth"))
|
||||
})
|
||||
)
|
||||
|
||||
app.use(
|
||||
"/_blacklist",
|
||||
createRouter(app => {
|
||||
app.post("/", requireAuth("blacklist.add"), require("./actions/addToBlacklist"))
|
||||
app.get("/", requireAuth("blacklist.read"), require("./actions/showBlacklist"))
|
||||
app.delete(
|
||||
"*",
|
||||
requireAuth("blacklist.remove"),
|
||||
validatePackageURL,
|
||||
require("./actions/removeFromBlacklist")
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
app.get("/_stats", require("./actions/showStats"))
|
||||
}
|
||||
|
||||
app.use("/", parseURL, checkBlacklist, fetchFile, serveFile)
|
||||
|
||||
return app
|
||||
return server;
|
||||
}
|
||||
|
||||
module.exports = createServer
|
||||
module.exports = createServer;
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* An express middleware that sets req.manifest from the build manifest
|
||||
* in the given file. Should be used in production together with
|
||||
* https://github.com/soundcloud/chunk-manifest-webpack-plugin
|
||||
* to get consistent hashes.
|
||||
*/
|
||||
function assetsManifest(webpackManifestFile) {
|
||||
let manifest;
|
||||
try {
|
||||
manifest = JSON.parse(fs.readFileSync(webpackManifestFile, "utf8"));
|
||||
} catch (error) {
|
||||
invariant(
|
||||
false,
|
||||
'assetsManifest middleware cannot read the manifest file "%s"; ' +
|
||||
"run `yarn build` before starting the server",
|
||||
webpackManifestFile
|
||||
);
|
||||
}
|
||||
|
||||
return (req, res, next) => {
|
||||
req.manifest = manifest;
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = assetsManifest;
|
|
@ -1,4 +1,4 @@
|
|||
const BlacklistAPI = require("../BlacklistAPI")
|
||||
const BlacklistAPI = require("../BlacklistAPI");
|
||||
|
||||
function checkBlacklist(req, res, next) {
|
||||
BlacklistAPI.includesPackage(req.packageName).then(
|
||||
|
@ -8,19 +8,19 @@ function checkBlacklist(req, res, next) {
|
|||
res
|
||||
.status(403)
|
||||
.type("text")
|
||||
.send(`Package "${req.packageName}" is blacklisted`)
|
||||
.send(`Package "${req.packageName}" is blacklisted`);
|
||||
} else {
|
||||
next()
|
||||
next();
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error(error)
|
||||
console.error(error);
|
||||
|
||||
res.status(500).send({
|
||||
error: "Unable to fetch the blacklist"
|
||||
})
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = checkBlacklist
|
||||
module.exports = checkBlacklist;
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
const invariant = require("invariant");
|
||||
const createBundle = require("./utils/createBundle");
|
||||
|
||||
/**
|
||||
* An express middleware that sets req.bundle from the
|
||||
* latest result from a running webpack compiler (i.e. using
|
||||
* webpack-dev-middleware). Should only be used in dev.
|
||||
*/
|
||||
function devAssets(webpackCompiler) {
|
||||
let bundle;
|
||||
webpackCompiler.plugin("done", stats => {
|
||||
bundle = createBundle(stats.toJson());
|
||||
});
|
||||
|
||||
return (req, res, next) => {
|
||||
invariant(
|
||||
bundle != null,
|
||||
"devAssets middleware needs a running compiler; " +
|
||||
"use webpack-dev-middleware in front of devAssets"
|
||||
);
|
||||
|
||||
req.bundle = bundle;
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = devAssets;
|
|
@ -1,20 +1,20 @@
|
|||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const semver = require("semver")
|
||||
const createPackageURL = require("../utils/createPackageURL")
|
||||
const createSearch = require("./utils/createSearch")
|
||||
const getPackageInfo = require("./utils/getPackageInfo")
|
||||
const getPackage = require("./utils/getPackage")
|
||||
const incrementCounter = require("./utils/incrementCounter")
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const semver = require("semver");
|
||||
const createPackageURL = require("../utils/createPackageURL");
|
||||
const createSearch = require("./utils/createSearch");
|
||||
const getPackageInfo = require("./utils/getPackageInfo");
|
||||
const getPackage = require("./utils/getPackage");
|
||||
const incrementCounter = require("./utils/incrementCounter");
|
||||
|
||||
function getBasename(file) {
|
||||
return path.basename(file, path.extname(file))
|
||||
return path.basename(file, path.extname(file));
|
||||
}
|
||||
|
||||
/**
|
||||
* File extensions to look for when automatically resolving.
|
||||
*/
|
||||
const FindExtensions = ["", ".js", ".json"]
|
||||
const FindExtensions = ["", ".js", ".json"];
|
||||
|
||||
/**
|
||||
* Resolves a path like "lib/file" into "lib/file.js" or "lib/file.json"
|
||||
|
@ -22,32 +22,36 @@ const FindExtensions = ["", ".js", ".json"]
|
|||
*/
|
||||
function findFile(base, useIndex, callback) {
|
||||
FindExtensions.reduceRight((next, ext) => {
|
||||
const file = base + ext
|
||||
const file = base + ext;
|
||||
|
||||
return () => {
|
||||
fs.stat(file, (error, stats) => {
|
||||
if (error) {
|
||||
if (error.code === "ENOENT" || error.code === "ENOTDIR") {
|
||||
next()
|
||||
next();
|
||||
} else {
|
||||
callback(error)
|
||||
callback(error);
|
||||
}
|
||||
} else if (useIndex && stats.isDirectory()) {
|
||||
findFile(path.join(file, "index"), false, (error, indexFile, indexStats) => {
|
||||
if (error) {
|
||||
callback(error)
|
||||
} else if (indexFile) {
|
||||
callback(null, indexFile, indexStats)
|
||||
} else {
|
||||
next()
|
||||
findFile(
|
||||
path.join(file, "index"),
|
||||
false,
|
||||
(error, indexFile, indexStats) => {
|
||||
if (error) {
|
||||
callback(error);
|
||||
} else if (indexFile) {
|
||||
callback(null, indexFile, indexStats);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
callback(null, file, stats)
|
||||
callback(null, file, stats);
|
||||
}
|
||||
})
|
||||
}
|
||||
}, callback)()
|
||||
});
|
||||
};
|
||||
}, callback)();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -57,112 +61,134 @@ function findFile(base, useIndex, callback) {
|
|||
function fetchFile(req, res, next) {
|
||||
getPackageInfo(req.packageName, (error, packageInfo) => {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
console.error(error);
|
||||
|
||||
return res
|
||||
.status(500)
|
||||
.type("text")
|
||||
.send(`Cannot get info for package "${req.packageName}"`)
|
||||
.send(`Cannot get info for package "${req.packageName}"`);
|
||||
}
|
||||
|
||||
if (packageInfo == null || packageInfo.versions == null)
|
||||
return res
|
||||
.status(404)
|
||||
.type("text")
|
||||
.send(`Cannot find package "${req.packageName}"`)
|
||||
.send(`Cannot find package "${req.packageName}"`);
|
||||
|
||||
req.packageInfo = packageInfo
|
||||
req.packageInfo = packageInfo;
|
||||
|
||||
if (req.packageVersion in req.packageInfo.versions) {
|
||||
// A valid request for a package we haven't downloaded yet.
|
||||
req.packageConfig = req.packageInfo.versions[req.packageVersion]
|
||||
req.packageConfig = req.packageInfo.versions[req.packageVersion];
|
||||
|
||||
getPackage(req.packageConfig, (error, outputDir) => {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
console.error(error);
|
||||
res
|
||||
.status(500)
|
||||
.type("text")
|
||||
.send(`Cannot fetch package ${req.packageSpec}`)
|
||||
.send(`Cannot fetch package ${req.packageSpec}`);
|
||||
} else {
|
||||
req.packageDir = outputDir
|
||||
req.packageDir = outputDir;
|
||||
|
||||
let filename = req.filename
|
||||
let useIndex = true
|
||||
let filename = req.filename;
|
||||
let useIndex = true;
|
||||
|
||||
if (req.query.module != null) {
|
||||
// They want an ES module. Try "module", "jsnext:main", and "/"
|
||||
// https://github.com/rollup/rollup/wiki/pkg.module
|
||||
if (!filename) {
|
||||
filename = req.packageConfig.module || req.packageConfig["jsnext:main"] || "/"
|
||||
filename =
|
||||
req.packageConfig.module ||
|
||||
req.packageConfig["jsnext:main"] ||
|
||||
"/";
|
||||
}
|
||||
} else if (filename) {
|
||||
// They are requesting an explicit filename. Only try to find an
|
||||
// index file if they are NOT requesting an HTML directory listing.
|
||||
useIndex = filename[filename.length - 1] !== "/"
|
||||
} else if (req.query.main && typeof req.packageConfig[req.query.main] === "string") {
|
||||
useIndex = filename[filename.length - 1] !== "/";
|
||||
} else if (
|
||||
req.query.main &&
|
||||
typeof req.packageConfig[req.query.main] === "string"
|
||||
) {
|
||||
// They specified a custom ?main field.
|
||||
filename = req.packageConfig[req.query.main]
|
||||
filename = req.packageConfig[req.query.main];
|
||||
|
||||
incrementCounter(
|
||||
"package-json-custom-main",
|
||||
req.packageSpec + "?main=" + req.query.main,
|
||||
1
|
||||
)
|
||||
);
|
||||
} else if (typeof req.packageConfig.unpkg === "string") {
|
||||
// The "unpkg" field allows packages to explicitly declare the
|
||||
// file to serve at the bare URL (see #59).
|
||||
filename = req.packageConfig.unpkg
|
||||
filename = req.packageConfig.unpkg;
|
||||
} else if (typeof req.packageConfig.browser === "string") {
|
||||
// Fall back to the "browser" field if declared (only support strings).
|
||||
filename = req.packageConfig.browser
|
||||
filename = req.packageConfig.browser;
|
||||
|
||||
// Count which packages + versions are actually using this fallback
|
||||
// so we can warn them when we deprecate this functionality.
|
||||
// See https://github.com/unpkg/unpkg/issues/63
|
||||
incrementCounter("package-json-browser-fallback", req.packageSpec, 1)
|
||||
incrementCounter(
|
||||
"package-json-browser-fallback",
|
||||
req.packageSpec,
|
||||
1
|
||||
);
|
||||
} else {
|
||||
// Fall back to "main" or / (same as npm).
|
||||
filename = req.packageConfig.main || "/"
|
||||
filename = req.packageConfig.main || "/";
|
||||
}
|
||||
|
||||
findFile(path.join(req.packageDir, filename), useIndex, (error, file, stats) => {
|
||||
if (error) console.error(error)
|
||||
findFile(
|
||||
path.join(req.packageDir, filename),
|
||||
useIndex,
|
||||
(error, file, stats) => {
|
||||
if (error) console.error(error);
|
||||
|
||||
if (file == null) {
|
||||
return res
|
||||
.status(404)
|
||||
.type("text")
|
||||
.send(`Cannot find module "${filename}" in package ${req.packageSpec}`)
|
||||
if (file == null) {
|
||||
return res
|
||||
.status(404)
|
||||
.type("text")
|
||||
.send(
|
||||
`Cannot find module "${filename}" in package ${
|
||||
req.packageSpec
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
filename = file.replace(req.packageDir, "");
|
||||
|
||||
if (
|
||||
req.query.main != null ||
|
||||
getBasename(req.filename) !== getBasename(filename)
|
||||
) {
|
||||
// Need to redirect to the module file so relative imports resolve
|
||||
// correctly. Cache module redirects for 1 minute.
|
||||
delete req.query.main;
|
||||
res
|
||||
.set({
|
||||
"Cache-Control": "public, max-age=60",
|
||||
"Cache-Tag": "redirect,module-redirect"
|
||||
})
|
||||
.redirect(
|
||||
302,
|
||||
createPackageURL(
|
||||
req.packageName,
|
||||
req.packageVersion,
|
||||
filename,
|
||||
createSearch(req.query)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
req.filename = filename;
|
||||
req.stats = stats;
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
filename = file.replace(req.packageDir, "")
|
||||
|
||||
if (req.query.main != null || getBasename(req.filename) !== getBasename(filename)) {
|
||||
// Need to redirect to the module file so relative imports resolve
|
||||
// correctly. Cache module redirects for 1 minute.
|
||||
delete req.query.main
|
||||
res
|
||||
.set({
|
||||
"Cache-Control": "public, max-age=60",
|
||||
"Cache-Tag": "redirect,module-redirect"
|
||||
})
|
||||
.redirect(
|
||||
302,
|
||||
createPackageURL(
|
||||
req.packageName,
|
||||
req.packageVersion,
|
||||
filename,
|
||||
createSearch(req.query)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
req.filename = filename
|
||||
req.stats = stats
|
||||
next()
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
} else if (req.packageVersion in req.packageInfo["dist-tags"]) {
|
||||
// Cache tag redirects for 1 minute.
|
||||
res
|
||||
|
@ -178,12 +204,12 @@ function fetchFile(req, res, next) {
|
|||
req.filename,
|
||||
req.search
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const maxVersion = semver.maxSatisfying(
|
||||
Object.keys(req.packageInfo.versions),
|
||||
req.packageVersion
|
||||
)
|
||||
);
|
||||
|
||||
if (maxVersion) {
|
||||
// Cache semver redirects for 1 minute.
|
||||
|
@ -192,15 +218,23 @@ function fetchFile(req, res, next) {
|
|||
"Cache-Control": "public, max-age=60",
|
||||
"Cache-Tag": "redirect,semver-redirect"
|
||||
})
|
||||
.redirect(302, createPackageURL(req.packageName, maxVersion, req.filename, req.search))
|
||||
.redirect(
|
||||
302,
|
||||
createPackageURL(
|
||||
req.packageName,
|
||||
maxVersion,
|
||||
req.filename,
|
||||
req.search
|
||||
)
|
||||
);
|
||||
} else {
|
||||
res
|
||||
.status(404)
|
||||
.type("text")
|
||||
.send(`Cannot find package ${req.packageSpec}`)
|
||||
.send(`Cannot find package ${req.packageSpec}`);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = fetchFile
|
||||
module.exports = fetchFile;
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
const validateNpmPackageName = require("validate-npm-package-name")
|
||||
const parsePackageURL = require("../utils/parsePackageURL")
|
||||
const createSearch = require("./utils/createSearch")
|
||||
const validateNpmPackageName = require("validate-npm-package-name");
|
||||
const parsePackageURL = require("../utils/parsePackageURL");
|
||||
const createSearch = require("./utils/createSearch");
|
||||
|
||||
const KnownQueryParams = {
|
||||
main: true,
|
||||
meta: true,
|
||||
module: true
|
||||
}
|
||||
};
|
||||
|
||||
function isKnownQueryParam(param) {
|
||||
return !!KnownQueryParams[param]
|
||||
return !!KnownQueryParams[param];
|
||||
}
|
||||
|
||||
function queryIsKnown(query) {
|
||||
return Object.keys(query).every(isKnownQueryParam)
|
||||
return Object.keys(query).every(isKnownQueryParam);
|
||||
}
|
||||
|
||||
function sanitizeQuery(query) {
|
||||
const saneQuery = {}
|
||||
const saneQuery = {};
|
||||
|
||||
Object.keys(query).forEach(param => {
|
||||
if (isKnownQueryParam(param)) saneQuery[param] = query[param]
|
||||
})
|
||||
if (isKnownQueryParam(param)) saneQuery[param] = query[param];
|
||||
});
|
||||
|
||||
return saneQuery
|
||||
return saneQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -32,54 +32,54 @@ function sanitizeQuery(query) {
|
|||
function parseURL(req, res, next) {
|
||||
// Redirect /_meta/path to /path?meta.
|
||||
if (req.path.match(/^\/_meta\//)) {
|
||||
req.query.meta = ""
|
||||
return res.redirect(302, req.path.substr(6) + createSearch(req.query))
|
||||
req.query.meta = "";
|
||||
return res.redirect(302, req.path.substr(6) + createSearch(req.query));
|
||||
}
|
||||
|
||||
// Redirect /path?json => /path?meta
|
||||
if (req.query.json != null) {
|
||||
delete req.query.json
|
||||
req.query.meta = ""
|
||||
return res.redirect(302, req.path + createSearch(req.query))
|
||||
delete req.query.json;
|
||||
req.query.meta = "";
|
||||
return res.redirect(302, req.path + createSearch(req.query));
|
||||
}
|
||||
|
||||
// Redirect requests with unknown query params to their equivalents
|
||||
// with only known params so they can be served from the cache. This
|
||||
// prevents people using random query params designed to bust the cache.
|
||||
if (!queryIsKnown(req.query)) {
|
||||
return res.redirect(302, req.path + createSearch(sanitizeQuery(req.query)))
|
||||
return res.redirect(302, req.path + createSearch(sanitizeQuery(req.query)));
|
||||
}
|
||||
|
||||
const url = parsePackageURL(req.url)
|
||||
const url = parsePackageURL(req.url);
|
||||
|
||||
// Disallow invalid URLs.
|
||||
if (url == null) {
|
||||
return res
|
||||
.status(403)
|
||||
.type("text")
|
||||
.send(`Invalid URL: ${req.url}`)
|
||||
.send(`Invalid URL: ${req.url}`);
|
||||
}
|
||||
|
||||
const nameErrors = validateNpmPackageName(url.packageName).errors
|
||||
const nameErrors = validateNpmPackageName(url.packageName).errors;
|
||||
|
||||
// Disallow invalid package names.
|
||||
if (nameErrors) {
|
||||
const reason = nameErrors.join(", ")
|
||||
const reason = nameErrors.join(", ");
|
||||
return res
|
||||
.status(403)
|
||||
.type("text")
|
||||
.send(`Invalid package name "${url.packageName}" (${reason})`)
|
||||
.send(`Invalid package name "${url.packageName}" (${reason})`);
|
||||
}
|
||||
|
||||
req.packageName = url.packageName
|
||||
req.packageVersion = url.packageVersion
|
||||
req.packageSpec = `${url.packageName}@${url.packageVersion}`
|
||||
req.pathname = url.pathname
|
||||
req.filename = url.filename
|
||||
req.search = url.search
|
||||
req.query = url.query
|
||||
req.packageName = url.packageName;
|
||||
req.packageVersion = url.packageVersion;
|
||||
req.packageSpec = `${url.packageName}@${url.packageVersion}`;
|
||||
req.pathname = url.pathname;
|
||||
req.filename = url.filename;
|
||||
req.search = url.search;
|
||||
req.query = url.query;
|
||||
|
||||
next()
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = parseURL
|
||||
module.exports = parseURL;
|
||||
|
|
|
@ -1,154 +0,0 @@
|
|||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const etag = require("etag")
|
||||
const babel = require("babel-core")
|
||||
const getMetadata = require("./utils/getMetadata")
|
||||
const getFileContentType = require("./utils/getFileContentType")
|
||||
const getIndexHTML = require("./utils/getIndexHTML")
|
||||
const unpkgRewrite = require("./utils/unpkgRewriteBabelPlugin")
|
||||
|
||||
/**
|
||||
* Automatically generate HTML pages that show package contents.
|
||||
*/
|
||||
const AutoIndex = !process.env.DISABLE_INDEX
|
||||
|
||||
/**
|
||||
* Maximum recursion depth for meta listings.
|
||||
*/
|
||||
const MaximumDepth = 128
|
||||
|
||||
function rewriteBareModuleIdentifiers(file, packageConfig, callback) {
|
||||
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)]
|
||||
}
|
||||
|
||||
babel.transformFile(file, options, (error, result) => {
|
||||
callback(error, result && result.code)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the file, JSON metadata, or HTML directory listing.
|
||||
*/
|
||||
function serveFile(req, res) {
|
||||
if (req.query.meta != null) {
|
||||
// Serve JSON metadata.
|
||||
getMetadata(req.packageDir, req.filename, req.stats, MaximumDepth, (error, metadata) => {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
|
||||
res
|
||||
.status(500)
|
||||
.type("text")
|
||||
.send(`Cannot generate metadata for ${req.packageSpec}${req.filename}`)
|
||||
} else {
|
||||
// Cache metadata for 1 year.
|
||||
res
|
||||
.set({
|
||||
"Cache-Control": "public, max-age=31536000",
|
||||
"Cache-Tag": "meta"
|
||||
})
|
||||
.send(metadata)
|
||||
}
|
||||
})
|
||||
} else if (req.stats.isFile()) {
|
||||
// Serve a file.
|
||||
const file = path.join(req.packageDir, req.filename)
|
||||
|
||||
let contentType = getFileContentType(file)
|
||||
|
||||
if (contentType === "text/html") contentType = "text/plain" // We can't serve HTML because bad people :(
|
||||
|
||||
if (contentType === "application/javascript" && req.query.module != null) {
|
||||
// Serve a JavaScript module.
|
||||
rewriteBareModuleIdentifiers(file, req.packageConfig, (error, code) => {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
|
||||
const debugInfo =
|
||||
error.constructor.name +
|
||||
": " +
|
||||
error.message.replace(/^.*?\/unpkg-.+?\//, `/${req.packageSpec}/`) +
|
||||
"\n\n" +
|
||||
error.codeFrame
|
||||
|
||||
res
|
||||
.status(500)
|
||||
.type("text")
|
||||
.send(`Cannot generate module for ${req.packageSpec}${req.filename}\n\n${debugInfo}`)
|
||||
} else {
|
||||
// Cache modules for 1 year.
|
||||
res
|
||||
.set({
|
||||
"Content-Type": `${contentType}; charset=utf-8`,
|
||||
"Content-Length": Buffer.byteLength(code),
|
||||
"Cache-Control": "public, max-age=31536000",
|
||||
"Cache-Tag": "file,js-file,js-module"
|
||||
})
|
||||
.send(code)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Serve some other static file.
|
||||
const tags = ["file"]
|
||||
|
||||
const ext = path.extname(req.filename).substr(1)
|
||||
if (ext) tags.push(`${ext}-file`)
|
||||
|
||||
if (contentType === "application/javascript") contentType += "; charset=utf-8"
|
||||
|
||||
// Cache files for 1 year.
|
||||
res.set({
|
||||
"Content-Type": contentType,
|
||||
"Content-Length": req.stats.size,
|
||||
"Cache-Control": "public, max-age=31536000",
|
||||
"Last-Modified": req.stats.mtime.toUTCString(),
|
||||
ETag: etag(req.stats),
|
||||
"Cache-Tag": tags.join(",")
|
||||
})
|
||||
|
||||
const stream = fs.createReadStream(file)
|
||||
|
||||
stream.on("error", error => {
|
||||
console.error(`Cannot send file ${req.packageSpec}${req.filename}`)
|
||||
console.error(error)
|
||||
res.sendStatus(500)
|
||||
})
|
||||
|
||||
stream.pipe(res)
|
||||
}
|
||||
} else if (AutoIndex && req.stats.isDirectory()) {
|
||||
// Serve an HTML directory listing.
|
||||
getIndexHTML(req.packageInfo, req.packageVersion, req.packageDir, req.filename).then(
|
||||
html => {
|
||||
// Cache HTML directory listings for 1 minute.
|
||||
res
|
||||
.set({
|
||||
"Cache-Control": "public, max-age=60",
|
||||
"Cache-Tag": "index"
|
||||
})
|
||||
.send(html)
|
||||
},
|
||||
error => {
|
||||
console.error(error)
|
||||
|
||||
res
|
||||
.status(500)
|
||||
.type("text")
|
||||
.send(`Cannot generate index page for ${req.packageSpec}${req.filename}`)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
res
|
||||
.status(403)
|
||||
.type("text")
|
||||
.send(`Cannot serve ${req.packageSpec}${req.filename}; it's not a file`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = serveFile
|
|
@ -0,0 +1,30 @@
|
|||
const fs = require("fs");
|
||||
const invariant = require("invariant");
|
||||
const createBundle = require("./utils/createBundle");
|
||||
|
||||
/**
|
||||
* An express middleware that sets req.bundle from the build
|
||||
* info in the given stats file. Should be used in production.
|
||||
*/
|
||||
function staticAssets(webpackStatsFile) {
|
||||
let stats;
|
||||
try {
|
||||
stats = JSON.parse(fs.readFileSync(webpackStatsFile, "utf8"));
|
||||
} catch (error) {
|
||||
invariant(
|
||||
false,
|
||||
"staticAssets middleware cannot read the build stats in %s; " +
|
||||
"run `yarn build` before starting the server",
|
||||
webpackStatsFile
|
||||
);
|
||||
}
|
||||
|
||||
const bundle = createBundle(stats);
|
||||
|
||||
return (req, res, next) => {
|
||||
req.bundle = bundle;
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = staticAssets;
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Creates a bundle object that is stored on req.bundle.
|
||||
*/
|
||||
function createBundle(webpackStats) {
|
||||
const { publicPath, assetsByChunkName } = webpackStats;
|
||||
|
||||
const createURL = asset => publicPath + asset;
|
||||
|
||||
const getAssets = (chunks = ["main"]) =>
|
||||
(Array.isArray(chunks) ? chunks : [chunks])
|
||||
.reduce((memo, chunk) => memo.concat(assetsByChunkName[chunk] || []), [])
|
||||
.map(createURL);
|
||||
|
||||
const getScripts = (...args) =>
|
||||
getAssets(...args).filter(asset => /\.js$/.test(asset));
|
||||
|
||||
const getStyles = (...args) =>
|
||||
getAssets(...args).filter(asset => /\.css$/.test(asset));
|
||||
|
||||
return {
|
||||
createURL,
|
||||
getAssets,
|
||||
getScripts,
|
||||
getStyles
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = createBundle;
|
|
@ -1,12 +0,0 @@
|
|||
function getFileType(stats) {
|
||||
if (stats.isFile()) return "file"
|
||||
if (stats.isDirectory()) return "directory"
|
||||
if (stats.isBlockDevice()) return "blockDevice"
|
||||
if (stats.isCharacterDevice()) return "characterDevice"
|
||||
if (stats.isSymbolicLink()) return "symlink"
|
||||
if (stats.isSocket()) return "socket"
|
||||
if (stats.isFIFO()) return "fifo"
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
module.exports = getFileType
|
|
@ -1,40 +0,0 @@
|
|||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const React = require("react")
|
||||
const ReactDOMServer = require("react-dom/server")
|
||||
const getFileStats = require("./getFileStats")
|
||||
const IndexPage = require("../components/IndexPage")
|
||||
|
||||
const e = React.createElement
|
||||
|
||||
function getEntries(dir) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readdir(dir, function(error, files) {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(
|
||||
Promise.all(files.map(file => getFileStats(path.join(dir, file)))).then(statsArray => {
|
||||
return statsArray.map((stats, index) => {
|
||||
return { file: files[index], stats }
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const DOCTYPE = "<!DOCTYPE html>"
|
||||
|
||||
function createHTML(props) {
|
||||
return DOCTYPE + ReactDOMServer.renderToStaticMarkup(e(IndexPage, props))
|
||||
}
|
||||
|
||||
function getIndexHTML(packageInfo, version, baseDir, dir) {
|
||||
return getEntries(path.join(baseDir, dir)).then(entries =>
|
||||
createHTML({ packageInfo, version, dir, entries })
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = getIndexHTML
|
|
@ -0,0 +1,25 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const getFileStats = require("./getFileStats");
|
||||
|
||||
function getEntries(dir) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readdir(dir, function(error, files) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(
|
||||
Promise.all(
|
||||
files.map(file => getFileStats(path.join(dir, file)))
|
||||
).then(statsArray => {
|
||||
return statsArray.map((stats, index) => {
|
||||
return { file: files[index], stats };
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = getEntries;
|
|
@ -0,0 +1,12 @@
|
|||
function getFileType(stats) {
|
||||
if (stats.isFile()) return "file";
|
||||
if (stats.isDirectory()) return "directory";
|
||||
if (stats.isBlockDevice()) return "blockDevice";
|
||||
if (stats.isCharacterDevice()) return "characterDevice";
|
||||
if (stats.isSymbolicLink()) return "symlink";
|
||||
if (stats.isSocket()) return "socket";
|
||||
if (stats.isFIFO()) return "fifo";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
module.exports = getFileType;
|
|
@ -0,0 +1,11 @@
|
|||
const React = require("react")
|
||||
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
|
||||
}
|
||||
|
||||
module.exports = renderPage
|
|
@ -1,18 +1,15 @@
|
|||
const path = require("path")
|
||||
const webpack = require("webpack")
|
||||
const HTMLWebpackPlugin = require("html-webpack-plugin")
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin")
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
|
||||
module.exports = {
|
||||
devtool: process.env.NODE_ENV === "production" ? false : "cheap-module-source-map",
|
||||
|
||||
entry: {
|
||||
client: path.resolve(__dirname, "client/index.js")
|
||||
main: path.resolve(__dirname, "client/main.js")
|
||||
},
|
||||
|
||||
output: {
|
||||
path: path.resolve(__dirname, "build"),
|
||||
filename: "[name]-[hash:8].js"
|
||||
filename: "[name]-[hash:8].js",
|
||||
path: path.resolve(__dirname, "public/_assets"),
|
||||
publicPath: "/_assets/"
|
||||
},
|
||||
|
||||
module: {
|
||||
|
@ -20,32 +17,18 @@ module.exports = {
|
|||
{ test: /\.js$/, exclude: /node_modules/, use: ["babel-loader"] },
|
||||
{ test: /\.css$/, use: ["style-loader", "css-loader"] },
|
||||
{ test: /\.md$/, use: ["html-loader", "markdown-loader"] },
|
||||
{ test: /\.png/, use: ["file-loader"] }
|
||||
{ test: /\.png$/, use: ["file-loader"] }
|
||||
]
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development")
|
||||
}),
|
||||
new HTMLWebpackPlugin({
|
||||
title: "unpkg",
|
||||
chunks: ["client"],
|
||||
template: path.resolve(__dirname, "client/index.html")
|
||||
}),
|
||||
new CopyWebpackPlugin(["public"])
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
process.env.NODE_ENV || "development"
|
||||
)
|
||||
})
|
||||
],
|
||||
|
||||
devServer: {
|
||||
proxy: {
|
||||
"**": {
|
||||
target: "http://localhost:8081",
|
||||
bypass: req => {
|
||||
if (req.path === "/") {
|
||||
return "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
devtool:
|
||||
process.env.NODE_ENV === "production" ? false : "cheap-module-source-map"
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue