mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-24 18:28:17 +00:00
Implement a first-pass static site generation
Summary: Some of the code here is adapted from my site (source available on GitHub at wchargin/wchargin.github.io). It has been improved when possible and made worse when necessary to fit into our existing build system with minimal churn. As of this commit, there remain the following outstanding tasks: - Use a non-hardcoded list of paths in static site generation router. This is not trivial. We have the paths nicely available in `routes.js`, but this module is written in ES6, and transitively depends on many files written in ES6 (i.e., the whole app). Yet naïvely it would be required from a Webpack config file, which is interpreted as vanilla JavaScript. - Add `csso-loader` to minify our CSS. This is easy. - Add unit tests for `dedent`. (As is, it comes from my site verbatim. I wrote it. dmnd’s `dedent` package on npm is insufficient because it dedents arguments as well as the format string, which is incorrect at least for our purposes.) - Link in canonical static data for the site. - Rip out the whole build system and replace it with my build config, which is orders of magnitude saner and less bad. (By “the whole build system” I mostly mean `webpack.config.{dev,prod}.js`.) Test Plan: ```shell $ yarn backend $ yarn build $ node ./bin/sourcecred.js start ``` wchargin-branch: static-v0
This commit is contained in:
parent
df76975fae
commit
b41009b1f7
@ -39,6 +39,7 @@ module.exports = {
|
||||
appPublic: resolveApp("src/app/public"),
|
||||
appHtml: resolveApp("src/app/public/index.html"),
|
||||
appIndexJs: resolveApp("src/app/index.js"),
|
||||
appServerSideRenderingIndexJs: resolveApp("src/app/server.js"),
|
||||
appPackageJson: resolveApp("package.json"),
|
||||
appSrc: resolveApp("src"),
|
||||
yarnLockFile: resolveApp("yarn.lock"),
|
||||
|
@ -1,12 +1,9 @@
|
||||
// @no-flow
|
||||
const autoprefixer = require("autoprefixer");
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const ExtractTextPlugin = require("extract-text-webpack-plugin");
|
||||
const ManifestPlugin = require("webpack-manifest-plugin");
|
||||
const InterpolateHtmlPlugin = require("react-dev-utils/InterpolateHtmlPlugin");
|
||||
const SWPrecacheWebpackPlugin = require("sw-precache-webpack-plugin");
|
||||
const StaticSiteGeneratorPlugin = require("static-site-generator-webpack-plugin");
|
||||
const eslintFormatter = require("react-dev-utils/eslintFormatter");
|
||||
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin");
|
||||
const paths = require("./paths");
|
||||
@ -15,9 +12,6 @@ const getClientEnvironment = require("./env");
|
||||
// Webpack uses `publicPath` to determine where the app is being served from.
|
||||
// It requires a trailing slash, or the file assets will get an incorrect path.
|
||||
const publicPath = paths.servedPath;
|
||||
// Some apps do not use client-side routing with pushState.
|
||||
// For these, "homepage" can be set to "." to enable relative asset paths.
|
||||
const shouldUseRelativeAssetPaths = publicPath === "./";
|
||||
// Source maps are resource heavy and can cause out of memory issue for large source files.
|
||||
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false";
|
||||
// `publicUrl` is just like `publicPath`, but we will provide it to our app
|
||||
@ -33,18 +27,6 @@ if (env.stringified["process.env"].NODE_ENV !== '"production"') {
|
||||
throw new Error("Production builds must have NODE_ENV=production.");
|
||||
}
|
||||
|
||||
// Note: defined here because it will be used more than once.
|
||||
const cssFilename = "static/css/[name].[contenthash:8].css";
|
||||
|
||||
// ExtractTextPlugin expects the build output to be flat.
|
||||
// (See https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/27)
|
||||
// However, our output is structured with css, js and media folders.
|
||||
// To have this structure working with relative paths, we have to use custom options.
|
||||
const extractTextPluginOptions = shouldUseRelativeAssetPaths
|
||||
? // Making sure that the publicPath goes back to to build folder.
|
||||
{publicPath: Array(cssFilename.split("/").length).join("../")}
|
||||
: {};
|
||||
|
||||
// This is the production configuration.
|
||||
// It compiles slowly and is focused on producing a fast and minimal bundle.
|
||||
// The development configuration is different and lives in a separate file.
|
||||
@ -55,7 +37,10 @@ module.exports = {
|
||||
// You can exclude the *.map files from the build during deployment.
|
||||
devtool: shouldUseSourceMap ? "source-map" : false,
|
||||
// In production, we only want to load the polyfills and the app code.
|
||||
entry: [require.resolve("./polyfills"), paths.appIndexJs],
|
||||
entry: {
|
||||
main: [require.resolve("./polyfills"), paths.appIndexJs],
|
||||
ssr: [require.resolve("./polyfills"), paths.appServerSideRenderingIndexJs],
|
||||
},
|
||||
output: {
|
||||
// The build folder.
|
||||
path: paths.appBuild,
|
||||
@ -71,6 +56,8 @@ module.exports = {
|
||||
path
|
||||
.relative(paths.appSrc, info.absoluteResourcePath)
|
||||
.replace(/\\/g, "/"),
|
||||
// We need to use a UMD module to build the static site.
|
||||
libraryTarget: "umd",
|
||||
},
|
||||
resolve: {
|
||||
// This allows you to set a fallback for where Webpack should look for modules.
|
||||
@ -149,64 +136,9 @@ module.exports = {
|
||||
compact: true,
|
||||
},
|
||||
},
|
||||
// The notation here is somewhat confusing.
|
||||
// "postcss" loader applies autoprefixer to our CSS.
|
||||
// "css" loader resolves paths in CSS and adds assets as dependencies.
|
||||
// "style" loader normally turns CSS into JS modules injecting <style>,
|
||||
// but unlike in development configuration, we do something different.
|
||||
// `ExtractTextPlugin` first applies the "postcss" and "css" loaders
|
||||
// (second argument), then grabs the result CSS and puts it into a
|
||||
// separate file in our build process. This way we actually ship
|
||||
// a single CSS file in production instead of JS code injecting <style>
|
||||
// tags. If you use code splitting, however, any async bundles will still
|
||||
// use the "style" loader inside the async code so CSS from them won't be
|
||||
// in the main CSS file.
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: ExtractTextPlugin.extract(
|
||||
Object.assign(
|
||||
{
|
||||
fallback: {
|
||||
loader: require.resolve("style-loader"),
|
||||
options: {
|
||||
hmr: false,
|
||||
},
|
||||
},
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
minimize: true,
|
||||
sourceMap: shouldUseSourceMap,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("postcss-loader"),
|
||||
options: {
|
||||
// Necessary for external CSS imports to work
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2677
|
||||
ident: "postcss",
|
||||
plugins: () => [
|
||||
require("postcss-flexbugs-fixes"),
|
||||
autoprefixer({
|
||||
browsers: [
|
||||
">1%",
|
||||
"last 4 versions",
|
||||
"Firefox ESR",
|
||||
"not ie < 9", // React doesn't support IE8 anyway
|
||||
],
|
||||
flexbox: "no-2009",
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
extractTextPluginOptions
|
||||
)
|
||||
),
|
||||
// Note: this won't work without `new ExtractTextPlugin()` in `plugins`.
|
||||
loader: "css-loader", // TODO(@wchargin): add csso-loader
|
||||
},
|
||||
// "file" loader makes sure assets end up in the `build` folder.
|
||||
// When you `import` an asset, you get its filename.
|
||||
@ -230,28 +162,10 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// In production, it will be an empty string unless you specify "homepage"
|
||||
// in `package.json`, in which case it will be the pathname of that URL.
|
||||
new InterpolateHtmlPlugin(env.raw),
|
||||
// Generates an `index.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
template: paths.appHtml,
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeRedundantAttributes: true,
|
||||
useShortDoctype: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
keepClosingSlash: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
minifyURLs: true,
|
||||
},
|
||||
new StaticSiteGeneratorPlugin({
|
||||
entry: "ssr",
|
||||
paths: /* TODO(@wchargin): Non-hard-coded routes */ ["/", "/explorer"],
|
||||
locals: {},
|
||||
}),
|
||||
// Makes some environment variables available to the JS code, for example:
|
||||
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
|
||||
@ -279,10 +193,6 @@ module.exports = {
|
||||
},
|
||||
sourceMap: shouldUseSourceMap,
|
||||
}),
|
||||
// Note: this won't work without ExtractTextPlugin.extract(..) in `loaders`.
|
||||
new ExtractTextPlugin({
|
||||
filename: cssFilename,
|
||||
}),
|
||||
// Generate a manifest file which contains a mapping of all asset filenames
|
||||
// to their corresponding output file so that tools can pick it up without
|
||||
// having to parse `index.html`.
|
||||
|
41
src/app/dedent.js
Normal file
41
src/app/dedent.js
Normal file
@ -0,0 +1,41 @@
|
||||
// @flow
|
||||
|
||||
/*
|
||||
* A template tag function that performs dedenting on the template, but
|
||||
* not its arguments.
|
||||
*
|
||||
* For instance, given the template
|
||||
*
|
||||
* |dedent`\
|
||||
* | one ${one}
|
||||
* | two ${two}
|
||||
* | done`,
|
||||
*
|
||||
* where `one === "1"` and `two === "\n 2"`, the template string
|
||||
* would expand to "one 1\n two\n 2\ndone". Note that four spaces
|
||||
* of indentation were stripped off of each of "one" and "two", but not
|
||||
* from "2".
|
||||
*
|
||||
* Lines that contain only whitespace are not used for measuring.
|
||||
*/
|
||||
export default function dedent(strings: string[], ...values: string[]) {
|
||||
const lineLengths = strings
|
||||
.join("")
|
||||
.split("\n")
|
||||
.filter((line) => line.trim().length !== 0)
|
||||
.map((line) => line.length - line.trimLeft().length);
|
||||
const trimAmount = Math.min.apply(null, lineLengths);
|
||||
|
||||
const parts = [];
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
const trimmed = strings[i]
|
||||
.split("\n")
|
||||
.map((line, j) => (i === 0 || j > 0 ? line.substr(trimAmount) : line))
|
||||
.join("\n");
|
||||
parts.push(trimmed);
|
||||
if (i < values.length) {
|
||||
parts.push(values[i]);
|
||||
}
|
||||
}
|
||||
return parts.join("");
|
||||
}
|
50
src/app/server.js
Normal file
50
src/app/server.js
Normal file
@ -0,0 +1,50 @@
|
||||
// @flow
|
||||
|
||||
import {StyleSheetServer} from "aphrodite/no-important";
|
||||
import React from "react";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import {match, RouterContext} from "react-router";
|
||||
|
||||
import {createRoutes, resolveTitleFromPath} from "./routes";
|
||||
import dedent from "./dedent";
|
||||
|
||||
export default function render(
|
||||
locals: {+path: string, +assets: {[string]: string}},
|
||||
callback: (error: ?mixed, result?: string) => void
|
||||
): void {
|
||||
const bundlePath = locals.assets["main"];
|
||||
const url = locals.path;
|
||||
const routes = createRoutes();
|
||||
match({routes, location: url}, (error, redirectLocation, renderProps) => {
|
||||
if (error) {
|
||||
callback(error);
|
||||
} else if (renderProps) {
|
||||
const component = <RouterContext {...renderProps} />;
|
||||
const {html, css} = StyleSheetServer.renderStatic(() =>
|
||||
ReactDOMServer.renderToString(component)
|
||||
);
|
||||
const page = dedent`\
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<title>${resolveTitleFromPath(url)}</title>
|
||||
<style>${require("./index.css")}</style>
|
||||
<style data-aphrodite>${css.content}</style>
|
||||
</head>
|
||||
<body style="overflow-y:scroll">
|
||||
<div id="root">${html}</div>
|
||||
<script src="${bundlePath}"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
callback(null, page);
|
||||
} else {
|
||||
// This shouldn't happen because we should only be visiting
|
||||
// the right routes.
|
||||
throw new Error(`unexpected 404 from ${url}`);
|
||||
}
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user