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:
Dandelion Mané 2020-06-21 20:58:49 -07:00 committed by GitHub
parent f9d62188e4
commit 84c9122a2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 0 additions and 1739 deletions

View File

@ -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"),

View File

@ -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());

View File

@ -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 "$@"

View File

@ -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

View File

@ -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);
}}
/>
);
}
}

View File

@ -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>
);
}

View File

@ -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;
}
}

View File

@ -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>
);
}

View File

@ -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>
Its 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 projects 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>Were 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
persons cred directly to contributions theyve 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 communitys 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. Its transparent: you can
always see how a nodes weight dervies from its neighbors. Its
extensible: plugins can embed new types of nodes and edges into the
graph. Its community-controlled: the weights, heuristics, and
algorithms are all configured by the project. Finally, its
decentralized: every project can run its own instance.
</p>
<p>
Naturally, there will be attempts to game the system. Well 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
SourceCreds 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 projects 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 dont 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, wed 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,
},
});

View File

@ -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)",
},
});

View File

@ -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} />
);
}
};
}

View File

@ -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>
);
}
};
}

View File

@ -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} />
);
}
};
}

View File

@ -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>
);
}

View File

@ -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);
}

View File

@ -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>
);
}

View File

@ -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} />
);
}
}

View File

@ -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}
/>
);
}
}

View File

@ -1,5 +0,0 @@
body {
margin: 0;
padding: 0;
font-family: 'Roboto', sans-serif;
}

View File

@ -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
}

View File

@ -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;

View File

@ -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 + "/");
}
}
});
});

View File

@ -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);
}
}