diff --git a/client/main.js b/client/main.js deleted file mode 100644 index ef7121d..0000000 --- a/client/main.js +++ /dev/null @@ -1,8 +0,0 @@ -import "./main.css"; - -import React from "react"; -import ReactDOM from "react-dom"; - -import App from "./main/App"; - -ReactDOM.render(, document.getElementById("root")); diff --git a/client/main/About.js b/client/main/About.js deleted file mode 100644 index bee1b31..0000000 --- a/client/main/About.js +++ /dev/null @@ -1,11 +0,0 @@ -import "./About.css"; - -import React from "react"; - -import html from "./About.md"; - -function About() { - return
; -} - -export default About; diff --git a/client/main/App.js b/client/main/App.js deleted file mode 100644 index 19fd6b5..0000000 --- a/client/main/App.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; -import { HashRouter } from "react-router-dom"; -import Layout from "./Layout"; - -const App = () => ( - - - -); - -export default App; diff --git a/client/main/Home.js b/client/main/Home.js deleted file mode 100644 index e16d897..0000000 --- a/client/main/Home.js +++ /dev/null @@ -1,11 +0,0 @@ -import "./Home.css"; - -import React from "react"; - -import html from "./Home.md"; - -function Home() { - return
; -} - -export default Home; diff --git a/client/utils/formatNumber.js b/client/utils/formatNumber.js deleted file mode 100644 index 94b33f9..0000000 --- a/client/utils/formatNumber.js +++ /dev/null @@ -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; diff --git a/client/utils/formatPercent.js b/client/utils/formatPercent.js deleted file mode 100644 index 9f68ac9..0000000 --- a/client/utils/formatPercent.js +++ /dev/null @@ -1,4 +0,0 @@ -const formatPercent = (n, fixed = 1) => - String((n.toPrecision(2) * 100).toFixed(fixed)); - -export default formatPercent; diff --git a/client/utils/parseNumber.js b/client/utils/parseNumber.js deleted file mode 100644 index 9ab1a6d..0000000 --- a/client/utils/parseNumber.js +++ /dev/null @@ -1,3 +0,0 @@ -const parseNumber = s => parseInt(s.replace(/,/g, ""), 10) || 0; - -export default parseNumber; diff --git a/docker-compose.yml b/docker-compose.yml index fd36f81..8a78cc4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/package.json b/package.json index 2c403b5..313c725 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server.js b/server.js index a22c19a..ab7ea5f 100644 --- a/server.js +++ b/server.js @@ -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, { diff --git a/server/actions/serveAutoIndexPage.js b/server/actions/serveAutoIndexPage.js new file mode 100644 index 0000000..8bd49e3 --- /dev/null +++ b/server/actions/serveAutoIndexPage.js @@ -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; diff --git a/server/actions/serveFile.js b/server/actions/serveFile.js index 240bb14..366ee91 100644 --- a/server/actions/serveFile.js +++ b/server/actions/serveFile.js @@ -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) { diff --git a/server/actions/serveJavaScriptModule.js b/server/actions/serveJavaScriptModule.js new file mode 100644 index 0000000..6afc9a5 --- /dev/null +++ b/server/actions/serveJavaScriptModule.js @@ -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; diff --git a/server/actions/serveMainPage.js b/server/actions/serveMainPage.js index 9bb3f09..028f0a6 100644 --- a/server/actions/serveMainPage.js +++ b/server/actions/serveMainPage.js @@ -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; diff --git a/server/actions/serveMetadata.js b/server/actions/serveMetadata.js new file mode 100644 index 0000000..e9449a2 --- /dev/null +++ b/server/actions/serveMetadata.js @@ -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; diff --git a/server/actions/serveStaticFile.js b/server/actions/serveStaticFile.js new file mode 100644 index 0000000..3a382ad --- /dev/null +++ b/server/actions/serveStaticFile.js @@ -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; diff --git a/client/.babelrc b/server/client/.babelrc similarity index 100% rename from client/.babelrc rename to server/client/.babelrc diff --git a/client/.eslintrc b/server/client/.eslintrc similarity index 100% rename from client/.eslintrc rename to server/client/.eslintrc diff --git a/server/client/MainPage.js b/server/client/MainPage.js new file mode 100644 index 0000000..bfb1532 --- /dev/null +++ b/server/client/MainPage.js @@ -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 ( + + + + + + + + + {styles.map(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} + + +
+ {scripts.map(s =>