mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-23 09:48:14 +00:00
Kill the old homepage (#1874)
This commit removes the old homepage entirely. This is a prelude to removing the react-router dependency, which is needed to unblock work on the initiatives editor. Because the homepage is gone, there's now no frontend included with SourceCred. As such, we should merge #1873 alongside this one, so that our README doesn't give any patently false information to our users. Test plan: `yarn test` passes. `yarn start` and `yarn build` are no longer commands. (`yarn start2` and `yarn build2` will be renamed later).
This commit is contained in:
parent
f9d62188e4
commit
84c9122a2a
@ -12,11 +12,8 @@ module.exports = {
|
||||
root: appDirectory,
|
||||
dotenv: resolveApp(".env"),
|
||||
favicon: resolveApp("src/assets/logo/rasterized/logo_32.png"),
|
||||
appBuild: resolveApp("build"),
|
||||
appBuild2: resolveApp("build2"),
|
||||
appIndexJs: resolveApp("src/homepage/index.js"),
|
||||
appIndexJs2: resolveApp("src/ui/index.js"),
|
||||
appServerSideRenderingIndexJs: resolveApp("src/homepage/server.js"),
|
||||
appServerSideRenderingIndexJs2: resolveApp("src/ui/server.js"),
|
||||
appPackageJson: resolveApp("package.json"),
|
||||
appSrc: resolveApp("src"),
|
||||
|
@ -1,255 +0,0 @@
|
||||
// @flow
|
||||
const express = require("express");
|
||||
/*::
|
||||
import type {
|
||||
$Application as ExpressApp,
|
||||
$Response as ExpressResponse,
|
||||
} from "express";
|
||||
*/
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const RemoveBuildDirectoryPlugin = require("./RemoveBuildDirectoryPlugin");
|
||||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
const ManifestPlugin = require("webpack-manifest-plugin");
|
||||
const StaticSiteGeneratorPlugin = require("static-site-generator-webpack-plugin");
|
||||
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin");
|
||||
const paths = require("./paths");
|
||||
const getClientEnvironment = require("./env");
|
||||
const _getProjectIds = require("../src/core/_getProjectIds");
|
||||
|
||||
// Source maps are resource heavy and can cause out of memory issue for large source files.
|
||||
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false";
|
||||
|
||||
function loadProjectIds() /*: Promise<$ReadOnlyArray<string>> */ {
|
||||
const env = process.env.SOURCECRED_DIRECTORY;
|
||||
// TODO(#945): de-duplicate finding the directory with src/cli/common.js
|
||||
const defaultDirectory = path.join(os.tmpdir(), "sourcecred");
|
||||
const scDirectory = env != null ? env : defaultDirectory;
|
||||
return _getProjectIds(scDirectory);
|
||||
}
|
||||
|
||||
async function makeConfig(
|
||||
mode /*: "production" | "development" */
|
||||
) /*: Promise<mixed> */ {
|
||||
return {
|
||||
// Don't attempt to continue if there are any errors.
|
||||
bail: true,
|
||||
// We generate sourcemaps in production. This is slow but gives good results.
|
||||
// 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: {
|
||||
main: [require.resolve("./polyfills"), paths.appIndexJs],
|
||||
ssr: [
|
||||
require.resolve("./polyfills"),
|
||||
paths.appServerSideRenderingIndexJs,
|
||||
],
|
||||
},
|
||||
devServer: {
|
||||
inline: false,
|
||||
before: (app /*: ExpressApp */) => {
|
||||
const apiRoot = "/api/v1/data";
|
||||
const rejectCache = (_unused_req, res /*: ExpressResponse */) => {
|
||||
res.status(400).send("Bad Request: Cache unavailable at runtime\n");
|
||||
};
|
||||
app.get(`${apiRoot}/cache`, rejectCache);
|
||||
app.get(`${apiRoot}/cache/*`, rejectCache);
|
||||
app.use(
|
||||
apiRoot,
|
||||
express.static(
|
||||
process.env.SOURCECRED_DIRECTORY ||
|
||||
path.join(os.tmpdir(), "sourcecred")
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
output: {
|
||||
// The build folder.
|
||||
path: paths.appBuild,
|
||||
// Generated JS file names (with nested folders).
|
||||
// There will be one main bundle, and one file per asynchronous chunk.
|
||||
// We don't currently advertise code splitting but Webpack supports it.
|
||||
filename: "static/js/[name].[chunkhash:8].js",
|
||||
chunkFilename: "static/js/[name].[chunkhash:8].chunk.js",
|
||||
// Point sourcemap entries to original disk location (format as URL on Windows)
|
||||
devtoolModuleFilenameTemplate: (
|
||||
info /*:
|
||||
{|
|
||||
// https://webpack.js.org/configuration/output/#output-devtoolmodulefilenametemplate
|
||||
+absoluteResourcePath: string,
|
||||
+allLoaders: string,
|
||||
+hash: string,
|
||||
+id: string,
|
||||
+loaders: string,
|
||||
+resource: string,
|
||||
+resourcePath: string,
|
||||
+namespace: string,
|
||||
|}
|
||||
*/
|
||||
) =>
|
||||
path
|
||||
.relative(paths.appSrc, info.absoluteResourcePath)
|
||||
.replace(/\\/g, "/"),
|
||||
// We need to use a UMD module to build the static site.
|
||||
libraryTarget: "umd",
|
||||
globalObject: "this",
|
||||
},
|
||||
resolve: {
|
||||
// This allows you to set a fallback for where Webpack should look for modules.
|
||||
// We placed these paths second because we want `node_modules` to "win"
|
||||
// if there are any conflicts. This matches Node resolution mechanism.
|
||||
// https://github.com/facebookincubator/create-react-app/issues/253
|
||||
modules: [
|
||||
"node_modules",
|
||||
paths.appNodeModules,
|
||||
...(process.env.NODE_PATH || "").split(path.delimiter).filter(Boolean),
|
||||
],
|
||||
// These are the reasonable defaults supported by the Node ecosystem.
|
||||
// We also include JSX as a common component filename extension to support
|
||||
// some tools, although we do not recommend using it, see:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/290
|
||||
// `web` extension prefixes have been added for better support
|
||||
// for React Native Web.
|
||||
extensions: [".web.js", ".mjs", ".js", ".json", ".web.jsx", ".jsx"],
|
||||
alias: {
|
||||
// Support React Native Web
|
||||
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
|
||||
"react-native": "react-native-web",
|
||||
},
|
||||
plugins: [
|
||||
// Prevents users from importing files from outside of src/ (or node_modules/).
|
||||
// This often causes confusion because we only process files within src/ with babel.
|
||||
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
|
||||
// please link the files into your node_modules/ and let module-resolution kick in.
|
||||
// Make sure your source files are compiled, as they will not be processed in any way.
|
||||
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
|
||||
],
|
||||
},
|
||||
module: {
|
||||
strictExportPresence: true,
|
||||
rules: [
|
||||
// TODO: Disable require.ensure as it's not a standard language feature.
|
||||
// We are waiting for https://github.com/facebookincubator/create-react-app/issues/2176.
|
||||
// { parser: { requireEnsure: false } },
|
||||
{
|
||||
// "oneOf" will traverse all following loaders until one will
|
||||
// match the requirements. When no loader matches it will fall
|
||||
// back to the "file" loader at the end of the loader list.
|
||||
oneOf: [
|
||||
// "url" loader works just like "file" loader but it also embeds
|
||||
// assets smaller than specified size as data URLs to avoid requests.
|
||||
{
|
||||
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
|
||||
loader: require.resolve("url-loader"),
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: "static/media/[name].[hash:8].[ext]",
|
||||
},
|
||||
},
|
||||
// Process JS with Babel.
|
||||
{
|
||||
test: /\.(js|jsx|mjs)$/,
|
||||
include: paths.appSrc,
|
||||
loader: require.resolve("babel-loader"),
|
||||
options: {
|
||||
compact: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: "css-loader", // TODO(@wchargin): add csso-loader
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
exclude: /node_modules/,
|
||||
loader: "svg-react-loader",
|
||||
},
|
||||
// "file" loader makes sure assets end up in the `build` folder.
|
||||
// When you `import` an asset, you get its filename.
|
||||
// This loader doesn't use a "test" so it will catch all modules
|
||||
// that fall through the other loaders.
|
||||
{
|
||||
loader: require.resolve("file-loader"),
|
||||
// Exclude `js` files to keep "css" loader working as it injects
|
||||
// it's runtime that would otherwise processed through "file" loader.
|
||||
// Also exclude `html` and `json` extensions so they get processed
|
||||
// by webpacks internal loaders.
|
||||
exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/],
|
||||
options: {
|
||||
name: "static/media/[name].[hash:8].[ext]",
|
||||
},
|
||||
},
|
||||
// ** STOP ** Are you adding a new loader?
|
||||
// Make sure to add the new loader(s) before the "file" loader.
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: await plugins(mode),
|
||||
// Some libraries import Node modules but don't use them in the browser.
|
||||
// Tell Webpack to provide empty mocks for them so importing them works.
|
||||
node: {
|
||||
dgram: "empty",
|
||||
fs: "empty",
|
||||
net: "empty",
|
||||
tls: "empty",
|
||||
child_process: "empty",
|
||||
},
|
||||
mode,
|
||||
};
|
||||
}
|
||||
|
||||
async function plugins(mode /*: "development" | "production" */) {
|
||||
const projectIds = await loadProjectIds();
|
||||
const env = getClientEnvironment(projectIds);
|
||||
const basePlugins = [
|
||||
new StaticSiteGeneratorPlugin({
|
||||
entry: "ssr",
|
||||
paths: require("../src/homepage/routeData")
|
||||
.makeRouteData(projectIds)
|
||||
.map(({path}) => path),
|
||||
locals: {},
|
||||
}),
|
||||
new CopyPlugin([{from: paths.favicon, to: "favicon.png"}]),
|
||||
// Makes some environment variables available to the JS code, for example:
|
||||
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
|
||||
// It is absolutely essential that NODE_ENV was set to production here.
|
||||
// Otherwise React will be compiled in the very slow development mode.
|
||||
new webpack.DefinePlugin(env.stringified),
|
||||
// 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`.
|
||||
new ManifestPlugin({
|
||||
fileName: "asset-manifest.json",
|
||||
}),
|
||||
// Moment.js is an extremely popular library that bundles large locale files
|
||||
// by default due to how Webpack interprets its code. This is a practical
|
||||
// solution that requires the user to opt into importing specific locales.
|
||||
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
|
||||
// You can remove this if you don't use Moment.js:
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||
];
|
||||
const prodOnlyPlugins = [
|
||||
// Remove the output directory before starting the build.
|
||||
new RemoveBuildDirectoryPlugin(),
|
||||
];
|
||||
switch (mode) {
|
||||
case "development":
|
||||
return basePlugins;
|
||||
case "production":
|
||||
return basePlugins.concat(prodOnlyPlugins);
|
||||
default:
|
||||
throw new Error(/*:: (*/ mode /*: empty) */);
|
||||
}
|
||||
}
|
||||
|
||||
function getMode() {
|
||||
const mode = process.env.NODE_ENV;
|
||||
if (mode !== "production" && mode !== "development") {
|
||||
throw new Error("unknown mode: " + String(mode));
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
module.exports = makeConfig(getMode());
|
@ -1,209 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
printf 'usage: build_static_site.sh --target TARGET\n'
|
||||
printf ' [--project PROJECT [...]]\n'
|
||||
printf ' [--project-file PROJECT_FILE [...]]\n'
|
||||
printf ' [--weights WEIGHTS_FILE]\n'
|
||||
printf ' [--cname DOMAIN]\n'
|
||||
printf ' [--no-backend]\n'
|
||||
printf ' [-h|--help]\n'
|
||||
printf '\n'
|
||||
printf 'Build the static SourceCred website, including example data.\n'
|
||||
printf '\n'
|
||||
printf '%s\n' '--target TARGET'
|
||||
printf '\t%s\n' 'an empty directory into which to build the site'
|
||||
printf '%s\n' '--project PROJECT'
|
||||
printf '\t%s\n' 'a project spec; see help for cli/load.js for details'
|
||||
printf '%s\n' '--project-file PROJECT_FILE'
|
||||
printf '\t%s\n' 'the path to a file containing a project config'
|
||||
printf '%s\n' '--weights WEIGHTS_FILE'
|
||||
printf '\t%s\n' 'path to a json file which contains a weights configuration.'
|
||||
printf '\t%s\n' 'This will be used instead of the default weights and persisted.'
|
||||
printf '%s\n' '--cname DOMAIN'
|
||||
printf '\t%s\n' 'configure DNS for a GitHub Pages site to point to'
|
||||
printf '\t%s\n' 'the provided custom domain'
|
||||
printf '%s\n' '--no-backend'
|
||||
printf '\t%s\n' 'do not run "yarn backend"; see also the SOURCECRED_BIN'
|
||||
printf '\t%s\n' 'environment variable'
|
||||
printf '%s\n' '-h|--help'
|
||||
printf '\t%s\n' 'show this message'
|
||||
printf '\n'
|
||||
printf 'Environment variables:\n'
|
||||
printf '\n'
|
||||
printf '%s\n' 'SOURCECRED_BIN'
|
||||
printf '\t%s\n' 'When using --no-backend, directory containing the'
|
||||
printf '\t%s\n' 'SourceCred executables (output of "yarn backend").'
|
||||
printf '\t%s\n' 'Default is ./bin. Ignored without --no-backend.'
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
toplevel="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
|
||||
cd "${toplevel}"
|
||||
|
||||
sourcecred_data=
|
||||
sourcecred_bin=
|
||||
trap cleanup EXIT
|
||||
|
||||
build
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
BACKEND=1
|
||||
target=
|
||||
cname=
|
||||
weights=
|
||||
repos=( )
|
||||
projects=( )
|
||||
project_files=( )
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--target)
|
||||
if [ -n "${target}" ]; then
|
||||
die '--target specified multiple times'
|
||||
fi
|
||||
shift
|
||||
if [ $# -eq 0 ]; then die 'missing value for --target'; fi
|
||||
target="$1"
|
||||
;;
|
||||
--weights)
|
||||
if [ -n "${weights}" ]; then
|
||||
die '--weights specified multiple times'
|
||||
fi
|
||||
shift
|
||||
if [ $# -eq 0 ]; then die 'missing value for --weights'; fi
|
||||
weights="$1"
|
||||
;;
|
||||
--project)
|
||||
shift
|
||||
if [ $# -eq 0 ]; then die 'missing value for --project'; fi
|
||||
projects+=( "$1" )
|
||||
;;
|
||||
--project-file)
|
||||
shift
|
||||
if [ $# -eq 0 ]; then die 'missing value for --project-file'; fi
|
||||
project_files+=( "$1" )
|
||||
;;
|
||||
--cname)
|
||||
shift
|
||||
if [ $# -eq 0 ]; then die 'missing value for --cname'; fi
|
||||
if [ -n "${cname}" ]; then
|
||||
die '--cname specified multiple times'
|
||||
fi
|
||||
cname="$1"
|
||||
if [ -z "${cname}" ]; then
|
||||
die 'empty value for --cname'
|
||||
fi
|
||||
;;
|
||||
--no-backend)
|
||||
BACKEND=0
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
printf >&2 'fatal: unknown argument: %s\n' "$1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
if [ -z "${target}" ]; then
|
||||
die 'target directory not specified'
|
||||
fi
|
||||
if ! [ -e "${target}" ]; then
|
||||
mkdir -p -- "${target}"
|
||||
fi
|
||||
if ! [ -d "${target}" ]; then
|
||||
die "target is not a directory: ${target}"
|
||||
fi
|
||||
if [ "$(command ls -A "${target}" | wc -l)" != 0 ]; then
|
||||
die "target directory is nonempty: ${target}"
|
||||
fi
|
||||
target="$(readlink -e "${target}")"
|
||||
: "${SOURCECRED_BIN:=./bin}"
|
||||
}
|
||||
|
||||
build() {
|
||||
sourcecred_data="$(mktemp -d --suffix ".sourcecred-data")"
|
||||
|
||||
if [ -n "${SOURCECRED_DIRECTORY:-}" ]; then
|
||||
# If $SOURCECRED_DIRECTORY is available, then give sourcecred access to
|
||||
# the cache. This will greatly speed up site builds on repos that have
|
||||
# already been loaded.
|
||||
# Note this speedup will only apply if the SOURCECRED_DIRECTORY has been
|
||||
# explicitly set.
|
||||
ln -s "${SOURCECRED_DIRECTORY}/cache" "${sourcecred_data}/cache"
|
||||
fi
|
||||
|
||||
export SOURCECRED_DIRECTORY="${sourcecred_data}"
|
||||
|
||||
if [ "${BACKEND}" -ne 0 ]; then
|
||||
sourcecred_bin="$(mktemp -d --suffix ".sourcecred-bin")"
|
||||
export SOURCECRED_BIN="${sourcecred_bin}"
|
||||
yarn
|
||||
yarn -s backend --output-path "${SOURCECRED_BIN}"
|
||||
fi
|
||||
|
||||
if [ "${#projects[@]}" -ne 0 ]; then
|
||||
local weightsStr=""
|
||||
if [ -n "${weights}" ]; then
|
||||
weightsStr="--weights ${weights}"
|
||||
fi
|
||||
for project in "${projects[@]}"; do
|
||||
NODE_PATH="./node_modules${NODE_PATH:+:${NODE_PATH}}" \
|
||||
node "${SOURCECRED_BIN:-./bin}/sourcecred.js" load "${project}" $weightsStr
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "${#project_files[@]}" -ne 0 ]; then
|
||||
local weightsStr=""
|
||||
if [ -n "${weights}" ]; then
|
||||
weightsStr="--weights ${weights}"
|
||||
fi
|
||||
for project_file in "${project_files[@]}"; do
|
||||
NODE_PATH="./node_modules${NODE_PATH:+:${NODE_PATH}}" \
|
||||
node "${SOURCECRED_BIN:-./bin}/sourcecred.js" load --project "${project_file}" $weightsStr
|
||||
done
|
||||
fi
|
||||
|
||||
yarn -s build --output-path "${target}"
|
||||
|
||||
# Copy the SourceCred data into the appropriate API route. Using
|
||||
# `mkdir` here will fail in the case where an `api/` folder exists,
|
||||
# which is the correct behavior. (In this case, our site's
|
||||
# architecture conflicts with the required static structure, and we
|
||||
# must fail.)
|
||||
mkdir "${target}/api/"
|
||||
mkdir "${target}/api/v1/"
|
||||
# Eliminate the cache, which is only an intermediate target used to
|
||||
# load the actual data. The development server similarly forbids
|
||||
# access to the cache so that the dev and prod environments have the
|
||||
# same semantics.
|
||||
rm -rf "${sourcecred_data}/cache"
|
||||
cp -r "${sourcecred_data}" "${target}/api/v1/data"
|
||||
|
||||
if [ -n "${cname:-}" ]; then
|
||||
cname_file="${target}/CNAME"
|
||||
if [ -e "${cname_file}" ]; then
|
||||
die 'CNAME file exists in static site output'
|
||||
fi
|
||||
printf '%s' "${cname}" >"${cname_file}" # no newline
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [ -d "${sourcecred_data:-}" ]; then rm -rf "${sourcecred_data}"; fi
|
||||
if [ -d "${sourcecred_bin:-}" ]; then rm -rf "${sourcecred_bin}"; fi
|
||||
}
|
||||
|
||||
die() {
|
||||
printf >&2 'fatal: %s\n' "$@"
|
||||
exit 1
|
||||
}
|
||||
|
||||
main "$@"
|
@ -1,249 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Disable these lint rules globally:
|
||||
# 2034 = unused variable (used by sharness)
|
||||
# 2016 = parameter expansion in single quotes
|
||||
# 1004 = backslash-newline in single quotes
|
||||
# shellcheck disable=SC2034,SC2016,SC1004
|
||||
:
|
||||
|
||||
test_description='tests for scripts/build_static_site.sh'
|
||||
|
||||
export GIT_CONFIG_NOSYSTEM=1
|
||||
export GIT_ATTR_NOSYSTEM=1
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
. ./sharness.sh
|
||||
|
||||
run() (
|
||||
set -eu
|
||||
toplevel="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
|
||||
"${toplevel}"/scripts/build_static_site.sh "$@"
|
||||
)
|
||||
|
||||
#
|
||||
# Start by checking a bunch of easy cases related to the argument
|
||||
# parser, mostly about rejecting various ill-formed invocations.
|
||||
|
||||
test_expect_success "should print a help message" '
|
||||
run --help >msg 2>err &&
|
||||
test_must_be_empty err &&
|
||||
test_path_is_file msg &&
|
||||
grep -qF "usage: build_static_site.sh" msg
|
||||
'
|
||||
|
||||
test_expect_success "should fail with no target" '
|
||||
test_must_fail run 2>err &&
|
||||
grep -qF -- "target directory not specified" err
|
||||
'
|
||||
|
||||
test_expect_success "should fail with missing target value" '
|
||||
test_must_fail run --target 2>err &&
|
||||
grep -qF -- "missing value for --target" err
|
||||
'
|
||||
|
||||
test_expect_success "should fail with multiple targets" '
|
||||
mkdir one two &&
|
||||
test_must_fail run --target one --target two 2>err &&
|
||||
grep -qF -- "--target specified multiple times" err
|
||||
'
|
||||
|
||||
test_expect_success "should fail with a file as target" '
|
||||
printf "important\nstuff" >important_data &&
|
||||
test_must_fail run --target important_data 2>err &&
|
||||
grep -qF -- "target is not a directory" err &&
|
||||
printf "important\nstuff" | test_cmp - important_data
|
||||
'
|
||||
|
||||
test_expect_success "should fail with a target under a file" '
|
||||
printf "important\nstuff" >important_data &&
|
||||
test_must_fail run --target important_data/something 2>err &&
|
||||
grep -q -- "cannot create directory.*Not a directory" err &&
|
||||
printf "important\nstuff" | test_cmp - important_data
|
||||
'
|
||||
|
||||
test_expect_success "should fail with a nonempty directory as target" '
|
||||
mkdir important_dir &&
|
||||
printf "redacted\n" >important_dir/.wallet.dat &&
|
||||
test_must_fail run --target important_dir 2>err &&
|
||||
grep -qF -- "target directory is nonempty: important_dir" err &&
|
||||
printf "redacted\n" | test_cmp - important_dir/.wallet.dat
|
||||
'
|
||||
|
||||
mkdir putative_output
|
||||
|
||||
test_expect_success "should fail with missing project value" '
|
||||
test_must_fail run --target putative_output --project 2>err &&
|
||||
grep -qF -- "missing value for --project" err &&
|
||||
printf "redacted\n" | test_cmp - important_dir/.wallet.dat
|
||||
'
|
||||
|
||||
test_expect_success "should fail with missing cname value" '
|
||||
test_must_fail run --target putative_output --cname 2>err &&
|
||||
grep -qF -- "missing value for --cname" err &&
|
||||
printf "redacted\n" | test_cmp - important_dir/.wallet.dat
|
||||
'
|
||||
|
||||
test_expect_success "should fail with empty cname" '
|
||||
test_must_fail run --target putative_output --cname "" 2>err &&
|
||||
grep -qF -- "empty value for --cname" err &&
|
||||
printf "redacted\n" | test_cmp - important_dir/.wallet.dat
|
||||
'
|
||||
|
||||
test_expect_success "should fail with multiple cname values" '
|
||||
test_must_fail run --target putative_output \
|
||||
--cname a.com --cname b.com 2>err &&
|
||||
grep -qF -- "--cname specified multiple times" err &&
|
||||
printf "redacted\n" | test_cmp - important_dir/.wallet.dat
|
||||
'
|
||||
|
||||
#
|
||||
# Now, actually generate output in two cases: one with projects, and
|
||||
# one with no projects. We can only do this if we have a token.
|
||||
|
||||
if [ -n "${SOURCECRED_GITHUB_TOKEN:-}" ]; then
|
||||
test_set_prereq HAVE_GITHUB_TOKEN
|
||||
fi
|
||||
|
||||
# run_build PREREQ_NAME DESCRIPTION [FLAGS...]
|
||||
# Build the site with the given FLAGS, and create a prereq PREREQ_NAME
|
||||
# to be used in any tests that depend on this build. The build will
|
||||
# itself have the EXPENSIVE prereq.
|
||||
run_build() {
|
||||
prereq_name="$1"; shift
|
||||
description="$1"; shift
|
||||
output_dir="build_output/output_${prereq_name}"
|
||||
api_dir="${output_dir}/api/v1/data"
|
||||
unsafe_arg=
|
||||
for arg in "${output_dir}" "$@"; do
|
||||
unusual_chars="$(printf '%s' "$arg" | sed -e 's#[A-Za-z0-9:/_.-]##g')"
|
||||
if [ -n "${unusual_chars}" ]; then
|
||||
unsafe_arg="${arg}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
flags="--target $output_dir $*" # only used if ! [ -n "${unsafe_arg}" ]
|
||||
test_expect_success EXPENSIVE,HAVE_GITHUB_TOKEN \
|
||||
"${prereq_name}: ${description}" '
|
||||
if [ -n "${unsafe_arg}" ]; then
|
||||
printf >&2 "fatal: potentially unsafe argument: %s\n" "${arg}" &&
|
||||
false
|
||||
fi &&
|
||||
run '"${flags}"' >out 2>err &&
|
||||
test_must_fail grep -vF \
|
||||
-e "Removing contents of build directory: " \
|
||||
-e "info: loading project" \
|
||||
-e "DeprecationWarning: Tapable.plugin is deprecated." \
|
||||
err &&
|
||||
test_path_is_dir "${output_dir}" &&
|
||||
test_path_is_dir "${api_dir}" &&
|
||||
test_set_prereq "${prereq_name}"
|
||||
'
|
||||
test_expect_success "${prereq_name}" \
|
||||
"${prereq_name}: should have no cache" '
|
||||
test_must_fail test_path_is_dir "${api_dir}/cache"
|
||||
'
|
||||
test_expect_success "${prereq_name}" \
|
||||
"${prereq_name}: should have a bundle" '
|
||||
js_bundle_path= &&
|
||||
js_bundle_path_glob="${output_dir}"/static/js/main.*.js &&
|
||||
for main_js in ${js_bundle_path_glob}; do
|
||||
if ! [ -e "${main_js}" ]; then
|
||||
printf >&2 "fatal: no main bundle found\n" &&
|
||||
return 1
|
||||
elif [ -n "${js_bundle_path}" ]; then
|
||||
printf >&2 "fatal: multiple main bundles found:\n" &&
|
||||
printf >&2 " %s\n" ${js_bundle_path_glob} &&
|
||||
return 1
|
||||
else
|
||||
js_bundle_path="${main_js}"
|
||||
fi
|
||||
done
|
||||
'
|
||||
}
|
||||
|
||||
# test_pages PREREQ_NAME
|
||||
# Test that the PREREQ_NAME build output includes a valid home page, a
|
||||
# valid prototype page, and a valid Discord invite page (which should be
|
||||
# a redirect).
|
||||
test_pages() {
|
||||
prereq="$1"
|
||||
test_expect_success "${prereq}" "${prereq}: should have a favicon" '
|
||||
test_path_is_file "${output_dir}/favicon.png" &&
|
||||
file -b --mime-type "${output_dir}/favicon.png" >./favicon_filetype &&
|
||||
printf "image/png\n" | test_cmp - ./favicon_filetype &&
|
||||
rm ./favicon_filetype
|
||||
'
|
||||
test_expect_success "${prereq}" \
|
||||
"${prereq}: should have a home page and a prototype" '
|
||||
test_path_is_file "${output_dir}/index.html" &&
|
||||
grep -qF "<script src=" "${output_dir}/index.html" &&
|
||||
test_path_is_file "${output_dir}/prototype/index.html" &&
|
||||
grep -qF "<script src=" "${output_dir}/prototype/index.html"
|
||||
'
|
||||
test_expect_success "${prereq}" \
|
||||
"${prereq}: should have a discord-invite with redirect" '
|
||||
file="${output_dir}/discord-invite/index.html" &&
|
||||
test_path_is_file "${file}" &&
|
||||
test_must_fail grep -qF "<script src=" "${file}" &&
|
||||
url="https://discord.gg/tsBTgc9" &&
|
||||
needle="<meta http-equiv=\"refresh\" content=\"0;url=$url\" />" &&
|
||||
grep -qxF "${needle}" "${file}"
|
||||
'
|
||||
}
|
||||
|
||||
run_build TWO_PROJECTS \
|
||||
"should build the site with two projects and a CNAME" \
|
||||
--no-backend \
|
||||
--cname sourcecred.example.com \
|
||||
--project sourcecred-test/example-git \
|
||||
--project sourcecred-test/example-github \
|
||||
;
|
||||
|
||||
test_pages TWO_PROJECTS
|
||||
|
||||
test_expect_success TWO_PROJECTS \
|
||||
"TWO_PROJECTS: should have project ids loaded into env" '
|
||||
grep -F "PROJECT_IDS" out &&
|
||||
grep -xF "PROJECT_IDS: [\"sourcecred-test/example-git\",\"sourcecred-test/example-github\"]" out
|
||||
'
|
||||
|
||||
test_expect_success TWO_PROJECTS \
|
||||
"TWO_PROJECTS: should have data for the two projects" '
|
||||
# encoded ids for sourcecred-test/example-git and sourcecred-test/example-github
|
||||
for id in c291cmNlY3JlZC10ZXN0L2V4YW1wbGUtZ2l0aHVi c291cmNlY3JlZC10ZXN0L2V4YW1wbGUtZ2l0; do
|
||||
test -s "${api_dir}/projects/${id}/cred.json" &&
|
||||
test -s "${api_dir}/projects/${id}/weightedGraph.json" ||
|
||||
return
|
||||
done
|
||||
'
|
||||
|
||||
test_expect_success TWO_PROJECTS "TWO_PROJECTS: should have a correct CNAME record" '
|
||||
test_path_is_file "${output_dir}/CNAME" &&
|
||||
printf "sourcecred.example.com" | test_cmp - "${output_dir}/CNAME"
|
||||
'
|
||||
|
||||
test_pages NO_PROJECTS
|
||||
|
||||
test_expect_success NO_PROJECTS \
|
||||
"NO_REPOS: should have empty list of project ids loaded into env" '
|
||||
grep -F "PROJECT_IDS" out &&
|
||||
grep -xF "PROJECT_IDS: []" out
|
||||
'
|
||||
|
||||
test_expect_success NO_REPOS \
|
||||
"NO_REPOS: should not have repository data" '
|
||||
for id in c291cmNlY3JlZC9leGFtcGxlLWdpdA== c291cmNlY3JlZC9leGFtcGxlLWdpdGh1Yg==; do
|
||||
for file in weightedGraph.json cred.json; do
|
||||
test_must_fail test -f "${api_dir}/projects/${id}/${file}" || return
|
||||
done
|
||||
done
|
||||
'
|
||||
|
||||
test_expect_success NO_REPOS "NO_REPOS: should have no CNAME record" '
|
||||
test_must_fail test -e "${output_dir}/CNAME"
|
||||
'
|
||||
|
||||
test_done
|
||||
|
||||
# vim: ft=sh
|
@ -1,28 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import {Router} from "react-router";
|
||||
import type {History /* actually `any` */} from "history";
|
||||
|
||||
import {createRoutes} from "./createRoutes";
|
||||
import {type RouteData, resolveTitleFromPath} from "./routeData";
|
||||
|
||||
export default class App extends React.Component<{|
|
||||
+routeData: RouteData,
|
||||
+history: History,
|
||||
|}> {
|
||||
render() {
|
||||
const {routeData, history} = this.props;
|
||||
return (
|
||||
<Router
|
||||
history={history}
|
||||
routes={createRoutes(routeData)}
|
||||
onUpdate={function () {
|
||||
const router = this;
|
||||
const path: string = router.state.location.pathname;
|
||||
document.title = resolveTitleFromPath(routeData, path);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
|
||||
type Props = {|
|
||||
+className: string,
|
||||
+altText: string,
|
||||
|};
|
||||
|
||||
export default function DiscordLogo(props: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 245 240"
|
||||
aria-label={props.altText}
|
||||
role="img"
|
||||
className={props.className}
|
||||
>
|
||||
<path d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z" />
|
||||
<path d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
|
||||
import Link from "../webutil/Link";
|
||||
|
||||
export default class ExternalRedirect extends React.Component<{|
|
||||
+redirectTo: string,
|
||||
|}> {
|
||||
render() {
|
||||
return (
|
||||
<div style={{maxWidth: 900, margin: "0 auto"}}>
|
||||
<h1>Redirecting…</h1>
|
||||
<p>
|
||||
Redirecting to:{" "}
|
||||
<Link href={this.props.redirectTo}>{this.props.redirectTo}</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// The server-rendered copy of this page will have a meta-refresh
|
||||
// tag, but someone could still plausibly navigate to this page with
|
||||
// the client-side renderer. In that case, we should redirect them.
|
||||
window.location.href = this.props.redirectTo;
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
|
||||
type Props = {|
|
||||
+className: string,
|
||||
+altText: string,
|
||||
|};
|
||||
|
||||
export default function GithubLogo(props: Props) {
|
||||
return (
|
||||
<svg
|
||||
aria-label={props.altText}
|
||||
role="img"
|
||||
className={props.className}
|
||||
viewBox="0 0 1024 1024"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>{props.altText}</title>
|
||||
<path d="M512 0C229.25 0 0 229.25 0 512c0 226.25 146.688 418.125 350.156 485.812 25.594 4.688 34.938-11.125 34.938-24.625 0-12.188-0.469-52.562-0.719-95.312C242 908.812 211.906 817.5 211.906 817.5c-23.312-59.125-56.844-74.875-56.844-74.875-46.531-31.75 3.53-31.125 3.53-31.125 51.406 3.562 78.47 52.75 78.47 52.75 45.688 78.25 119.875 55.625 149 42.5 4.654-33 17.904-55.625 32.5-68.375C304.906 725.438 185.344 681.5 185.344 485.312c0-55.938 19.969-101.562 52.656-137.406-5.219-13-22.844-65.094 5.062-135.562 0 0 42.938-13.75 140.812 52.5 40.812-11.406 84.594-17.031 128.125-17.219 43.5 0.188 87.312 5.875 128.188 17.281 97.688-66.312 140.688-52.5 140.688-52.5 28 70.531 10.375 122.562 5.125 135.5 32.812 35.844 52.625 81.469 52.625 137.406 0 196.688-119.75 240-233.812 252.688 18.438 15.875 34.75 47 34.75 94.75 0 68.438-0.688 123.625-0.688 140.5 0 13.625 9.312 29.562 35.25 24.562C877.438 930 1024 738.125 1024 512 1024 229.25 794.75 0 512 0z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -1,207 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
|
||||
import type {Assets} from "../webutil/assets";
|
||||
import Link from "../webutil/Link";
|
||||
import {StyleSheet, css} from "aphrodite/no-important";
|
||||
|
||||
export default class HomePage extends React.Component<{|+assets: Assets|}> {
|
||||
render() {
|
||||
const urls = {
|
||||
numpyFunding:
|
||||
"https://numfocus.org/blog/numpy-receives-first-ever-funding-thanks-to-moore-foundation",
|
||||
opensslFunding:
|
||||
"https://arstechnica.com/information-technology/2014/04/tech-giants-chastened-by-heartbleed-finally-agree-to-fund-openssl/",
|
||||
graph: "https://en.wikipedia.org/wiki/Graph_(discrete_mathematics)",
|
||||
pagerank: "https://en.wikipedia.org/wiki/PageRank",
|
||||
ast: "https://en.wikipedia.org/wiki/Abstract_syntax_tree",
|
||||
protocolLabs: "https://protocol.ai/",
|
||||
discord: "https://discord.gg/tsBTgc9",
|
||||
github: "https://github.com/sourcecred/sourcecred",
|
||||
contributionsWelcome:
|
||||
"https://github.com/sourcecred/sourcecred/issues?q=is%3Aissue+is%3Aopen+label%3A%22contributions+welcome%22",
|
||||
readme: "https://github.com/sourcecred/sourcecred/blob/master/README.md",
|
||||
};
|
||||
return (
|
||||
<div className={css(styles.container)}>
|
||||
<h1>SourceCred vision</h1>
|
||||
<p>
|
||||
<strong>The open-source movement is amazing. </strong>
|
||||
It’s inspiring that some of our best technology is developed in the
|
||||
open and available to everyone.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Despite all the value provided by open-source projects, many are
|
||||
chronically underfunded. For example, NumPy{" "}
|
||||
<Link href={urls.numpyFunding}>
|
||||
received no funding at all until 2017
|
||||
</Link>
|
||||
, and{" "}
|
||||
<Link href={urls.opensslFunding}>
|
||||
a world where OpenSSL was funded might have been a world without
|
||||
Heartbleed
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
These projects also impose a heavy burden on maintainers. Popular
|
||||
projects have hundreds or thousands of open issues, with many new ones
|
||||
being created every day, and only a few overworked volunteers trying
|
||||
to triage and respond to them. Burnout is inevitable.
|
||||
</p>
|
||||
|
||||
<p>SourceCred is our attempt to help.</p>
|
||||
|
||||
<h2>Mission</h2>
|
||||
<p>
|
||||
SourceCred aims to empower open-source developers and communities by
|
||||
creating a project-specific reputation metric called <em>cred</em>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
A project’s contributors earn cred for helping out. For example, a
|
||||
project might reward:
|
||||
</p>
|
||||
<ul style={{marginTop: "-1.5ex"}}>
|
||||
<li>Triaging issues</li>
|
||||
<li>Maintaining the build</li>
|
||||
<li>Fixing bugs</li>
|
||||
<li>Writing documentation</li>
|
||||
<li>Refactoring code</li>
|
||||
<li>Adding features</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
SourceCred will build social capital within communities, recognize
|
||||
their hardworking contributors, and encourage more people to help
|
||||
maintain and develop open-source projects.
|
||||
</p>
|
||||
|
||||
<p>We’re designing SourceCred around the following four principles:</p>
|
||||
|
||||
<dl>
|
||||
<Dt>Transparency</Dt>
|
||||
<Dd>
|
||||
It should be easy to see why cred is attributed as it is, and link a
|
||||
person’s cred directly to contributions they’ve made.
|
||||
</Dd>
|
||||
<Dt>Extensibility</Dt>
|
||||
<Dd>
|
||||
SourceCred is designed around a plugin architecture, so you can add
|
||||
support for new data sources, new algorithms, or even entirely new
|
||||
kinds of work.
|
||||
</Dd>
|
||||
<Dt>Community control</Dt>
|
||||
<Dd>
|
||||
Each community has the final say on that community’s cred. When the
|
||||
algorithm and the community disagree, the community wins.
|
||||
</Dd>
|
||||
<Dt>Decentralization</Dt>
|
||||
<Dd>
|
||||
Projects own their own data, and control their own cred. The
|
||||
SourceCred project provides tools, but has no control.
|
||||
</Dd>
|
||||
</dl>
|
||||
|
||||
<h2>How cred works</h2>
|
||||
<p>
|
||||
Cred is computed by first creating a contribution{" "}
|
||||
<Link href={urls.graph}>graph</Link>, which contains every
|
||||
contribution to the project and the relations among them. For example,
|
||||
GitHub issues, Git commits, and individual files and functions can be
|
||||
included in the graph. Then, SourceCred runs a modified version of{" "}
|
||||
<Link href={urls.pagerank}>PageRank</Link> on that graph to produce a
|
||||
cred attribution. The attribution is highly configurable; project
|
||||
maintainers can add new heuristics and adjust weights.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This approach satisfies our four principles. It’s transparent: you can
|
||||
always see how a node’s weight dervies from its neighbors. It’s
|
||||
extensible: plugins can embed new types of nodes and edges into the
|
||||
graph. It’s community-controlled: the weights, heuristics, and
|
||||
algorithms are all configured by the project. Finally, it’s
|
||||
decentralized: every project can run its own instance.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Naturally, there will be attempts to game the system. We’ll provide
|
||||
tools that make it obvious when people are gaming their cred, and
|
||||
empower maintainers to moderate and correct the attribution when
|
||||
needed. In case of deeply contentious disagreements, cred can be
|
||||
forked alongside the project.
|
||||
</p>
|
||||
|
||||
<h2>Roadmap</h2>
|
||||
<p>
|
||||
SourceCred is under active development.{" "}
|
||||
<Link to="/prototype/">We have a prototype</Link> that ingests data
|
||||
from Git and GitHub, computes cred, and allows the user to explore and
|
||||
experiment on the results. We have a long way to go to realize
|
||||
SourceCred’s full vision, but the prototype can already surface some
|
||||
interesting insights!
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In the near term, we want to help with issue triage and
|
||||
prioritization. Open-source projects are drowning in issues; many
|
||||
people file them, but few are motivated to triage them. We want to
|
||||
recognize the people who show up to do that work, and reward them by
|
||||
giving them more influence over issue prioritization.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In the longer term, we will continue to add signal to cred
|
||||
attribution. For example, we plan to parse the{" "}
|
||||
<Link href={urls.ast}>AST</Link> of a project’s code so that we can
|
||||
attribute cred at the level of individual functions, and create a
|
||||
“spotlight” mechanic that will let contributors flow more cred to
|
||||
their peers’ important contributions. As SourceCred improves, we have
|
||||
plans for how to use it to help open-source projects become
|
||||
financially sustainable.
|
||||
</p>
|
||||
|
||||
<h2>About</h2>
|
||||
<p>
|
||||
SourceCred is an open-source project, and is committed to being
|
||||
decentralized. We don’t think communities should have to give their
|
||||
data to us, or entrust us with control over their cred. The lead
|
||||
developers are grateful to be supported by{" "}
|
||||
<Link href={urls.protocolLabs}>Protocol Labs</Link>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you think this vision is exciting, we’d love for you to get
|
||||
involved! You can join our <Link href={urls.discord}>Discord</Link>{" "}
|
||||
and check out our <Link href={urls.github}>GitHub</Link>—many of our
|
||||
issues are marked{" "}
|
||||
<Link href={urls.contributionsWelcome}>contributions welcome</Link>.
|
||||
If you want to try running SourceCred on open-source projects you care
|
||||
about, check out <Link href={urls.readme}>our README</Link>.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function Dt(props) {
|
||||
return <dt style={{fontWeight: "bold"}}>{props.children}</dt>;
|
||||
}
|
||||
|
||||
function Dd(props) {
|
||||
return <dd style={{marginBottom: 15}}>{props.children}</dd>;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
maxWidth: 900,
|
||||
margin: "0 auto",
|
||||
marginBottom: 200,
|
||||
padding: "0 10px",
|
||||
lineHeight: 1.5,
|
||||
fontSize: 20,
|
||||
},
|
||||
});
|
@ -1,162 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, {type Node} from "react";
|
||||
import {StyleSheet, css} from "aphrodite/no-important";
|
||||
|
||||
import type {Assets} from "../webutil/assets";
|
||||
import Colors from "../webutil/Colors";
|
||||
import Link from "../webutil/Link";
|
||||
import GithubLogo from "./GithubLogo";
|
||||
import TwitterLogo from "./TwitterLogo";
|
||||
import DiscordLogo from "./DiscordLogo";
|
||||
import type {RouteData} from "./routeData";
|
||||
import * as NullUtil from "../util/null";
|
||||
import {VERSION_SHORT, VERSION_FULL} from "../core/version";
|
||||
|
||||
export default class Page extends React.Component<{|
|
||||
+assets: Assets,
|
||||
+routeData: RouteData,
|
||||
+children: Node,
|
||||
|}> {
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={css(style.nonFooter)}>
|
||||
<header>
|
||||
<nav className={css(style.nav)}>
|
||||
<ul className={css(style.navList)}>
|
||||
<li className={css(style.navItem, style.navItemLeft)}>
|
||||
<Link to="/" styles={[style.navLink, style.navLinkTitle]}>
|
||||
SourceCred
|
||||
</Link>
|
||||
</li>
|
||||
{this.props.routeData.map(({navTitle, path}) =>
|
||||
NullUtil.map(navTitle, (navTitle) => (
|
||||
<li
|
||||
key={path}
|
||||
className={css(style.navItem, style.navItemRight)}
|
||||
>
|
||||
<Link to={path} styles={[style.navLink]}>
|
||||
{navTitle}
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
<li className={css(style.navItem, style.navItemRight)}>
|
||||
<Link
|
||||
styles={[style.navLink]}
|
||||
href="https://github.com/sourcecred/sourcecred"
|
||||
>
|
||||
<GithubLogo
|
||||
altText="SourceCred Github"
|
||||
className={css(style.navLogoSmall)}
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={css(style.navItem, style.navItemRight)}>
|
||||
<Link
|
||||
styles={[style.navLink]}
|
||||
href="https://twitter.com/sourcecred"
|
||||
>
|
||||
<TwitterLogo
|
||||
altText="SourceCred Twitter"
|
||||
className={css(style.navLogoSmall)}
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={css(style.navItem, style.navItemRightSmall)}>
|
||||
<Link
|
||||
styles={[style.navLink]}
|
||||
href="https://discordapp.com/invite/tsBTgc9"
|
||||
>
|
||||
<DiscordLogo
|
||||
altText="Join the SourceCred Discord"
|
||||
className={css(style.navLogoMedium)}
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<main>{this.props.children}</main>
|
||||
</div>
|
||||
<footer className={css(style.footer)}>
|
||||
<div className={css(style.footerWrapper)}>
|
||||
<span className={css(style.footerText)}>
|
||||
({VERSION_FULL}) <strong>{VERSION_SHORT}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const footerHeight = 30;
|
||||
const style = StyleSheet.create({
|
||||
footer: {
|
||||
color: "#666",
|
||||
height: footerHeight,
|
||||
fontSize: 14,
|
||||
position: "relative",
|
||||
},
|
||||
footerWrapper: {
|
||||
textAlign: "right",
|
||||
position: "absolute",
|
||||
bottom: 5,
|
||||
width: "100%",
|
||||
},
|
||||
footerText: {
|
||||
marginRight: 5,
|
||||
},
|
||||
nonFooter: {
|
||||
minHeight: `calc(100vh - ${footerHeight}px)`,
|
||||
},
|
||||
nav: {
|
||||
padding: "20px 50px 0 50px",
|
||||
maxWidth: 900,
|
||||
margin: "0 auto",
|
||||
},
|
||||
navLinkTitle: {
|
||||
fontSize: 24,
|
||||
},
|
||||
navItem: {
|
||||
display: "inline-block",
|
||||
},
|
||||
navList: {
|
||||
listStyle: "none",
|
||||
paddingLeft: 0,
|
||||
margin: 0,
|
||||
display: "flex",
|
||||
},
|
||||
navLink: {
|
||||
fontFamily: "Roboto Condensed",
|
||||
fontSize: 18,
|
||||
textDecoration: "none",
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
":visited:not(:active)": {
|
||||
color: Colors.brand.medium,
|
||||
fill: Colors.brand.medium, // for SVG icons
|
||||
},
|
||||
},
|
||||
navItemLeft: {
|
||||
flex: 1,
|
||||
},
|
||||
navItemRight: {
|
||||
marginLeft: 20,
|
||||
},
|
||||
navItemRightSmall: {
|
||||
marginLeft: 15,
|
||||
},
|
||||
navLogoSmall: {
|
||||
height: 20,
|
||||
width: 20,
|
||||
},
|
||||
navLogoMedium: {
|
||||
height: 25,
|
||||
width: 25,
|
||||
transform: "translateY(-1px)",
|
||||
},
|
||||
});
|
@ -1,18 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, {type ComponentType} from "react";
|
||||
|
||||
import type {Assets} from "../webutil/assets";
|
||||
import HomepageExplorer from "./homepageExplorer";
|
||||
|
||||
export default function makeProjectPage(
|
||||
projectId: string
|
||||
): ComponentType<{|+assets: Assets|}> {
|
||||
return class ProjectPage extends React.Component<{|+assets: Assets|}> {
|
||||
render() {
|
||||
return (
|
||||
<HomepageExplorer assets={this.props.assets} projectId={projectId} />
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, {type ComponentType} from "react";
|
||||
|
||||
import Link from "../webutil/Link";
|
||||
import type {Assets} from "../webutil/assets";
|
||||
|
||||
export default function makePrototypesPage(
|
||||
projectIds: $ReadOnlyArray<string>
|
||||
): ComponentType<{|+assets: Assets|}> {
|
||||
return class PrototypesPage extends React.Component<{|+assets: Assets|}> {
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 900,
|
||||
margin: "0 auto",
|
||||
padding: "0 10px",
|
||||
lineHeight: 1.5,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<p>Select a project:</p>
|
||||
<ul>
|
||||
{projectIds.map((projectId) => (
|
||||
<li key={projectId}>
|
||||
<Link to={`/timeline/${projectId}/`}>{`${projectId}`}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, {type ComponentType} from "react";
|
||||
|
||||
import type {Assets} from "../webutil/assets";
|
||||
import HomepageTimeline from "./homepageTimeline";
|
||||
|
||||
export default function makeTimelinePage(
|
||||
projectId: string
|
||||
): ComponentType<{|+assets: Assets|}> {
|
||||
return class TimelinePage extends React.Component<{|+assets: Assets|}> {
|
||||
render() {
|
||||
return (
|
||||
<HomepageTimeline assets={this.props.assets} projectId={projectId} />
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
|
||||
type Props = {|
|
||||
+className: string,
|
||||
+altText: string,
|
||||
|};
|
||||
|
||||
export default function TwitterLogo(props: Props) {
|
||||
return (
|
||||
<svg
|
||||
aria-label={props.altText}
|
||||
role="img"
|
||||
className={props.className}
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<title>{props.altText}</title>
|
||||
<path
|
||||
d="M512,97.209c-18.838,8.354-39.082,14.001-60.329,16.54c21.687-13,38.343-33.585,46.187-58.114
|
||||
c-20.299,12.038-42.778,20.779-66.705,25.489c-19.16-20.415-46.461-33.17-76.674-33.17c-58.012,0-105.043,47.029-105.043,105.039
|
||||
c0,8.233,0.929,16.25,2.72,23.939c-87.3-4.382-164.701-46.2-216.509-109.753c-9.042,15.514-14.224,33.558-14.224,52.809
|
||||
c0,36.444,18.544,68.596,46.73,87.433c-17.219-0.546-33.416-5.271-47.577-13.139c-0.01,0.438-0.01,0.878-0.01,1.321
|
||||
c0,50.894,36.209,93.348,84.261,103c-8.813,2.398-18.094,3.686-27.674,3.686c-6.77,0-13.349-0.66-19.764-1.887
|
||||
c13.367,41.73,52.159,72.104,98.126,72.949c-35.95,28.175-81.243,44.967-130.458,44.967c-8.479,0-16.841-0.497-25.059-1.471
|
||||
c46.486,29.806,101.701,47.197,161.021,47.197c193.211,0,298.868-160.063,298.868-298.873c0-4.554-0.104-9.084-0.305-13.59
|
||||
C480.11,136.773,497.918,118.273,512,97.209z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import {type RouteData, makeRouteData} from "./routeData";
|
||||
|
||||
export default function createRouteDataFromEnvironment(): RouteData {
|
||||
const raw = process.env.PROJECT_IDS;
|
||||
if (raw == null) {
|
||||
throw new Error("fatal: project IDs unset");
|
||||
}
|
||||
const ids: $ReadOnlyArray<string> = JSON.parse(raw);
|
||||
return makeRouteData(ids);
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import {IndexRoute, Route} from "react-router";
|
||||
|
||||
import withAssets from "../webutil/withAssets";
|
||||
import ExternalRedirect from "./ExternalRedirect";
|
||||
import Page from "./Page";
|
||||
import type {RouteData} from "./routeData";
|
||||
|
||||
export function createRoutes(routeData: RouteData) {
|
||||
const PageWithAssets = withAssets(Page);
|
||||
const PageWithRoutes = (props) => (
|
||||
<PageWithAssets routeData={routeData} {...props} />
|
||||
);
|
||||
return (
|
||||
<Route path="/" component={PageWithRoutes}>
|
||||
{routeData.map(({path, contents}) => {
|
||||
switch (contents.type) {
|
||||
case "PAGE":
|
||||
if (path === "/") {
|
||||
return (
|
||||
<IndexRoute
|
||||
key={path}
|
||||
component={withAssets(contents.component())}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
component={withAssets(contents.component())}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "EXTERNAL_REDIRECT":
|
||||
return (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
component={() => (
|
||||
<ExternalRedirect redirectTo={contents.redirectTo} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
throw new Error((contents.type: empty));
|
||||
}
|
||||
})}
|
||||
</Route>
|
||||
);
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
|
||||
import type {Assets} from "../webutil/assets";
|
||||
import {AppPage} from "../explorer/legacy/App";
|
||||
|
||||
export default class HomepageExplorer extends React.Component<{|
|
||||
+assets: Assets,
|
||||
+projectId: string,
|
||||
|}> {
|
||||
render() {
|
||||
return (
|
||||
<AppPage assets={this.props.assets} projectId={this.props.projectId} />
|
||||
);
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
|
||||
import type {Assets} from "../webutil/assets";
|
||||
import {TimelineApp, defaultLoader} from "../explorer/TimelineApp";
|
||||
|
||||
export default class TimelineExplorer extends React.Component<{|
|
||||
+assets: Assets,
|
||||
+projectId: string,
|
||||
|}> {
|
||||
render() {
|
||||
return (
|
||||
<TimelineApp
|
||||
assets={this.props.assets}
|
||||
projectId={this.props.projectId}
|
||||
loader={defaultLoader}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import createBrowserHistory from "history/lib/createBrowserHistory";
|
||||
|
||||
import normalize from "../util/pathNormalize";
|
||||
import createRelativeHistory from "../webutil/createRelativeHistory";
|
||||
import App from "./App";
|
||||
import createRouteDataFromEnvironment from "./createRouteDataFromEnvironment";
|
||||
|
||||
const target = document.getElementById("root");
|
||||
if (target == null) {
|
||||
throw new Error("Unable to find root element!");
|
||||
}
|
||||
|
||||
let initialRoot: string = target.dataset.initialRoot;
|
||||
if (initialRoot == null) {
|
||||
console.error(
|
||||
`Initial root unset (${initialRoot}): this should not happen! ` +
|
||||
'Falling back to ".".'
|
||||
);
|
||||
initialRoot = ".";
|
||||
}
|
||||
const basename = normalize(`${window.location.pathname}/${initialRoot}/`);
|
||||
const history = createRelativeHistory(createBrowserHistory(), basename);
|
||||
|
||||
const routeData = createRouteDataFromEnvironment();
|
||||
ReactDOM.hydrate(<App routeData={routeData} history={history} />, target);
|
||||
|
||||
// In Chrome, relative favicon URLs are recomputed at every pushState,
|
||||
// although other assets (like the `src` of an `img`) are not. We don't
|
||||
// want to have to keep the shortcut icon's path up to date as we
|
||||
// transition; it's simpler to make it absolute at page load.
|
||||
for (const el of document.querySelectorAll('link[rel="shortcut icon"]')) {
|
||||
const link: HTMLLinkElement = (el: any);
|
||||
// (Appearances aside, this is not a no-op.)
|
||||
link.href = link.href; // eslint-disable-line no-self-assign
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
// @flow
|
||||
|
||||
// NOTE: This module must be written in vanilla ECMAScript that can be
|
||||
// run by Node without a preprocessor. That means that we use `exports`
|
||||
// and `require` instead of ECMAScript module keywords, we lazy-load all
|
||||
// dependent modules, and we use the Flow comment syntax instead of the
|
||||
// inline syntax.
|
||||
|
||||
/*::
|
||||
import type {Assets} from "../webutil/assets";
|
||||
|
||||
type RouteDatum = {|
|
||||
+path: string,
|
||||
+contents:
|
||||
| {|
|
||||
+type: "PAGE",
|
||||
+component: () => React$ComponentType<{|+assets: Assets|}>,
|
||||
|}
|
||||
| {|
|
||||
+type: "EXTERNAL_REDIRECT",
|
||||
+redirectTo: string,
|
||||
|},
|
||||
+title: string,
|
||||
+navTitle: ?string,
|
||||
|};
|
||||
export type RouteData = $ReadOnlyArray<RouteDatum>;
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds an 'Inspection Test', which is a standalone React component
|
||||
* which allows us to manually inspect some frontend behavior.
|
||||
*
|
||||
* Writing inspection tests is especially convenient for cases where it's
|
||||
* easy to verify that a component is working properly by manually interacting
|
||||
* with it, but hard/expensive to test automatically.
|
||||
*
|
||||
* An example is a FileUploader component which uploads a file from the user,
|
||||
* goes through the FileReader API, etc.
|
||||
*
|
||||
* TODO([#1148]): Improve the inspection testing system (e.g. so we can access
|
||||
* a list of all tests from the frontend), and separate it from serving the
|
||||
* homepage.
|
||||
*
|
||||
* [#1148]: https://github.com/sourcecred/sourcecred/issues/1148
|
||||
*/
|
||||
function inspectionTestFor(name, component) /*: RouteDatum */ {
|
||||
return {
|
||||
path: "/test/" + name + "/",
|
||||
contents: {
|
||||
type: "PAGE",
|
||||
component: component,
|
||||
},
|
||||
title: "Inspection test for: " + name,
|
||||
navTitle: null,
|
||||
};
|
||||
}
|
||||
|
||||
function makeRouteData(
|
||||
projectIds /*: $ReadOnlyArray<string> */
|
||||
) /*: RouteData */ {
|
||||
return [
|
||||
{
|
||||
path: "/",
|
||||
contents: {
|
||||
type: "PAGE",
|
||||
component: () => require("./HomePage").default,
|
||||
},
|
||||
title: "SourceCred",
|
||||
navTitle: "Home",
|
||||
},
|
||||
{
|
||||
path: "/prototype/",
|
||||
contents: {
|
||||
type: "PAGE",
|
||||
component: () => require("./PrototypesPage").default(projectIds),
|
||||
},
|
||||
title: "SourceCred prototype",
|
||||
navTitle: "Prototype",
|
||||
},
|
||||
...projectIds.map((id) => ({
|
||||
path: `/prototype/${id}/`,
|
||||
contents: {
|
||||
type: "PAGE",
|
||||
component: () => require("./ProjectPage").default(id),
|
||||
},
|
||||
title: `${id} • SourceCred`,
|
||||
navTitle: null,
|
||||
})),
|
||||
...projectIds.map((id) => ({
|
||||
path: `/timeline/${id}/`,
|
||||
contents: {
|
||||
type: "PAGE",
|
||||
component: () => require("./TimelinePage").default(id),
|
||||
},
|
||||
title: `${id} • Timeline`,
|
||||
navTitle: null,
|
||||
})),
|
||||
{
|
||||
path: "/discord-invite/",
|
||||
contents: {
|
||||
type: "EXTERNAL_REDIRECT",
|
||||
redirectTo: "https://discord.gg/tsBTgc9",
|
||||
},
|
||||
title: "SourceCred Discord invite",
|
||||
navTitle: null,
|
||||
},
|
||||
// Inspection Tests Below //
|
||||
inspectionTestFor(
|
||||
"FileUploader",
|
||||
() => require("../util/FileUploaderInspectionTest").default
|
||||
),
|
||||
inspectionTestFor(
|
||||
"TimelineCredView",
|
||||
() => require("../explorer/TimelineCredViewInspectionTest").default
|
||||
),
|
||||
];
|
||||
}
|
||||
exports.makeRouteData = makeRouteData;
|
||||
|
||||
function resolveRouteFromPath(
|
||||
routeData /*: RouteData */,
|
||||
path /*: string */
|
||||
) /*: ?RouteDatum */ {
|
||||
const matches = (candidateRoute) => {
|
||||
const candidatePath = candidateRoute.path;
|
||||
const start = path.substring(0, candidatePath.length);
|
||||
const end = path.substring(candidatePath.length);
|
||||
return start === candidatePath && (end.length === 0 || end === "/");
|
||||
};
|
||||
return routeData.filter(matches)[0] || null;
|
||||
}
|
||||
exports.resolveRouteFromPath = resolveRouteFromPath;
|
||||
|
||||
function resolveTitleFromPath(
|
||||
routeData /*: RouteData */,
|
||||
path /*: string */
|
||||
) /*: string */ {
|
||||
const route = resolveRouteFromPath(routeData, path);
|
||||
const fallback = "SourceCred";
|
||||
return route ? route.title : fallback;
|
||||
}
|
||||
exports.resolveTitleFromPath = resolveTitleFromPath;
|
@ -1,51 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import {makeRouteData} from "./routeData";
|
||||
|
||||
describe("homepage/routeData", () => {
|
||||
function routeData() {
|
||||
return makeRouteData([
|
||||
"sourcecred-test/example-github",
|
||||
"sourcecred/sourcecred",
|
||||
]);
|
||||
}
|
||||
|
||||
/*
|
||||
* React Router doesn't support relative paths. I'm not sure exactly
|
||||
* what a path without a leading slash would do; it's asking for
|
||||
* trouble. If we need them, we can reconsider this test.
|
||||
*/
|
||||
it("every path has a leading slash", () => {
|
||||
for (const route of routeData()) {
|
||||
if (!route.path.startsWith("/")) {
|
||||
expect(route.path).toEqual("/" + route.path);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* A route representing a page should have a trailing slash so that
|
||||
* relative links work in the expected way. For instance, a route
|
||||
* "/about/team/" may reference "/about/logo.png" via "../logo.png".
|
||||
* But for the route "/about/team", "../logo.png" refers instead to
|
||||
* "/logo.png", which is not the intended semantics. Therefore, we
|
||||
* should consistently either include or omit trailing slashes to
|
||||
* avoid confusion.
|
||||
*
|
||||
* The choice is made for us by the fact that many web servers
|
||||
* (prominently, GitHub Pages and Python's SimpleHTTPServer) redirect
|
||||
* "/foo" to "/foo/" when serving "/foo/index.html".
|
||||
*
|
||||
* In theory, we might have some file routes like "/about/data.csv"
|
||||
* that we actually want to appear without a trailing slash. But those
|
||||
* are outside the scope of our React application, and should be
|
||||
* handled by a different pipeline (e.g., `copy-webpack-plugin`).
|
||||
*/
|
||||
it("every path has a trailing slash", () => {
|
||||
for (const route of routeData()) {
|
||||
if (!route.path.endsWith("/")) {
|
||||
expect(route.path).toEqual(route.path + "/");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
@ -1,110 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import {StyleSheetServer} from "aphrodite/no-important";
|
||||
import createMemoryHistory from "history/lib/createMemoryHistory";
|
||||
import React from "react";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import {match, RouterContext} from "react-router";
|
||||
|
||||
import dedent from "../util/dedent";
|
||||
import {Assets, rootFromPath} from "../webutil/assets";
|
||||
import createRelativeHistory from "../webutil/createRelativeHistory";
|
||||
import ExternalRedirect from "./ExternalRedirect";
|
||||
import Page from "./Page";
|
||||
import createRouteDataFromEnvironment from "./createRouteDataFromEnvironment";
|
||||
import {createRoutes} from "./createRoutes";
|
||||
import {resolveRouteFromPath, resolveTitleFromPath} from "./routeData";
|
||||
|
||||
// Side effect for testing purposes
|
||||
console.log(`PROJECT_IDS: ${process.env.PROJECT_IDS || "bad"}`);
|
||||
|
||||
// NOTE: This is a side-effect at module load time.
|
||||
const routeData = createRouteDataFromEnvironment();
|
||||
|
||||
export default function render(
|
||||
locals: {+path: string, +assets: {[string]: string}},
|
||||
callback: (error: ?mixed, result?: string) => void
|
||||
): void {
|
||||
const path = locals.path;
|
||||
const root = rootFromPath(path);
|
||||
const assets = new Assets(root);
|
||||
const history = createRelativeHistory(createMemoryHistory(path), "/");
|
||||
{
|
||||
const route = resolveRouteFromPath(routeData, path);
|
||||
if (route && route.contents.type === "EXTERNAL_REDIRECT") {
|
||||
return renderRedirect(route.contents.redirectTo);
|
||||
} else {
|
||||
return renderStandardRoute();
|
||||
}
|
||||
}
|
||||
|
||||
function renderStandardRoute() {
|
||||
const bundlePath = locals.assets["main"];
|
||||
const routes = createRoutes(routeData);
|
||||
match({history, routes}, (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="${assets.resolve("/favicon.png")}" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed" rel="stylesheet">
|
||||
<title>${resolveTitleFromPath(routeData, path)}</title>
|
||||
<style>${require("./index.css")}</style>
|
||||
<style data-aphrodite>${css.content}</style>
|
||||
</head>
|
||||
<body style="overflow-y:scroll">
|
||||
<div id="root" data-initial-root="${root}">${html}</div>
|
||||
<script src="${assets.resolve(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 ${path}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderRedirect(redirectTo: string) {
|
||||
const component = (
|
||||
<Page routeData={routeData} assets={assets}>
|
||||
<ExternalRedirect redirectTo={redirectTo} />
|
||||
</Page>
|
||||
);
|
||||
const {html, css} = StyleSheetServer.renderStatic(() =>
|
||||
ReactDOMServer.renderToStaticMarkup(component)
|
||||
);
|
||||
const page = dedent`\
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta http-equiv="refresh" content="0;url=${redirectTo}" />
|
||||
<link rel="shortcut icon" href="${assets.resolve("favicon.png")}" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed" rel="stylesheet">
|
||||
<title>${resolveTitleFromPath(routeData, path)}</title>
|
||||
<style>${require("./index.css")}</style>
|
||||
<style data-aphrodite>${css.content}</style>
|
||||
</head>
|
||||
<body style="overflow-y:scroll">
|
||||
<div id="root">${html}</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
callback(null, page);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user