mirror of
https://github.com/wassname/talk.git
synced 2026-06-27 18:07:26 +08:00
Merge client into next (#1709)
* Merge client * Add linting script * Rename serve to start:development * Move error harmonization and handling to network layer * Show Comment Stream * Added initial test
This commit is contained in:
+13
-2
@@ -1,5 +1,16 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
*.js
|
||||
yarn.lock
|
||||
npm-debug.log*
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
coverage
|
||||
|
||||
.idea/
|
||||
.docz
|
||||
*.swp
|
||||
*.DS_STORE
|
||||
|
||||
*.css.d.ts
|
||||
__generated__
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"execMap": {
|
||||
"ts": "ts-node"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
dist
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"trailingComma": "es5"
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const paths = require("./paths");
|
||||
|
||||
// Make sure that including paths.js after env.js will read .env variables.
|
||||
delete require.cache[require.resolve("./paths")];
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
if (!NODE_ENV) {
|
||||
throw new Error(
|
||||
"The NODE_ENV environment variable is required but was not specified."
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
|
||||
var dotenvFiles = [
|
||||
`${paths.dotenv}.${NODE_ENV}.local`,
|
||||
`${paths.dotenv}.${NODE_ENV}`,
|
||||
// Don't include `.env.local` for `test` environment
|
||||
// since normally you expect tests to produce the same
|
||||
// results for everyone
|
||||
NODE_ENV !== "test" && `${paths.dotenv}.local`,
|
||||
paths.dotenv,
|
||||
].filter(Boolean);
|
||||
|
||||
// Load environment variables from .env* files. Suppress warnings using silent
|
||||
// if this file is missing. dotenv will never modify any environment variables
|
||||
// that have already been set. Variable expansion is supported in .env files.
|
||||
// https://github.com/motdotla/dotenv
|
||||
// https://github.com/motdotla/dotenv-expand
|
||||
dotenvFiles.forEach(dotenvFile => {
|
||||
if (fs.existsSync(dotenvFile)) {
|
||||
require("dotenv-expand")(
|
||||
require("dotenv").config({
|
||||
path: dotenvFile,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// We support resolving modules according to `NODE_PATH`.
|
||||
// This lets you use absolute paths in imports inside large monorepos:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/253.
|
||||
// It works similar to `NODE_PATH` in Node itself:
|
||||
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
|
||||
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
|
||||
// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims.
|
||||
// https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421
|
||||
// We also resolve them to make sure all tools using them work consistently.
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
process.env.NODE_PATH = (process.env.NODE_PATH || "")
|
||||
.split(path.delimiter)
|
||||
.filter(folder => folder && !path.isAbsolute(folder))
|
||||
.map(folder => path.resolve(appDirectory, folder))
|
||||
.join(path.delimiter);
|
||||
|
||||
// Grab NODE_ENV and TALK_* environment variables and prepare them to be
|
||||
// injected into the application via DefinePlugin in Webpack configuration.
|
||||
const REACT_APP = /^TALK_/i;
|
||||
|
||||
function getClientEnvironment(publicUrl) {
|
||||
const raw = Object.keys(process.env)
|
||||
.filter(key => REACT_APP.test(key))
|
||||
.reduce(
|
||||
(env, key) => {
|
||||
env[key] = process.env[key];
|
||||
return env;
|
||||
},
|
||||
{
|
||||
// Useful for determining whether we’re running in production mode.
|
||||
// Most importantly, it switches React into the correct mode.
|
||||
NODE_ENV: process.env.NODE_ENV || "development",
|
||||
// Useful for resolving the correct path to static assets in `public`.
|
||||
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
|
||||
// This should only be used as an escape hatch. Normally you would put
|
||||
// images into the `src` and `import` them in code to get their paths.
|
||||
PUBLIC_URL: publicUrl,
|
||||
}
|
||||
);
|
||||
// Stringify all values so we can feed into Webpack DefinePlugin
|
||||
const stringified = {
|
||||
"process.env": Object.keys(raw).reduce((env, key) => {
|
||||
env[key] = JSON.stringify(raw[key]);
|
||||
return env;
|
||||
}, {}),
|
||||
};
|
||||
|
||||
return { raw, stringified };
|
||||
}
|
||||
|
||||
module.exports = getClientEnvironment;
|
||||
@@ -0,0 +1,41 @@
|
||||
const paths = require("./paths");
|
||||
|
||||
module.exports = {
|
||||
rootDir: "../",
|
||||
roots: ["<rootDir>/src", "<rootDir>/scripts"],
|
||||
collectCoverageFrom: [
|
||||
"src/**/*.{js,jsx,mjs,ts,tsx}"
|
||||
],
|
||||
coveragePathIgnorePatterns: ["/node_modules/"],
|
||||
setupFiles: [
|
||||
"<rootDir>/config/polyfills.js"
|
||||
],
|
||||
testMatch: [
|
||||
"**/__tests__/**/*.{js,jsx,mjs,ts,tsx}",
|
||||
"**/*.(spec|test).{js,jsx,mjs,ts,tsx}"
|
||||
],
|
||||
testEnvironment: "node",
|
||||
testURL: "http://localhost",
|
||||
transform: {
|
||||
"^.+\\.(js|jsx|mjs|ts|tsx)$": "<rootDir>/node_modules/ts-jest",
|
||||
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
|
||||
"^(?!.*\\.(js|jsx|mjs|css|json|ftl)$)": "<rootDir>/config/jest/fileTransform.js"
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|ts|tsx)$"
|
||||
],
|
||||
moduleNameMapper: {
|
||||
"^react-native$": "react-native-web"
|
||||
},
|
||||
moduleFileExtensions: [
|
||||
"web.js",
|
||||
"js",
|
||||
"json",
|
||||
"web.jsx",
|
||||
"jsx",
|
||||
"node",
|
||||
"mjs",
|
||||
"ts",
|
||||
"tsx"
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
// This is a custom Jest transformer turning style imports into empty objects.
|
||||
// http://facebook.github.io/jest/docs/en/webpack.html
|
||||
|
||||
module.exports = {
|
||||
process() {
|
||||
return 'module.exports = {};';
|
||||
},
|
||||
getCacheKey() {
|
||||
// The output is always the same.
|
||||
return 'cssTransform';
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
// This is a custom Jest transformer turning file imports into filenames.
|
||||
// http://facebook.github.io/jest/docs/en/webpack.html
|
||||
|
||||
module.exports = {
|
||||
process(src, filename) {
|
||||
return `module.exports = ${JSON.stringify(path.basename(filename))};`;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"exec": "npm-run-all compile:relay-stream",
|
||||
"ext": "ts,tsx,graphql",
|
||||
"watch": [
|
||||
"./src/core/client/stream",
|
||||
"./src/core/**/*.graphql"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"exec": "npm run start:development",
|
||||
"ext": "ts,graphql",
|
||||
"watch": [
|
||||
"./src"
|
||||
],
|
||||
"ignore": [
|
||||
"./src/client"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
|
||||
// A script from `create-react-app` ejected `25.06.2018`.
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const url = require("url");
|
||||
|
||||
// Make sure any symlinks in the project folder are resolved:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/637
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
|
||||
|
||||
const envPublicUrl = process.env.PUBLIC_URL;
|
||||
|
||||
function ensureSlash(p, needsSlash) {
|
||||
const hasSlash = p.endsWith("/");
|
||||
if (hasSlash && !needsSlash) {
|
||||
return p.substr(p, p.length - 1);
|
||||
} else if (!hasSlash && needsSlash) {
|
||||
return `${p}/`;
|
||||
} else {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
const getPublicUrl = appPackageJson =>
|
||||
envPublicUrl || require(appPackageJson).homepage;
|
||||
|
||||
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
|
||||
// "public path" at which the app is served.
|
||||
// Webpack needs to know it to put the right <script> hrefs into HTML even in
|
||||
// single-page apps that may serve index.html for nested URLs like /todos/42.
|
||||
// We can't use a relative path in HTML because we don't want to load something
|
||||
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
|
||||
function getServedPath(appPackageJson) {
|
||||
const publicUrl = getPublicUrl(appPackageJson);
|
||||
const servedUrl =
|
||||
envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : "/");
|
||||
return ensureSlash(servedUrl, true);
|
||||
}
|
||||
|
||||
// config after eject: we're in ./config/
|
||||
module.exports = {
|
||||
dotenv: resolveApp(".env"),
|
||||
appPostCssConfig: resolveApp("config/postcss.config.js"),
|
||||
appJestConfig: resolveApp("config/jest.config.js"),
|
||||
appLoaders: resolveApp("loaders"),
|
||||
appDist: resolveApp("dist"),
|
||||
appPublic: resolveApp("public"),
|
||||
appPackageJson: resolveApp("package.json"),
|
||||
appSrc: resolveApp("src"),
|
||||
appTsconfig: resolveApp("src/core/client/tsconfig.json"),
|
||||
appLocales: resolveApp("src/locales"),
|
||||
appThemeVariables: resolveApp("src/core/client/ui/theme/variables.ts"),
|
||||
testsSetup: resolveApp("src/setupTests.js"),
|
||||
appNodeModules: resolveApp("node_modules"),
|
||||
|
||||
publicUrl: getPublicUrl(resolveApp("package.json")),
|
||||
servedPath: getServedPath(resolveApp("package.json")),
|
||||
|
||||
appStreamHtml: resolveApp("src/core/client/stream/index.html"),
|
||||
appStreamLocalesTemplate: resolveApp("src/core/client/stream/locales.ts"),
|
||||
appStreamIndex: resolveApp("src/core/client/stream/index.tsx"),
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
require("@babel/polyfill");
|
||||
@@ -0,0 +1,32 @@
|
||||
const precss = require("precss");
|
||||
const autoprefixer = require("autoprefixer");
|
||||
const fontMagician = require("postcss-font-magician");
|
||||
const kebabCase = require("lodash/kebabCase");
|
||||
const mapKeys = require("lodash/mapKeys");
|
||||
const flat = require("flat");
|
||||
const flexbugsFixes = require("postcss-flexbugs-fixes");
|
||||
const paths = require('./paths');
|
||||
|
||||
delete require.cache[paths.appThemeVariables];
|
||||
const variables = require(paths.appThemeVariables);
|
||||
const flatKebabVariables = mapKeys(flat(variables, {delimiter: "-"}), (_, k) => kebabCase(k));
|
||||
|
||||
module.exports = {
|
||||
// Necessary for external CSS imports to work
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2677
|
||||
ident: 'postcss',
|
||||
plugins: [
|
||||
precss({ variables: flatKebabVariables }),
|
||||
fontMagician(),
|
||||
flexbugsFixes,
|
||||
autoprefixer({
|
||||
browsers: [
|
||||
'>1%',
|
||||
'last 4 versions',
|
||||
'Firefox ESR',
|
||||
'not ie < 9', // React doesn't support IE8 anyway
|
||||
],
|
||||
flexbox: 'no-2009',
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,328 @@
|
||||
"use strict";
|
||||
|
||||
const autoprefixer = require("autoprefixer");
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const CaseSensitivePathsPlugin = require("case-sensitive-paths-webpack-plugin");
|
||||
const InterpolateHtmlPlugin = require("react-dev-utils/InterpolateHtmlPlugin");
|
||||
const WatchMissingNodeModulesPlugin = require("react-dev-utils/WatchMissingNodeModulesPlugin");
|
||||
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin");
|
||||
const getClientEnvironment = require("./env");
|
||||
const paths = require("./paths");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
|
||||
// Webpack uses `publicPath` to determine where the app is being served from.
|
||||
// In development, we always serve from the root. This makes config easier.
|
||||
const publicPath = "/";
|
||||
// `publicUrl` is just like `publicPath`, but we will provide it to our app
|
||||
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
|
||||
// Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz.
|
||||
const publicUrl = "";
|
||||
// Get environment variables to inject into our app.
|
||||
const env = getClientEnvironment(publicUrl);
|
||||
|
||||
// This is the development configuration.
|
||||
// It is focused on developer experience and fast rebuilds.
|
||||
// The production configuration is different and lives in a separate file.
|
||||
module.exports = {
|
||||
// Set webpack mode.
|
||||
mode: process.env.NODE_ENV === "production" ? "production" : "development",
|
||||
|
||||
// You may want 'eval' instead if you prefer to see the compiled output in DevTools.
|
||||
// See the discussion in https://github.com/facebookincubator/create-react-app/issues/343.
|
||||
devtool: "cheap-module-source-map",
|
||||
// These are the "entry points" to our application.
|
||||
// This means they will be the "root" imports that are included in JS bundle.
|
||||
// The first two entry points enable "hot" CSS and auto-refreshes for JS.
|
||||
entry: [
|
||||
// We ship polyfills by default:
|
||||
require.resolve("./polyfills"),
|
||||
// Include an alternative client for WebpackDevServer. A client's job is to
|
||||
// connect to WebpackDevServer by a socket and get notified about changes.
|
||||
// When you save a file, the client will either apply hot updates (in case
|
||||
// of CSS changes), or refresh the page (in case of JS changes). When you
|
||||
// make a syntax error, this client will display a syntax error overlay.
|
||||
// Note: instead of the default WebpackDevServer client, we use a custom one
|
||||
// to bring better experience for Create React App users. You can replace
|
||||
// the line below with these two lines if you prefer the stock client:
|
||||
// require.resolve('webpack-dev-server/client') + '?/',
|
||||
// require.resolve('webpack/hot/dev-server'),
|
||||
require.resolve("react-dev-utils/webpackHotDevClient"),
|
||||
// Finally, this is your app's code:
|
||||
paths.appStreamIndex,
|
||||
// We include the app code last so that if there is a runtime error during
|
||||
// initialization, it doesn't blow up the WebpackDevServer client, and
|
||||
// changing JS code would still trigger a refresh.
|
||||
],
|
||||
output: {
|
||||
// Add /* filename */ comments to generated require()s in the output.
|
||||
pathinfo: true,
|
||||
// This does not produce a real file. It's just the virtual path that is
|
||||
// served by WebpackDevServer in development. This is the JS bundle
|
||||
// containing code from all our entry points, and the Webpack runtime.
|
||||
filename: "static/js/bundle.js",
|
||||
// There are also additional JS chunk files if you use code splitting.
|
||||
chunkFilename: "static/js/[name].chunk.js",
|
||||
// This is the URL that app is served from. We use "/" in development.
|
||||
publicPath: publicPath,
|
||||
// Point sourcemap entries to original disk location (format as URL on Windows)
|
||||
devtoolModuleFilenameTemplate: info =>
|
||||
path.resolve(info.absoluteResourcePath).replace(/\\/g, "/"),
|
||||
},
|
||||
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].concat(
|
||||
// It is guaranteed to exist because we tweak it in `env.js`
|
||||
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",
|
||||
".ts",
|
||||
".tsx",
|
||||
],
|
||||
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]),
|
||||
// Support `tsconfig.json` `path` setting.
|
||||
new TsconfigPathsPlugin({
|
||||
configFile: paths.appTsconfig,
|
||||
extensions: [".js", ".jsx", ".mjs", ".ts", ".tsx"],
|
||||
}),
|
||||
],
|
||||
},
|
||||
resolveLoader: {
|
||||
// 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, paths.appLoaders].concat(
|
||||
// It is guaranteed to exist because we tweak it in `env.js`
|
||||
process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
|
||||
),
|
||||
},
|
||||
module: {
|
||||
strictExportPresence: true,
|
||||
rules: [
|
||||
// Disable require.ensure as it's not a standard language feature.
|
||||
{ parser: { requireEnsure: false } },
|
||||
|
||||
// First, run the linter.
|
||||
// It's important to do this before Babel processes the JS.
|
||||
{
|
||||
test: /\.(js|jsx|mjs|ts|tsx)$/,
|
||||
enforce: "pre",
|
||||
use: [
|
||||
{
|
||||
options: {
|
||||
tsConfigFile: paths.appTsconfig,
|
||||
},
|
||||
loader: require.resolve("tslint-loader"),
|
||||
},
|
||||
],
|
||||
include: paths.appSrc,
|
||||
},
|
||||
{
|
||||
// "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: [
|
||||
{
|
||||
test: paths.appStreamLocalesTemplate,
|
||||
use: [
|
||||
// This is the locales loader that loads available locales
|
||||
// from a particular target.
|
||||
{
|
||||
loader: "locales-loader",
|
||||
options: {
|
||||
pathToLocales: paths.appLocales,
|
||||
|
||||
// Default locale if non could be negotiated.
|
||||
defaultLocale: "en-US",
|
||||
|
||||
// Fallback locale if a translation was not found.
|
||||
// If not set, will use the text that is already
|
||||
// in the code base.
|
||||
fallbackLocale: "en-US",
|
||||
|
||||
// Common fluent files are always included in the locale bundles.
|
||||
commonFiles: ["framework.ftl", "common.ftl"],
|
||||
|
||||
// Locales that come with the main bundle. Others are loaded on demand.
|
||||
bundled: ["en-US"],
|
||||
|
||||
// Target specifies the prefix for fluent files to be loaded.
|
||||
// ${target}-xyz.ftl and ${†arget}.ftl are loaded into the locales.
|
||||
target: "stream",
|
||||
|
||||
// All available locales can be loadable on demand.
|
||||
// To restrict available locales set:
|
||||
// availableLocales: ["en-US"]
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// Loader for our fluent files.
|
||||
{
|
||||
test: /\.ftl$/,
|
||||
use: ["raw-loader"],
|
||||
},
|
||||
// "url" loader works like "file" loader except that it embeds assets
|
||||
// smaller than specified limit in bytes as data URLs to avoid requests.
|
||||
// A missing `test` is equivalent to a match.
|
||||
{
|
||||
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|ts|tsx)$/,
|
||||
include: paths.appSrc,
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve("babel-loader"),
|
||||
options: {
|
||||
// This is a feature of `babel-loader` for webpack (not Babel itself).
|
||||
// It enables caching results in ./node_modules/.cache/babel-loader/
|
||||
// directory for faster rebuilds.
|
||||
cacheDirectory: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
compilerOptions: {
|
||||
target: "es2015",
|
||||
module: "esnext",
|
||||
jsx: "preserve",
|
||||
noEmit: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// "postcss" loader applies autoprefixer to our CSS.
|
||||
// "css" loader resolves paths in CSS and adds assets as dependencies.
|
||||
// "style" loader turns CSS into JS modules that inject <style> tags.
|
||||
// In production, we use a plugin to extract that CSS to a file, but
|
||||
// in development "style" loader enables hot editing of CSS.
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
require.resolve("style-loader"),
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
modules: true,
|
||||
importLoaders: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("postcss-loader"),
|
||||
options: {
|
||||
config: {
|
||||
path: paths.appPostCssConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// "file" loader makes sure those assets get served by WebpackDevServer.
|
||||
// When you `import` an asset, you get its (virtual) filename.
|
||||
// In production, they would get copied to the `build` folder.
|
||||
// This loader doesn't use a "test" so it will catch all modules
|
||||
// that fall through the other loaders.
|
||||
{
|
||||
// Exclude `js` files to keep "css" loader working as it injects
|
||||
// its 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|ts|tsx)$/, /\.html$/, /\.json$/],
|
||||
loader: require.resolve("file-loader"),
|
||||
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: [
|
||||
// Generates an `index.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
template: paths.appStreamHtml,
|
||||
}),
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// In development, this will be an empty string.
|
||||
new InterpolateHtmlPlugin(env.raw),
|
||||
// Add module names to factory functions so they appear in browser profiler.
|
||||
new webpack.NamedModulesPlugin(),
|
||||
// Makes some environment variables available to the JS code, for example:
|
||||
// if (process.env.NODE_ENV === 'development') { ... }. See `./env.js`.
|
||||
new webpack.DefinePlugin(env.stringified),
|
||||
// This is necessary to emit hot updates (currently CSS only):
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
// Watcher doesn't work well if you mistype casing in a path so we use
|
||||
// a plugin that prints an error when you attempt to do this.
|
||||
// See https://github.com/facebookincubator/create-react-app/issues/240
|
||||
new CaseSensitivePathsPlugin(),
|
||||
// If you require a missing module and then `npm install` it, you still have
|
||||
// to restart the development server for Webpack to discover it. This plugin
|
||||
// makes the discovery automatic so you don't have to restart.
|
||||
// See https://github.com/facebookincubator/create-react-app/issues/186
|
||||
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
|
||||
// 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$/),
|
||||
],
|
||||
// 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",
|
||||
},
|
||||
// Turn off performance hints during development because we don't do any
|
||||
// splitting or minification in interest of speed. These warnings become
|
||||
// cumbersome.
|
||||
performance: {
|
||||
hints: false,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,388 @@
|
||||
"use strict";
|
||||
|
||||
const autoprefixer = require("autoprefixer");
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const ExtractTextPlugin = require("extract-text-webpack-plugin");
|
||||
const ManifestPlugin = require("webpack-manifest-plugin");
|
||||
const InterpolateHtmlPlugin = require("react-dev-utils/InterpolateHtmlPlugin");
|
||||
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin");
|
||||
const getClientEnvironment = require("./env");
|
||||
const paths = require("./paths");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
|
||||
|
||||
// Webpack uses `publicPath` to determine where the app is being served from.
|
||||
// It requires a trailing slash, or the file assets will get an incorrect path.
|
||||
const publicPath = paths.servedPath;
|
||||
// Some apps do not use client-side routing with pushState.
|
||||
// For these, "homepage" can be set to "." to enable relative asset paths.
|
||||
const shouldUseRelativeAssetPaths = publicPath === "./";
|
||||
// Source maps are resource heavy and can cause out of memory issue for large source files.
|
||||
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false";
|
||||
// `publicUrl` is just like `publicPath`, but we will provide it to our app
|
||||
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
|
||||
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
|
||||
const publicUrl = publicPath.slice(0, -1);
|
||||
// Get environment variables to inject into our app.
|
||||
const env = getClientEnvironment(publicUrl);
|
||||
|
||||
// Assert this just to be safe.
|
||||
// Development builds of React are slow and not intended for production.
|
||||
if (env.stringified["process.env"].NODE_ENV !== '"production"') {
|
||||
throw new Error("Production builds must have NODE_ENV=production.");
|
||||
}
|
||||
|
||||
// Note: defined here because it will be used more than once.
|
||||
// We use [md5:contenthash:hex:20] instead of [contenthash:8]
|
||||
// because of this bug https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/763.
|
||||
// TODO: Repalce with mini-css-extract-plugin once it supports HMR.
|
||||
// https://github.com/webpack-contrib/mini-css-extract-plugin
|
||||
const cssFilename = "static/css/[name].[md5:contenthash:hex:20].css";
|
||||
|
||||
// ExtractTextPlugin expects the build output to be flat.
|
||||
// (See https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/27)
|
||||
// However, our output is structured with css, js and media folders.
|
||||
// To have this structure working with relative paths, we have to use custom options.
|
||||
const extractTextPluginOptions = shouldUseRelativeAssetPaths
|
||||
? // Making sure that the publicPath goes back to to build folder.
|
||||
{ publicPath: Array(cssFilename.split("/").length).join("../") }
|
||||
: {};
|
||||
|
||||
// This is the production configuration.
|
||||
// It compiles slowly and is focused on producing a fast and minimal bundle.
|
||||
// The development configuration is different and lives in a separate file.
|
||||
module.exports = {
|
||||
// Don't attempt to continue if there are any errors.
|
||||
bail: true,
|
||||
|
||||
// Set webpack mode.
|
||||
mode: "production",
|
||||
|
||||
// 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: [require.resolve("./polyfills"), paths.appStreamIndex],
|
||||
output: {
|
||||
// The dist folder.
|
||||
path: paths.appDist,
|
||||
// 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",
|
||||
// We inferred the "public path" (such as / or /my-project) from homepage.
|
||||
publicPath: publicPath,
|
||||
// Point sourcemap entries to original disk location (format as URL on Windows)
|
||||
devtoolModuleFilenameTemplate: info =>
|
||||
path
|
||||
.relative(paths.appSrc, info.absoluteResourcePath)
|
||||
.replace(/\\/g, "/"),
|
||||
},
|
||||
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].concat(
|
||||
// It is guaranteed to exist because we tweak it in `env.js`
|
||||
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",
|
||||
".ts",
|
||||
".tsx",
|
||||
],
|
||||
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]),
|
||||
// Support `tsconfig.json` `path` setting.
|
||||
new TsconfigPathsPlugin({
|
||||
configFile: paths.appTsconfig,
|
||||
extensions: [".js", ".jsx", ".mjs", ".ts", ".tsx"],
|
||||
}),
|
||||
],
|
||||
},
|
||||
resolveLoader: {
|
||||
// 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, paths.appLoaders].concat(
|
||||
// It is guaranteed to exist because we tweak it in `env.js`
|
||||
process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
|
||||
),
|
||||
},
|
||||
module: {
|
||||
strictExportPresence: true,
|
||||
rules: [
|
||||
// Disable require.ensure as it's not a standard language feature.
|
||||
{ parser: { requireEnsure: false } },
|
||||
|
||||
// First, run the linter.
|
||||
// It's important to do this before Babel processes the JS.
|
||||
{
|
||||
test: /\.(js|jsx|mjs|ts|tsx)$/,
|
||||
enforce: "pre",
|
||||
use: [
|
||||
{
|
||||
options: {
|
||||
tsConfigFile: paths.appTsconfig,
|
||||
},
|
||||
loader: require.resolve("tslint-loader"),
|
||||
},
|
||||
],
|
||||
include: paths.appSrc,
|
||||
},
|
||||
{
|
||||
// "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: [
|
||||
{
|
||||
test: paths.appStreamLocalesTemplate,
|
||||
use: [
|
||||
// This is the locales loader that loads available locales
|
||||
// from a particular target.
|
||||
{
|
||||
loader: "locales-loader",
|
||||
options: {
|
||||
pathToLocales: paths.appLocales,
|
||||
|
||||
// Default locale if non could be negotiated.
|
||||
defaultLocale: "en-US",
|
||||
|
||||
// Fallback locale if a translation was not found.
|
||||
// If not set, will use the text that is already
|
||||
// in the code base.
|
||||
fallbackLocale: "en-US",
|
||||
|
||||
// Common fluent files are always included in the locale bundles.
|
||||
commonFiles: ["framework.ftl", "common.ftl"],
|
||||
|
||||
// Locales that come with the main bundle. Others are loaded on demand.
|
||||
bundled: ["en-US"],
|
||||
|
||||
// Target specifies the prefix for fluent files to be loaded.
|
||||
// ${target}-xyz.ftl and ${†arget}.ftl are loaded into the locales.
|
||||
target: "stream",
|
||||
|
||||
// All available locales can be loadable on demand.
|
||||
// To restrict available locales set:
|
||||
// availableLocales: ["en-US"]
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// Loader for our fluent files.
|
||||
{
|
||||
test: /\.ftl$/,
|
||||
use: ["raw-loader"],
|
||||
},
|
||||
// "url" loader works like "file" loader except that it embeds assets
|
||||
// smaller than specified limit in bytes as data URLs to avoid requests.
|
||||
// A missing `test` is equivalent to a match.
|
||||
{
|
||||
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|ts|tsx)$/,
|
||||
include: paths.appSrc,
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve("babel-loader"),
|
||||
options: {
|
||||
compact: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
compilerOptions: {
|
||||
target: "es2015",
|
||||
module: "esnext",
|
||||
jsx: "preserve",
|
||||
noEmit: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// The notation here is somewhat confusing.
|
||||
// "postcss" loader applies autoprefixer to our CSS.
|
||||
// "css" loader resolves paths in CSS and adds assets as dependencies.
|
||||
// "style" loader normally turns CSS into JS modules injecting <style>,
|
||||
// but unlike in development configuration, we do something different.
|
||||
// `ExtractTextPlugin` first applies the "postcss" and "css" loaders
|
||||
// (second argument), then grabs the result CSS and puts it into a
|
||||
// separate file in our build process. This way we actually ship
|
||||
// a single CSS file in production instead of JS code injecting <style>
|
||||
// tags. If you use code splitting, however, any async bundles will still
|
||||
// use the "style" loader inside the async code so CSS from them won't be
|
||||
// in the main CSS file.
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: ExtractTextPlugin.extract(
|
||||
Object.assign(
|
||||
{
|
||||
fallback: {
|
||||
loader: require.resolve("style-loader"),
|
||||
options: {
|
||||
hmr: false,
|
||||
},
|
||||
},
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
modules: true,
|
||||
importLoaders: 1,
|
||||
minimize: true,
|
||||
sourceMap: shouldUseSourceMap,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("postcss-loader"),
|
||||
options: {
|
||||
config: {
|
||||
path: paths.appPostCssConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
extractTextPluginOptions
|
||||
)
|
||||
),
|
||||
// Note: this won't work without `new ExtractTextPlugin()` in `plugins`.
|
||||
},
|
||||
// "file" loader makes sure those assets get served by WebpackDevServer.
|
||||
// When you `import` an asset, you get its (virtual) filename.
|
||||
// In production, they would get copied to the `build` folder.
|
||||
// This loader doesn't use a "test" so it will catch all modules
|
||||
// that fall through the other loaders.
|
||||
{
|
||||
// Exclude `js` files to keep "css" loader working as it injects
|
||||
// its 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|ts|tsx)$/, /\.html$/, /\.json$/],
|
||||
loader: require.resolve("file-loader"),
|
||||
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: [
|
||||
// Generates an `index.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
template: paths.appStreamHtml,
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeRedundantAttributes: true,
|
||||
useShortDoctype: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
keepClosingSlash: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
minifyURLs: true,
|
||||
},
|
||||
}),
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// In development, this will be an empty string.
|
||||
new InterpolateHtmlPlugin(env.raw),
|
||||
// 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),
|
||||
// Minify the code.
|
||||
new UglifyJsPlugin({
|
||||
uglifyOptions: {
|
||||
compress: {
|
||||
warnings: false,
|
||||
// Disabled because of an issue with Uglify breaking seemingly valid code:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2376
|
||||
// Pending further investigation:
|
||||
// https://github.com/mishoo/UglifyJS2/issues/2011
|
||||
comparisons: false,
|
||||
},
|
||||
mangle: {
|
||||
safari10: true,
|
||||
},
|
||||
output: {
|
||||
comments: false,
|
||||
// Turned on because emoji and regex is not minified properly using default
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2488
|
||||
ascii_only: true,
|
||||
},
|
||||
sourceMap: shouldUseSourceMap,
|
||||
},
|
||||
}),
|
||||
// Note: this won't work without ExtractTextPlugin.extract(..) in `loaders`.
|
||||
new ExtractTextPlugin({
|
||||
filename: cssFilename,
|
||||
}),
|
||||
// Generate a manifest file which contains a mapping of all asset filenames
|
||||
// to their corresponding output file so that tools can pick it up without
|
||||
// having to parse `index.html`.
|
||||
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$/),
|
||||
],
|
||||
// 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",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
"use strict";
|
||||
|
||||
const errorOverlayMiddleware = require("react-dev-utils/errorOverlayMiddleware");
|
||||
const noopServiceWorkerMiddleware = require("react-dev-utils/noopServiceWorkerMiddleware");
|
||||
const ignoredFiles = require("react-dev-utils/ignoredFiles");
|
||||
const config = require("./webpack.config.dev");
|
||||
const paths = require("./paths");
|
||||
|
||||
const protocol = process.env.HTTPS === "true" ? "https" : "http";
|
||||
const host = process.env.HOST || "0.0.0.0";
|
||||
const serverPort = process.env.PORT || 3000;
|
||||
|
||||
module.exports = function(proxy, allowedHost) {
|
||||
return {
|
||||
// WebpackDevServer 2.4.3 introduced a security fix that prevents remote
|
||||
// websites from potentially accessing local content through DNS rebinding:
|
||||
// https://github.com/webpack/webpack-dev-server/issues/887
|
||||
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
|
||||
// However, it made several existing use cases such as development in cloud
|
||||
// environment or subdomains in development significantly more complicated:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2271
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2233
|
||||
// While we're investigating better solutions, for now we will take a
|
||||
// compromise. Since our WDS configuration only serves files in the `public`
|
||||
// folder we won't consider accessing them a vulnerability. However, if you
|
||||
// use the `proxy` feature, it gets more dangerous because it can expose
|
||||
// remote code execution vulnerabilities in backends like Django and Rails.
|
||||
// So we will disable the host check normally, but enable it if you have
|
||||
// specified the `proxy` setting. Finally, we let you override it if you
|
||||
// really know what you're doing with a special environment variable.
|
||||
disableHostCheck:
|
||||
!proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === "true",
|
||||
// Enable gzip compression of generated files.
|
||||
compress: true,
|
||||
// Silence WebpackDevServer's own logs since they're generally not useful.
|
||||
// It will still show compile warnings and errors with this setting.
|
||||
clientLogLevel: "none",
|
||||
// By default WebpackDevServer serves physical files from current directory
|
||||
// in addition to all the virtual build products that it serves from memory.
|
||||
// This is confusing because those files won’t automatically be available in
|
||||
// production build folder unless we copy them. However, copying the whole
|
||||
// project directory is dangerous because we may expose sensitive files.
|
||||
// Instead, we establish a convention that only files in `public` directory
|
||||
// get served. Our build script will copy `public` into the `build` folder.
|
||||
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
|
||||
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
|
||||
// Note that we only recommend to use `public` folder as an escape hatch
|
||||
// for files like `favicon.ico`, `manifest.json`, and libraries that are
|
||||
// for some reason broken when imported through Webpack. If you just want to
|
||||
// use an image, put it in `src` and `import` it from JavaScript instead.
|
||||
contentBase: paths.appPublic,
|
||||
// By default files from `contentBase` will not trigger a page reload.
|
||||
watchContentBase: true,
|
||||
// Enable hot reloading server. It will provide /sockjs-node/ endpoint
|
||||
// for the WebpackDevServer client so it can learn when the files were
|
||||
// updated. The WebpackDevServer client is included as an entry point
|
||||
// in the Webpack development configuration. Note that only changes
|
||||
// to CSS are currently hot reloaded. JS changes will refresh the browser.
|
||||
hot: true,
|
||||
// It is important to tell WebpackDevServer to use the same "root" path
|
||||
// as we specified in the config. In development, we always serve from /.
|
||||
publicPath: config.output.publicPath,
|
||||
// WebpackDevServer is noisy by default so we emit custom message instead
|
||||
// by listening to the compiler events with `compiler.plugin` calls above.
|
||||
quiet: true,
|
||||
// Reportedly, this avoids CPU overload on some systems.
|
||||
// https://github.com/facebookincubator/create-react-app/issues/293
|
||||
// src/node_modules is not ignored to support absolute imports
|
||||
// https://github.com/facebookincubator/create-react-app/issues/1065
|
||||
watchOptions: {
|
||||
ignored: ignoredFiles(paths.appSrc),
|
||||
},
|
||||
// Enable HTTPS if the HTTPS environment variable is set to 'true'
|
||||
https: protocol === "https",
|
||||
host: host,
|
||||
overlay: false,
|
||||
historyApiFallback: {
|
||||
// Paths with dots should still use the history fallback.
|
||||
// See https://github.com/facebookincubator/create-react-app/issues/387.
|
||||
disableDotRule: true,
|
||||
},
|
||||
public: allowedHost,
|
||||
// Proxy to the graphql server.
|
||||
proxy: proxy || {
|
||||
"/api": {
|
||||
target: `http://localhost:${serverPort}`,
|
||||
},
|
||||
},
|
||||
before(app) {
|
||||
// This lets us open files from the runtime error overlay.
|
||||
app.use(errorOverlayMiddleware());
|
||||
// This service worker file is effectively a 'no-op' that will reset any
|
||||
// previous service worker registered for the same host:port combination.
|
||||
// We do this in development to avoid hitting the production cache if
|
||||
// it used the same host and port.
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432
|
||||
app.use(noopServiceWorkerMiddleware());
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
const extensions = [".ts", ".tsx", ".js"];
|
||||
const paths = require("./config/paths");
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require("./config/env");
|
||||
|
||||
// const stringify = require('json-stringify-safe');
|
||||
|
||||
export default {
|
||||
title: "Talk 5.0",
|
||||
source: "./src",
|
||||
typescript: true,
|
||||
host: process.env.HOST || "0.0.0.0",
|
||||
port: parseInt(process.env.DOCZ_PORT, 10) || 3000,
|
||||
modifyBundlerConfig: config => {
|
||||
config.module.rules.push({
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
require.resolve("style-loader"),
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
modules: true,
|
||||
importLoaders: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("postcss-loader"),
|
||||
options: {
|
||||
config: {
|
||||
path: paths.appPostCssConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
config.resolve.plugins = [new TsconfigPathsPlugin({ extensions, configFile: paths.appTsconfig })];
|
||||
// fs.writeFileSync(path.resolve(__dirname, "tmp"), stringify(config, null, 2));
|
||||
return config;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,149 @@
|
||||
const loaderUtils = require('loader-utils');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const camelCase = require('lodash/camelCase');
|
||||
const upperFirst = require('lodash/upperFirst');
|
||||
const memoize = require('lodash/memoize');
|
||||
|
||||
const pascalCase = (x) => upperFirst(camelCase(x));
|
||||
|
||||
/**
|
||||
* Default values for every param that can be passed in the loader query.
|
||||
*/
|
||||
const DEFAULT_QUERY_VALUES = {
|
||||
// Path to locales.
|
||||
pathToLocales: null,
|
||||
|
||||
// Default locale if non could be negotiated.
|
||||
defaultLocale: 'en-US',
|
||||
|
||||
// Fallback locale if a translation was not found.
|
||||
// If not set, will use the text that is already
|
||||
// in the code base.
|
||||
fallbackLocale: '',
|
||||
|
||||
// If set, restrict to this list of available locales.
|
||||
availableLocales: null,
|
||||
|
||||
// Common fluent files are always included in the locale bundles.
|
||||
commonFiles: [],
|
||||
|
||||
// Locales that come with the main bundle. Others are loaded on demand.
|
||||
bundled: [],
|
||||
|
||||
// Target specifies the prefix for fluent files to be loaded. ${target}-xyz.ftl and ${†arget}.ftl are
|
||||
// loaded into the locales.
|
||||
target: '',
|
||||
};
|
||||
|
||||
function getFiles(target, pathToLocale, context) {
|
||||
const {pathToLocales, commonFiles} = context;
|
||||
|
||||
const common = [];
|
||||
const suffixes = [];
|
||||
|
||||
const files = fs.readdirSync(pathToLocale);
|
||||
|
||||
files.forEach(f => {
|
||||
if (commonFiles.includes(f)) {
|
||||
common.push(f);
|
||||
return;
|
||||
}
|
||||
if (f.startsWith(target)) {
|
||||
suffixes.push(f.substr(target.length));
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return {common, suffixes};
|
||||
}
|
||||
|
||||
function generateTarget(target, context) {
|
||||
const {defaultLocale, fallbackLocale, pathToLocales, locales, commonFiles, bundled} = context;
|
||||
const getLocalePath = locale => path.join(pathToLocales, locale);
|
||||
const getLocaleFiles = memoize(locale => getFiles(target, getLocalePath(locale), context));
|
||||
const loadables = locales.filter(local => !bundled.includes(local));
|
||||
|
||||
return `
|
||||
var ret = {
|
||||
defaultLocale: ${JSON.stringify(defaultLocale)},
|
||||
fallbackLocale: ${JSON.stringify(fallbackLocale)},
|
||||
availableLocales: ${JSON.stringify(locales)},
|
||||
bundled: {},
|
||||
loadables: {},
|
||||
};
|
||||
|
||||
// Bundled locales are directly available in the main bundle.
|
||||
${bundled.map(locale => `
|
||||
{
|
||||
var suffixes = ${JSON.stringify(getLocaleFiles(locale).suffixes)};
|
||||
var contents = [];
|
||||
${getLocaleFiles(locale).common.map(file => `
|
||||
contents.push(require('${getLocalePath(locale)}/${file}'));
|
||||
`).join("\n")}
|
||||
contents = contents.concat(suffixes.map(function(suffix) { return require(\`${getLocalePath(locale)}/${target}\${suffix}\`); }));
|
||||
ret.bundled[${JSON.stringify(locale)}] = contents.join("\\n");
|
||||
}
|
||||
`).join("\n")}
|
||||
|
||||
// Loadables are in a separate bundle, that can be easily loaded.
|
||||
${loadables.map(locale => `
|
||||
ret.loadables[${JSON.stringify(locale)}] = function() {
|
||||
var suffixes = ${JSON.stringify(getLocaleFiles(locale).suffixes)};
|
||||
var promises = [];
|
||||
${getLocaleFiles(locale).common.map(file => `
|
||||
promises.push(
|
||||
import(
|
||||
/* webpackChunkName: ${JSON.stringify(`${target}-locale-${locale}`)}, webpackMode: "lazy" */
|
||||
'${getLocalePath(locale)}/${file}'
|
||||
)
|
||||
);
|
||||
`).join("\n")}
|
||||
promises = promises.concat(suffixes.map(function(suffix) {
|
||||
return import(
|
||||
/* webpackChunkName: ${JSON.stringify(`${target}-locale-${locale}`)}, webpackMode: "lazy-once" */
|
||||
\`${getLocalePath(locale)}/${target}\${suffix}\`
|
||||
)
|
||||
}));
|
||||
return Promise.all(promises).then(function(modules) {
|
||||
return modules.map(function(m){return m.default}).join("\\n");
|
||||
});
|
||||
};
|
||||
`).join("\n")}
|
||||
module.exports = ret;
|
||||
`;
|
||||
}
|
||||
|
||||
module.exports = function(source) {
|
||||
const options = Object.assign({}, DEFAULT_QUERY_VALUES, loaderUtils.getOptions(this));
|
||||
const {pathToLocales, defaultLocale, fallbackLocale, availableLocales, target, bundled, commonFiles} = options;
|
||||
|
||||
let locales = fs.readdirSync(pathToLocales);
|
||||
if (availableLocales) {
|
||||
availableLocales.forEach(locale => {
|
||||
if (!locales.includes(locale)) {
|
||||
throw new Error(`locale ${fallbackLocale} not available`);
|
||||
}
|
||||
});
|
||||
locales = availableLocales;
|
||||
}
|
||||
|
||||
if (fallbackLocale && !locales.includes(fallbackLocale)) {
|
||||
throw new Error(`fallbackLocale ${fallbackLocale} not in available locales`);
|
||||
}
|
||||
if (!pathToLocales) {
|
||||
throw new Error(`pathToLocales is required`);
|
||||
}
|
||||
if (!defaultLocale) {
|
||||
throw new Error(`defaultLocale is required`);
|
||||
}
|
||||
|
||||
if (!locales.includes(defaultLocale)) {
|
||||
throw new Error(`defaultLocale ${defaultLocale} not in available locales`);
|
||||
}
|
||||
|
||||
const context = {pathToLocales, defaultLocale, fallbackLocale, commonFiles, locales, bundled};
|
||||
|
||||
this.cacheable();
|
||||
return generateTarget(target, context);
|
||||
};
|
||||
Generated
+16786
-5
File diff suppressed because it is too large
Load Diff
+86
-6
@@ -4,9 +4,23 @@
|
||||
"description": "A better commenting experience from Mozilla, The Washington Post, and The New York Times.",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"build": "tsc",
|
||||
"watch": "nodemon --config .nodemonrc.json src/index.ts",
|
||||
"lint": "prettier --write \"src/**/*.ts\""
|
||||
"test": "node scripts/test.js --env=jsdom",
|
||||
"build": "npm-run-all --parallel compile:* && npm-run-all --parallel build:*",
|
||||
"build:client": "node ./scripts/build.js",
|
||||
"build:server": "tsc",
|
||||
"watch": "NODE_ENV=development npm-run-all compile:* --parallel watch:*",
|
||||
"watch:client": "node ./scripts/start.js",
|
||||
"watch:css-types": "tcm src/core/client/ --watch",
|
||||
"watch:relay-stream": "nodemon --config ./config/nodemon/relay-stream.json",
|
||||
"watch:server": "nodemon --config ./config/nodemon/server.json",
|
||||
"compile:css-types": "tcm src/core/client/",
|
||||
"compile:relay-stream": "relay-compiler --src ./src/core/client/stream --schema ./src/core/server/graph/tenant/schema/schema.graphql --language typescript --artifactDirectory ./src/core/client/stream/__generated__ --no-watchman",
|
||||
"start:development": "NODE_ENV=development ts-node -r tsconfig-paths/register src/index.ts",
|
||||
"lint-fix": "tslint --fix --project ./tsconfig.json && tslint --fix --project ./src/core/client/tsconfig.json",
|
||||
"lint": "npm-run-all --parallel lint:*",
|
||||
"lint:server": "tslint --project ./tsconfig.json",
|
||||
"lint:client": "tslint --project ./src/core/client/tsconfig.json",
|
||||
"docz:watch": "docz dev"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
@@ -16,9 +30,11 @@
|
||||
"convict": "^4.3.0",
|
||||
"dataloader": "^1.4.0",
|
||||
"dotenv": "^6.0.0",
|
||||
"dotenv-expand": "^4.2.0",
|
||||
"dotize": "^0.2.0",
|
||||
"express": "^4.16.3",
|
||||
"express-static-gzip": "^0.3.2",
|
||||
"fs-extra": "^6.0.1",
|
||||
"graphql": "^0.13.2",
|
||||
"graphql-config": "^2.0.1",
|
||||
"graphql-redis-subscriptions": "^1.5.0",
|
||||
@@ -34,27 +50,91 @@
|
||||
"uuid": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.0.0-beta.49",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.0.0-beta.49",
|
||||
"@babel/polyfill": "7.0.0-beta.49",
|
||||
"@babel/preset-env": "7.0.0-beta.49",
|
||||
"@babel/preset-react": "7.0.0-beta.49",
|
||||
"@types/bunyan": "^1.8.4",
|
||||
"@types/classnames": "^2.2.4",
|
||||
"@types/convict": "^4.2.0",
|
||||
"@types/dotenv": "^4.0.3",
|
||||
"@types/express": "^4.16.0",
|
||||
"@types/graphql": "^0.13.1",
|
||||
"@types/ioredis": "^3.2.8",
|
||||
"@types/jest": "^23.1.1",
|
||||
"@types/joi": "^13.0.8",
|
||||
"@types/lodash": "^4.14.109",
|
||||
"@types/luxon": "^0.5.3",
|
||||
"@types/mongodb": "^3.0.19",
|
||||
"@types/node": "^10.3.1",
|
||||
"@types/passport": "^0.4.5",
|
||||
"@types/query-string": "^6.1.0",
|
||||
"@types/react-dom": "^16.0.6",
|
||||
"@types/react-relay": "^1.3.6",
|
||||
"@types/recompose": "^0.26.1",
|
||||
"@types/relay-runtime": "github:coralproject/patched#types/relay-runtime",
|
||||
"@types/uuid": "^3.4.3",
|
||||
"@types/ws": "^5.1.2",
|
||||
"autoprefixer": "^8.6.0",
|
||||
"babel-loader": "^8.0.0-beta",
|
||||
"babel-plugin-relay": "github:coralproject/patched#babel-plugin-relay",
|
||||
"babel-preset-react-optimize": "^1.0.1",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.1.2",
|
||||
"chalk": "^2.4.1",
|
||||
"classnames": "^2.2.5",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"css-loader": "^0.28.11",
|
||||
"docz": "^0.2.6",
|
||||
"docz-theme-default": "^0.2.10",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"final-form": "^4.8.1",
|
||||
"flat": "^4.0.0",
|
||||
"fluent": "^0.6.4",
|
||||
"fluent-intl-polyfill": "^0.1.0",
|
||||
"fluent-langneg": "^0.1.0",
|
||||
"fluent-react": "^0.7.0",
|
||||
"graphql-playground-middleware-express": "^1.7.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"jest": "^23.2.0",
|
||||
"loader-utils": "^1.1.0",
|
||||
"nodemon": "^1.17.5",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"postcss-flexbugs-fixes": "^3.3.1",
|
||||
"postcss-font-magician": "^2.2.1",
|
||||
"postcss-loader": "^2.1.5",
|
||||
"precss": "^3.1.2",
|
||||
"prettier": "^1.13.4",
|
||||
"ts-node": "^6.1.1",
|
||||
"query-string": "^6.1.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"react": "^16.4.0",
|
||||
"react-dev-utils": "6.0.0-next.3e165448",
|
||||
"react-dom": "^16.4.0",
|
||||
"react-final-form": "^3.6.0",
|
||||
"react-relay": "github:coralproject/patched#react-relay",
|
||||
"recompose": "^0.27.1",
|
||||
"relay-compiler": "github:coralproject/patched#relay-compiler",
|
||||
"relay-compiler-language-typescript": "github:coralproject/patched#relay-compiler-language-typescript",
|
||||
"relay-runtime": "github:coralproject/patched#relay-runtime",
|
||||
"relay-test-utils": "github:coralproject/patched#relay-test-utils",
|
||||
"style-loader": "^0.21.0",
|
||||
"ts-jest": "^22.4.6",
|
||||
"ts-loader": "^4.3.1",
|
||||
"ts-node": "^6.1.0",
|
||||
"tsconfig-paths": "^3.4.0",
|
||||
"tsconfig-paths-webpack-plugin": "^3.1.4",
|
||||
"tslint": "^5.10.0",
|
||||
"tslint-config-prettier": "^1.13.0",
|
||||
"tslint-loader": "^3.6.0",
|
||||
"tslint-plugin-prettier": "^1.3.0",
|
||||
"typescript": "^2.9.1"
|
||||
"tslint-react": "^3.6.0",
|
||||
"typed-css-modules": "^0.3.4",
|
||||
"typescript": "^2.9.1",
|
||||
"uglifyjs-webpack-plugin": "^1.2.5",
|
||||
"webpack": "4.12.0",
|
||||
"webpack-cli": "^3.0.2",
|
||||
"webpack-dev-server": "^3.1.4",
|
||||
"webpack-hot-client": "^4.0.3",
|
||||
"webpack-manifest-plugin": "^2.0.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"use strict";
|
||||
|
||||
// tslint:disable: no-console
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = "production";
|
||||
process.env.NODE_ENV = "production";
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on("unhandledRejection", err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require("../config/env");
|
||||
|
||||
const path = require("path");
|
||||
const chalk = require("chalk");
|
||||
const fs = require("fs-extra");
|
||||
const webpack = require("webpack");
|
||||
const config = require("../config/webpack.config.prod");
|
||||
const paths = require("../config/paths");
|
||||
const checkRequiredFiles = require("react-dev-utils/checkRequiredFiles");
|
||||
const formatWebpackMessages = require("react-dev-utils/formatWebpackMessages");
|
||||
const printHostingInstructions = require("react-dev-utils/printHostingInstructions");
|
||||
const FileSizeReporter = require("react-dev-utils/FileSizeReporter");
|
||||
const printBuildError = require("react-dev-utils/printBuildError");
|
||||
|
||||
const measureFileSizesBeforeBuild =
|
||||
FileSizeReporter.measureFileSizesBeforeBuild;
|
||||
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
|
||||
|
||||
// These sizes are pretty large. We'll warn for bundles exceeding them.
|
||||
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
|
||||
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
|
||||
|
||||
// Treat warnings as errors when we are in CI
|
||||
// TODO: This is currently turned off until we have
|
||||
// an optimized build.
|
||||
const treatWarningsAsErrors = false && process.env.CI &&
|
||||
(typeof process.env.CI !== "string" ||
|
||||
process.env.CI.toLowerCase() !== "false");
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([paths.appStreamHtml, paths.appStreamIndex])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// First, read the current file sizes in build directory.
|
||||
// This lets us display how much they changed later.
|
||||
measureFileSizesBeforeBuild(paths.appDist)
|
||||
.then(previousFileSizes => {
|
||||
// Remove all content but keep the directory so that
|
||||
// if you're in it, you don't end up in Trash
|
||||
fs.emptyDirSync(paths.appDist);
|
||||
// Merge with the public folder
|
||||
if (fs.pathExistsSync(paths.appPublic)) {
|
||||
copyPublicFolder();
|
||||
}
|
||||
// Start the webpack build
|
||||
return build(previousFileSizes);
|
||||
})
|
||||
.then(
|
||||
({ stats, previousFileSizes, warnings }) => {
|
||||
if (warnings.length) {
|
||||
console.log(chalk.yellow("Compiled with warnings.\n"));
|
||||
console.log(warnings.join("\n\n"));
|
||||
console.log(
|
||||
"\nSearch for the " +
|
||||
chalk.underline(chalk.yellow("keywords")) +
|
||||
" to learn more about each warning."
|
||||
);
|
||||
console.log(
|
||||
"To ignore, add " +
|
||||
chalk.cyan("// eslint-disable-next-line") +
|
||||
" to the line before.\n"
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.green("Compiled successfully.\n"));
|
||||
}
|
||||
|
||||
console.log("File sizes after gzip:\n");
|
||||
printFileSizesAfterBuild(
|
||||
stats,
|
||||
previousFileSizes,
|
||||
paths.appDist,
|
||||
WARN_AFTER_BUNDLE_GZIP_SIZE,
|
||||
WARN_AFTER_CHUNK_GZIP_SIZE
|
||||
);
|
||||
console.log();
|
||||
},
|
||||
err => {
|
||||
console.log(chalk.red("Failed to compile.\n"));
|
||||
printBuildError(err);
|
||||
process.exit(1);
|
||||
}
|
||||
);
|
||||
|
||||
// Create the production build and print the deployment instructions.
|
||||
function build(previousFileSizes) {
|
||||
console.log("Creating an optimized production build...");
|
||||
|
||||
let compiler = webpack(config);
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
const messages = formatWebpackMessages(stats.toJson({}, true));
|
||||
if (messages.errors.length) {
|
||||
// Only keep the first error. Others are often indicative
|
||||
// of the same problem, but confuse the reader with noise.
|
||||
if (messages.errors.length > 1) {
|
||||
messages.errors.length = 1;
|
||||
}
|
||||
return reject(new Error(messages.errors.join("\n\n")));
|
||||
}
|
||||
if (
|
||||
treatWarningsAsErrors &&
|
||||
messages.warnings.length
|
||||
) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
"\nTreating warnings as errors because process.env.CI = true.\n" +
|
||||
"Most CI servers set it automatically.\n"
|
||||
)
|
||||
);
|
||||
return reject(new Error(messages.warnings.join("\n\n")));
|
||||
}
|
||||
return resolve({
|
||||
stats,
|
||||
previousFileSizes,
|
||||
warnings: messages.warnings,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyPublicFolder() {
|
||||
fs.copySync(paths.appPublic, paths.appDist, {
|
||||
dereference: true,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
"use strict";
|
||||
|
||||
// tslint:disable: no-console
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = "development";
|
||||
process.env.NODE_ENV = "development";
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on("unhandledRejection", err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require("../config/env");
|
||||
|
||||
const fs = require("fs");
|
||||
const chalk = require("chalk");
|
||||
const webpack = require("webpack");
|
||||
const WebpackDevServer = require("webpack-dev-server");
|
||||
const clearConsole = require("react-dev-utils/clearConsole");
|
||||
const checkRequiredFiles = require("react-dev-utils/checkRequiredFiles");
|
||||
const {
|
||||
choosePort,
|
||||
createCompiler,
|
||||
prepareProxy,
|
||||
prepareUrls,
|
||||
} = require("react-dev-utils/WebpackDevServerUtils");
|
||||
const openBrowser = require("react-dev-utils/openBrowser");
|
||||
const paths = require("../config/paths");
|
||||
const config = require("../config/webpack.config.dev");
|
||||
const createDevServerConfig = require("../config/webpackDevServer.config");
|
||||
|
||||
const isInteractive = process.stdout.isTTY;
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([paths.appStreamHtml, paths.appStreamIndex])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 8080;
|
||||
const HOST = process.env.HOST || "0.0.0.0";
|
||||
|
||||
if (process.env.HOST) {
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Attempting to bind to HOST environment variable: ${chalk.yellow(
|
||||
chalk.bold(process.env.HOST)
|
||||
)}`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
`If this was unintentional, check that you haven't mistakenly set it in your shell.`
|
||||
);
|
||||
console.log(`Learn more here: ${chalk.yellow("http://bit.ly/2mwWSwH")}`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// We attempt to use the default port but if it is busy, we offer the user to
|
||||
// run on a different port. `choosePort()` Promise resolves to the next free port.
|
||||
choosePort(HOST, PORT)
|
||||
.then(port => {
|
||||
if (port == null) {
|
||||
// We have not found a port.
|
||||
return;
|
||||
}
|
||||
const protocol = process.env.HTTPS === "true" ? "https" : "http";
|
||||
const appName = require(paths.appPackageJson).name;
|
||||
const urls = prepareUrls(protocol, HOST, port);
|
||||
// Create a webpack compiler that is configured with custom messages.
|
||||
const compiler = createCompiler(webpack, config, appName, urls);
|
||||
// Load proxy config
|
||||
const proxySetting = require(paths.appPackageJson).proxy;
|
||||
const proxyConfig = prepareProxy(proxySetting, paths.appPublic);
|
||||
// Serve webpack assets generated by the compiler over a web sever.
|
||||
const serverConfig = createDevServerConfig(
|
||||
proxyConfig,
|
||||
urls.lanUrlForConfig
|
||||
);
|
||||
const devServer = new WebpackDevServer(compiler, serverConfig);
|
||||
// Launch WebpackDevServer.
|
||||
devServer.listen(port, HOST, err => {
|
||||
if (err) {
|
||||
return console.log(err);
|
||||
}
|
||||
if (isInteractive) {
|
||||
clearConsole();
|
||||
}
|
||||
console.log(chalk.cyan("Starting the development server...\n"));
|
||||
openBrowser(urls.localUrlForBrowser);
|
||||
});
|
||||
|
||||
["SIGINT", "SIGTERM"].forEach(function(sig) {
|
||||
process.on(sig, function() {
|
||||
devServer.close();
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
if (err && err.message) {
|
||||
console.log(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
"use strict";
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = "test";
|
||||
process.env.NODE_ENV = "test";
|
||||
process.env.PUBLIC_URL = "";
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on("unhandledRejection", err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require("../config/env");
|
||||
|
||||
const paths = require("../config/paths");
|
||||
|
||||
const jest = require("jest");
|
||||
let argv = process.argv.slice(2);
|
||||
|
||||
// Watch unless on CI or in coverage mode
|
||||
if (!process.env.CI && argv.indexOf("--coverage") < 0) {
|
||||
argv.push("--watch");
|
||||
argv.push("--config", paths.appJestConfig);
|
||||
}
|
||||
|
||||
jest.run(argv);
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
["@babel/env", { targets: "last 2 versions, ie 11", modules: false }],
|
||||
"@babel/react"
|
||||
],
|
||||
plugins: [
|
||||
"@babel/syntax-dynamic-import",
|
||||
],
|
||||
env: {
|
||||
"production": {
|
||||
"plugins": [],
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# Framework
|
||||
|
||||
All our client targets (e.g. stream, admin, ...) are based functionality provided by this framework.
|
||||
|
||||
## What should be inside `framework`
|
||||
|
||||
- Code that are specific to a certain target (e.g. stream, admin, ...) must not live here.
|
||||
- Code that are shared by different targets should be put in `framework`
|
||||
@@ -0,0 +1,3 @@
|
||||
# Lib
|
||||
|
||||
This folder contains functionality of integral parts of our technology stack.
|
||||
@@ -0,0 +1,33 @@
|
||||
import { LocalizationProvider } from "fluent-react/compat";
|
||||
import { MessageContext } from "fluent/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
export interface TalkContext {
|
||||
// relayEnvironment for our relay framework.
|
||||
relayEnvironment: Environment;
|
||||
|
||||
// localMessages for our i18n framework.
|
||||
localeMessages: MessageContext[];
|
||||
}
|
||||
|
||||
const { Provider, Consumer } = React.createContext<TalkContext>({} as any);
|
||||
|
||||
/**
|
||||
* Allows consuming the provided context using the React Context API.
|
||||
*/
|
||||
export const TalkContextConsumer = Consumer;
|
||||
|
||||
/**
|
||||
* In addition to just providing the context, TalkContextProvider also
|
||||
* renders the `LocalizationProvider` with the appropite data.
|
||||
*/
|
||||
export const TalkContextProvider: StatelessComponent<{
|
||||
value: TalkContext;
|
||||
}> = ({ value, children }) => (
|
||||
<Provider value={value}>
|
||||
<LocalizationProvider messages={value.localeMessages}>
|
||||
{children}
|
||||
</LocalizationProvider>
|
||||
</Provider>
|
||||
);
|
||||
@@ -0,0 +1,55 @@
|
||||
import { noop } from "lodash";
|
||||
import { Environment, Network, RecordSource, Store } from "relay-runtime";
|
||||
|
||||
import { generateMessages, LocalesData, negotiateLanguages } from "../i18n";
|
||||
import { fetchQuery } from "../network";
|
||||
import { TalkContext } from "./TalkContext";
|
||||
|
||||
interface CreateContextArguments {
|
||||
// Locales that the user accepts, usually `navigator.languages`.
|
||||
userLocales: ReadonlyArray<string>;
|
||||
|
||||
// Locales data that is returned by our `locales-loader`.
|
||||
localesData: LocalesData;
|
||||
|
||||
// Init will be called after the context has been created.
|
||||
init?: ((context: TalkContext) => void | Promise<void>);
|
||||
}
|
||||
|
||||
/**
|
||||
* `createContext` manages the dependencies of our framework
|
||||
* and returns a `TalkContext` that can be passed to the
|
||||
* `TalkContextProvider`.
|
||||
*/
|
||||
export default async function createContext({
|
||||
init = noop,
|
||||
userLocales,
|
||||
localesData,
|
||||
}: CreateContextArguments): Promise<TalkContext> {
|
||||
// Initialize Relay.
|
||||
const relayEnvironment = new Environment({
|
||||
network: Network.create(fetchQuery),
|
||||
store: new Store(new RecordSource()),
|
||||
});
|
||||
|
||||
// Initialize i18n.
|
||||
const locales = negotiateLanguages(userLocales, localesData);
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
// tslint:disable:next-line: no-console
|
||||
console.log(`Negotiated locales ${JSON.stringify(locales)}`);
|
||||
}
|
||||
|
||||
const localeMessages = await generateMessages(locales, localesData);
|
||||
|
||||
// Assemble context.
|
||||
const context = {
|
||||
relayEnvironment,
|
||||
localeMessages,
|
||||
};
|
||||
|
||||
// Run custom initializations.
|
||||
await init(context);
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./TalkContext";
|
||||
export { default as createContext } from "./createContext";
|
||||
export { default as withContext } from "./withContext";
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as React from "react";
|
||||
import { hoistStatics, InferableComponentEnhancer } from "recompose";
|
||||
|
||||
import { TalkContext, TalkContextConsumer } from "./TalkContext";
|
||||
|
||||
/**
|
||||
* withContext is a HOC wrapper around `TalkContextConsumer`.
|
||||
* `propsCallback` must be provided which accepts the `TalkContext`
|
||||
* and returns the props the should be injected.
|
||||
*/
|
||||
function withContext<T>(
|
||||
propsCallback: (context: TalkContext) => T
|
||||
): InferableComponentEnhancer<T> {
|
||||
return hoistStatics<T>(
|
||||
<U extends T>(WrappedComponent: React.ComponentType<U>) => (props: any) => (
|
||||
<TalkContextConsumer>
|
||||
{context => <WrappedComponent {...props} {...propsCallback(context)} />}
|
||||
</TalkContextConsumer>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default withContext;
|
||||
@@ -0,0 +1,79 @@
|
||||
import { mapValues, once } from "lodash";
|
||||
import { ReactNode } from "react";
|
||||
import { VALIDATION_REQUIRED, VALIDATION_TOO_SHORT } from "../messages";
|
||||
|
||||
/**
|
||||
* ValidationError represents all possible string values
|
||||
* that is responded by the server.
|
||||
*/
|
||||
type ValidationError = "TOO_SHORT";
|
||||
|
||||
/**
|
||||
* InvalidArgsMap as responded by the server.
|
||||
*/
|
||||
interface InvalidArgsMap {
|
||||
[key: string]: ValidationError;
|
||||
}
|
||||
|
||||
/**
|
||||
* The localized version of `InvalidArgsMap`.
|
||||
*/
|
||||
interface InvalidArgsMapLocalilzed {
|
||||
[key: string]: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape of the `BadUserInput` extension.
|
||||
*/
|
||||
interface BadUserInputExtension {
|
||||
code: "BAD_USER_INPUT";
|
||||
exception: {
|
||||
invalidArgs: InvalidArgsMap;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map server `ValidationError` to a translation message.
|
||||
*/
|
||||
const validationMap = {
|
||||
TOO_SHORT: VALIDATION_TOO_SHORT,
|
||||
REQUIRED: VALIDATION_REQUIRED,
|
||||
};
|
||||
|
||||
/**
|
||||
* BadUserInputError wraps the `BAD_USER_INPUT` error returned from the
|
||||
* server.
|
||||
*/
|
||||
export default class BadUserInputError extends Error {
|
||||
// Keep origin of original server response.
|
||||
public readonly origin: BadUserInputExtension;
|
||||
|
||||
constructor(error: BadUserInputExtension) {
|
||||
super("BadUserInputError");
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown.
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, BadUserInputError);
|
||||
}
|
||||
|
||||
this.origin = error;
|
||||
}
|
||||
|
||||
get invalidArgs(): InvalidArgsMap {
|
||||
return this.origin.exception.invalidArgs;
|
||||
}
|
||||
|
||||
get invalidArgsLocalized(): InvalidArgsMapLocalilzed {
|
||||
return this.computeInvalidArgsLocalized();
|
||||
}
|
||||
|
||||
// Perform localization and memoize result.
|
||||
private computeInvalidArgsLocalized = once(() => {
|
||||
return mapValues(this.invalidArgs, v => {
|
||||
if (v in validationMap) {
|
||||
return validationMap[v]();
|
||||
}
|
||||
return v;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export interface GraphQLErrorItem {
|
||||
message: string;
|
||||
locations: Array<{
|
||||
line: number;
|
||||
column: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graphql wraps graphql errors at the network layer.
|
||||
*/
|
||||
export default class GraphQLError extends Error {
|
||||
// Original error.
|
||||
public readonly origin: GraphQLErrorItem[];
|
||||
|
||||
constructor(origin: GraphQLErrorItem[]) {
|
||||
super(origin.map(o => o.message).join(" "));
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown.
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, GraphQLError);
|
||||
}
|
||||
this.origin = origin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as NetworkError } from "./networkError";
|
||||
export { default as UnknownServerError } from "./unknownServerError";
|
||||
export { default as BadUserInputError } from "./badUserInputError";
|
||||
export { default as GraphQLError } from "./graphqlError";
|
||||
export * from "./graphqlError";
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* NetworkError wraps errors at the network layer.
|
||||
*/
|
||||
export default class NetworkError extends Error {
|
||||
// Original error.
|
||||
public readonly origin: Error;
|
||||
|
||||
constructor(origin: Error) {
|
||||
// Pass remaining arguments (including vendor specific ones) to parent constructor.
|
||||
super(origin.message);
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown.
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, NetworkError);
|
||||
}
|
||||
this.origin = origin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Shape of the `UnknownError` extension.
|
||||
*/
|
||||
interface UnknownErrorExtension {
|
||||
code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* UnknownServerError wraps any error returned from the
|
||||
* server that we don't know of.
|
||||
*/
|
||||
export default class UnknownServerError extends Error {
|
||||
// Keep origin of original server response.
|
||||
public origin: UnknownErrorExtension;
|
||||
|
||||
constructor(msg: string, error: UnknownErrorExtension) {
|
||||
super(msg);
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown.
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, UnknownServerError);
|
||||
}
|
||||
|
||||
this.origin = error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FormApi } from "final-form";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type ErrorsObject<T> = { [K in keyof T]?: ReactNode };
|
||||
|
||||
/**
|
||||
* A version of FormProps["onSubmit"] with support for Generic Types.
|
||||
*/
|
||||
export type OnSubmit<T> = (
|
||||
values: T,
|
||||
form: FormApi
|
||||
) => ErrorsObject<T> | Promise<ErrorsObject<T> | void> | void;
|
||||
@@ -0,0 +1,103 @@
|
||||
import "fluent-intl-polyfill/compat";
|
||||
import { negotiateLanguages as negotiate } from "fluent-langneg/compat";
|
||||
import { MessageContext } from "fluent/compat";
|
||||
|
||||
export interface BundledLocales {
|
||||
[locale: string]: string;
|
||||
}
|
||||
|
||||
export interface LoadableLocales {
|
||||
[locale: string]: (() => Promise<string>);
|
||||
}
|
||||
|
||||
/**
|
||||
* This type describes the shape of the generated code from our `locales-loader`.
|
||||
* Please check `./src/loaders` and the webpack config for more information.
|
||||
*/
|
||||
export interface LocalesData {
|
||||
readonly defaultLocale: string;
|
||||
readonly fallbackLocale: string;
|
||||
readonly availableLocales: ReadonlyArray<string>;
|
||||
readonly bundled: BundledLocales;
|
||||
readonly loadables: LoadableLocales;
|
||||
}
|
||||
|
||||
/**
|
||||
* negotiateLanguages accepts `userLocales` which usually comes from
|
||||
* `navigator.languages` and the locales `data` as generated by
|
||||
* the `locales-loader` and returns an array of matching languages.
|
||||
*/
|
||||
export function negotiateLanguages(
|
||||
userLocales: ReadonlyArray<string>,
|
||||
data: LocalesData
|
||||
) {
|
||||
// Choose locale that is best for the user.
|
||||
const languages = negotiate(userLocales, data.availableLocales, {
|
||||
defaultLocale: data.defaultLocale,
|
||||
strategy: "lookup",
|
||||
});
|
||||
|
||||
if (data.fallbackLocale && languages[0] !== data.fallbackLocale) {
|
||||
// Use default locale as fallback in case we have
|
||||
// missing keys.
|
||||
languages.push(data.fallbackLocale);
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
// Don't warn in production.
|
||||
let decorateWarnMissing = (cx: MessageContext) => cx;
|
||||
|
||||
// Warn about missing locales if we are not in production.
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
decorateWarnMissing = (() => {
|
||||
const warnings: string[] = [];
|
||||
return (cx: MessageContext) => {
|
||||
const original = cx.hasMessage;
|
||||
cx.hasMessage = (id: string) => {
|
||||
const result = original.apply(cx, [id]);
|
||||
if (!result) {
|
||||
const warn = `${cx.locales} translation for key "${id}" not found`;
|
||||
if (!warnings.includes(warn)) {
|
||||
// tslint:disable:next-line: no-console
|
||||
console.warn(warn);
|
||||
warnings.push(warn);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return cx;
|
||||
};
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a locales array and the `data` from the `locales-loader`,
|
||||
* generateMessages returns an Array of MessageContext as a Promise.
|
||||
* This array is meant to be consumed by `react-fluent`.
|
||||
*
|
||||
* Use it in conjunction with `negotiateLanguages`.
|
||||
*/
|
||||
export async function generateMessages(
|
||||
locales: ReadonlyArray<string>,
|
||||
data: LocalesData
|
||||
): Promise<MessageContext[]> {
|
||||
const promises = [];
|
||||
|
||||
for (const locale of locales) {
|
||||
const cx = new MessageContext(locale);
|
||||
if (locale in data.bundled) {
|
||||
cx.addMessages(data.bundled[locale]);
|
||||
promises.push(decorateWarnMissing(cx));
|
||||
} else if (locale in data.loadables) {
|
||||
const content = await data.loadables[locale]();
|
||||
cx.addMessages(content);
|
||||
promises.push(decorateWarnMissing(cx));
|
||||
} else {
|
||||
throw Error(`Locale ${locale} not available`);
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* This file contains localization messages that are shared by
|
||||
* different parts of the framework.
|
||||
*/
|
||||
|
||||
export const VALIDATION_REQUIRED = () => (
|
||||
<Localized id="framework-validation-required">
|
||||
<span>This field is required.</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export const VALIDATION_TOO_SHORT = () => (
|
||||
<Localized id="framework-validation-too-short">
|
||||
<span>This field is too short.</span>
|
||||
</Localized>
|
||||
);
|
||||
@@ -0,0 +1,61 @@
|
||||
import { FetchFunction } from "relay-runtime";
|
||||
|
||||
import {
|
||||
BadUserInputError,
|
||||
GraphQLError,
|
||||
NetworkError,
|
||||
UnknownServerError,
|
||||
} from "../errors";
|
||||
|
||||
// Normalize errors.
|
||||
function getError(errors: Error[]): Error {
|
||||
if (errors.length > 1) {
|
||||
// Multiple errors are GraphQL errors.
|
||||
// TODO: (cvle) Is this assumption correct?
|
||||
return new GraphQLError(errors as any);
|
||||
}
|
||||
const err = errors[0] as Error;
|
||||
if ((err as any).extensions) {
|
||||
if ((err as any).code === "BAD_USER_INPUT") {
|
||||
return new BadUserInputError((err as any).extensions);
|
||||
}
|
||||
return new UnknownServerError(err.message, (err as any).extensions);
|
||||
}
|
||||
// No extensions == GraphQL error.
|
||||
// TODO: (cvle) harmonize with server.
|
||||
return new GraphQLError(errors as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* fetchQuery is a simple implementation of the `FetchFunction`
|
||||
* required by Relay. It'll return a `NetworkError` on failure.
|
||||
*/
|
||||
const fetchQuery: FetchFunction = async (operation, variables) => {
|
||||
try {
|
||||
const response = await fetch("/api/tenant/graphql", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: operation.text,
|
||||
variables,
|
||||
}),
|
||||
});
|
||||
if (response.status >= 500) {
|
||||
throw new Error(`${response.status} ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
if (data.errors) {
|
||||
throw getError(data.errors);
|
||||
}
|
||||
return data;
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError) {
|
||||
throw new NetworkError(err);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export default fetchQuery;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as fetchQuery } from "./fetchQuery";
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { Component } from "react";
|
||||
import { QueryRenderer } from "react-relay";
|
||||
import { CacheConfig, GraphQLTaggedNode, RerunParam } from "relay-runtime";
|
||||
|
||||
import { TalkContextConsumer } from "../bootstrap/TalkContext";
|
||||
|
||||
// Taken from relay types and added Generic support for Variables and Response
|
||||
export interface QueryRendererProps<V, R> {
|
||||
cacheConfig?: CacheConfig;
|
||||
query?: GraphQLTaggedNode | null;
|
||||
render(readyState: ReadyState<R>): React.ReactElement<any> | undefined | null;
|
||||
variables: V;
|
||||
rerunParamExperimental?: RerunParam;
|
||||
}
|
||||
|
||||
// Taken from relay types and added Generic support for Variables and Response
|
||||
export interface ReadyState<R> {
|
||||
error: Error | undefined | null;
|
||||
props: R | undefined | null;
|
||||
retry?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* TalkQueryRenderer is a wrappper around Relay's `QueryRenderer`.
|
||||
* It supplies the `environment` from the context and has better
|
||||
* generics type support.
|
||||
*/
|
||||
class TalkQueryRenderer<V, R> extends Component<QueryRendererProps<V, R>> {
|
||||
public render() {
|
||||
return (
|
||||
<TalkContextConsumer>
|
||||
{({ relayEnvironment }) => (
|
||||
<QueryRenderer environment={relayEnvironment} {...this.props} />
|
||||
)}
|
||||
</TalkContextConsumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TalkQueryRenderer;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { commitMutation } from "react-relay";
|
||||
import { Environment, MutationConfig } from "relay-runtime";
|
||||
|
||||
import { Omit } from "talk-framework/types";
|
||||
|
||||
/**
|
||||
* Like `MutationConfig` but omits `onCompleted` and `onError`
|
||||
* because we are going to use a Promise API.
|
||||
*/
|
||||
export type MutationPromiseConfig<T, U> = Omit<
|
||||
MutationConfig<T, U>,
|
||||
"onCompleted" | "onError"
|
||||
>;
|
||||
|
||||
// Extract the payload from the response,
|
||||
function getPayload(response: { [key: string]: any }): any {
|
||||
const keys = Object.keys(response);
|
||||
if (keys.length !== 1) {
|
||||
return response;
|
||||
}
|
||||
return response[keys[0]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes response and error from `commitMutationPromise`.
|
||||
* Meaning `response` will directly contain the payload
|
||||
* and errors are wrapped inside of application specific
|
||||
* error instances.
|
||||
*/
|
||||
export async function commitMutationPromiseNormalized<R, V>(
|
||||
environment: Environment,
|
||||
config: MutationPromiseConfig<R, V>
|
||||
): Promise<R> {
|
||||
try {
|
||||
const response = await commitMutationPromise(environment, config);
|
||||
return getPayload(response);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `commitMutation` of the Relay API but returns a Promise.
|
||||
*/
|
||||
export function commitMutationPromise<R, V>(
|
||||
environment: Environment,
|
||||
config: MutationPromiseConfig<R, V>
|
||||
): Promise<R> {
|
||||
return new Promise((resolve, reject) => {
|
||||
commitMutation(environment, {
|
||||
...config,
|
||||
onCompleted: (response, errors) => {
|
||||
if (errors) {
|
||||
// This should not happen, as the network layer
|
||||
// will throw on errors which should result to
|
||||
// `onError` rather than `onCompleted``.
|
||||
reject(errors);
|
||||
return;
|
||||
}
|
||||
resolve(getPayload(response));
|
||||
},
|
||||
onError: error => {
|
||||
reject(error);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
|
||||
|
||||
/**
|
||||
* Creates a Record and retain it forever.
|
||||
* This means that the garbage collector will
|
||||
* not remove the record on the next run.
|
||||
*
|
||||
* See https://github.com/facebook/relay/issues/1656#issuecomment-380519761
|
||||
*/
|
||||
export default function createAndRetain(
|
||||
environment: Environment,
|
||||
source: RecordSourceProxy,
|
||||
id: string,
|
||||
type: string
|
||||
): RecordProxy {
|
||||
const result = source.create(id, type);
|
||||
environment.retain({
|
||||
dataID: id,
|
||||
node: { selections: [] },
|
||||
variables: {},
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import * as React from "react";
|
||||
import { compose, hoistStatics, InferableComponentEnhancer } from "recompose";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { withContext } from "../bootstrap";
|
||||
|
||||
/**
|
||||
* createMutationContainer creates a HOC that
|
||||
* injects a property with the name specified in `propName`
|
||||
* and the signature (input: I) => Promise<R>. Calling
|
||||
* this will call the specified `commit` callback with
|
||||
* the Relay `environment` provided by the context.
|
||||
*/
|
||||
function createMutationContainer<T extends string, I, R>(
|
||||
propName: T,
|
||||
commit: (environment: Environment, input: I) => Promise<R>
|
||||
): InferableComponentEnhancer<{ [P in T]: (input: I) => Promise<R> }> {
|
||||
return compose(
|
||||
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
|
||||
hoistStatics((WrappedComponent: React.ComponentType<any>) => {
|
||||
class CreateMutationContainer extends React.Component<any> {
|
||||
private commit = (input: I) => {
|
||||
return commit(this.props.relayEnvironment, input);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { relayEnvironment: _, ...rest } = this.props;
|
||||
const inject = {
|
||||
[propName]: this.commit,
|
||||
};
|
||||
return <WrappedComponent {...rest} {...inject} />;
|
||||
}
|
||||
}
|
||||
return CreateMutationContainer as React.ComponentType<any>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default createMutationContainer;
|
||||
@@ -0,0 +1,14 @@
|
||||
export { default as withFragmentContainer } from "./withFragmentContainer";
|
||||
export { default as withPaginationContainer } from "./withPaginationContainer";
|
||||
export { default as withRefetchContainer } from "./withRefetchContainer";
|
||||
export { default as withLocalStateContainer } from "./withLocalStateContainer";
|
||||
export * from "./withLocalStateContainer";
|
||||
export { default as QueryRenderer } from "./QueryRenderer";
|
||||
export * from "./QueryRenderer";
|
||||
export { default as createMutationContainer } from "./createMutationContainer";
|
||||
export { default as createAndRetain } from "./createAndRetain";
|
||||
export {
|
||||
commitMutationPromise,
|
||||
commitMutationPromiseNormalized,
|
||||
} from "./commitMutationPromise";
|
||||
export { graphql } from "react-relay";
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createFragmentContainer, GraphQLTaggedNode } from "react-relay";
|
||||
import { InferableComponentEnhancerWithProps } from "recompose";
|
||||
|
||||
/**
|
||||
* withFragmentContainer is a curried version of `createFragmentContainers`
|
||||
* from Relay.
|
||||
*/
|
||||
export default <T>(
|
||||
fragmentSpec: GraphQLTaggedNode
|
||||
): InferableComponentEnhancerWithProps<T, { [P in keyof T]: any }> => (
|
||||
component: React.ComponentType<any>
|
||||
) => createFragmentContainer(component, fragmentSpec) as any;
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as React from "react";
|
||||
import { compose, hoistStatics, InferableComponentEnhancer } from "recompose";
|
||||
import { CSelector, CSnapshot, Environment } from "relay-runtime";
|
||||
|
||||
import { withContext } from "../bootstrap";
|
||||
|
||||
interface Props {
|
||||
relayEnvironment: Environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Root Record of Client-Side Schema Extension must be of this type.
|
||||
*/
|
||||
export const LOCAL_TYPE = "Local";
|
||||
|
||||
/**
|
||||
* The Root Record of Client-Side Schema Extension must have this id.
|
||||
*/
|
||||
export const LOCAL_ID = "client:root.local";
|
||||
|
||||
/**
|
||||
* withLocalStateContainer allows for subscribing to local state
|
||||
* that has been added using Client-Side Schema Extensions.
|
||||
* The `fragmentSpec` must be a `Fragment` on the `LOCAL_TYPE` which
|
||||
* must have the `LOCAL_ID`.
|
||||
*/
|
||||
function withLocalStateContainer<T>(
|
||||
fragmentSpec: any
|
||||
): InferableComponentEnhancer<{ local: T }> {
|
||||
return compose(
|
||||
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
|
||||
hoistStatics((WrappedComponent: React.ComponentType<any>) => {
|
||||
class LocalStateContainer extends React.Component<Props, any> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const fragment = fragmentSpec.data().default;
|
||||
if (fragment.kind !== "Fragment") {
|
||||
throw new Error("Expected fragment");
|
||||
}
|
||||
if (fragment.type !== LOCAL_TYPE) {
|
||||
throw new Error(
|
||||
`Type must be "Local" in "Fragment ${fragment.name}"`
|
||||
);
|
||||
}
|
||||
const selector: CSelector<any> = {
|
||||
dataID: LOCAL_ID,
|
||||
node: { selections: fragment.selections },
|
||||
variables: {},
|
||||
};
|
||||
const snapshot = props.relayEnvironment.lookup(selector);
|
||||
props.relayEnvironment.subscribe(snapshot, this.updateSnapshot);
|
||||
this.state = {
|
||||
data: snapshot.data,
|
||||
};
|
||||
}
|
||||
|
||||
private updateSnapshot = (snapshot: CSnapshot<any>) => {
|
||||
this.setState({ data: snapshot.data });
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { relayEnvironment: _, ...rest } = this.props;
|
||||
return <WrappedComponent {...rest} local={this.state.data} />;
|
||||
}
|
||||
}
|
||||
return LocalStateContainer as React.ComponentType<any>;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default withLocalStateContainer;
|
||||
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
ConnectionConfig,
|
||||
createPaginationContainer,
|
||||
GraphQLTaggedNode,
|
||||
RelayPaginationProp,
|
||||
} from "react-relay";
|
||||
import { InferableComponentEnhancerWithProps } from "recompose";
|
||||
|
||||
/**
|
||||
* withPaginationContainer is a curried version of `createPaginationContainers`
|
||||
* from Relay.
|
||||
*/
|
||||
export default <T, InnerProps>(
|
||||
fragmentSpec: GraphQLTaggedNode,
|
||||
connectionConfig: ConnectionConfig<InnerProps>
|
||||
): InferableComponentEnhancerWithProps<
|
||||
T & { relay: RelayPaginationProp },
|
||||
{ [P in keyof T]: any }
|
||||
> => (component: React.ComponentType<any>) =>
|
||||
createPaginationContainer(component, fragmentSpec, connectionConfig) as any;
|
||||
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
createRefetchContainer,
|
||||
GraphQLTaggedNode,
|
||||
RelayRefetchProp,
|
||||
} from "react-relay";
|
||||
import { InferableComponentEnhancerWithProps } from "recompose";
|
||||
|
||||
/**
|
||||
* withRefetchContainer is a curried version of `createRefetchContainers`
|
||||
* from Relay.
|
||||
*/
|
||||
export default <T>(
|
||||
fragmentSpec: GraphQLTaggedNode,
|
||||
refetchQuery: GraphQLTaggedNode
|
||||
): InferableComponentEnhancerWithProps<
|
||||
T & { relay: RelayRefetchProp },
|
||||
{ [P in keyof T]: any }
|
||||
> => (component: React.ComponentType<any>) =>
|
||||
createRefetchContainer(component, fragmentSpec, refetchQuery) as any;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createValidator } from "./validation";
|
||||
|
||||
describe("createValidator", () => {
|
||||
it("should report error when condition is unmet", () => {
|
||||
const truthy = createValidator(v => !!v, "must be truthy");
|
||||
expect(truthy(false, {})).toBe("must be truthy");
|
||||
});
|
||||
it("should NOT report error when condition is met", () => {
|
||||
const truthy = createValidator(v => !!v, "must be truthy");
|
||||
expect(truthy(true, {})).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ReactNode } from "react";
|
||||
import { VALIDATION_REQUIRED } from "./messages";
|
||||
|
||||
type Validator<T, V> = (v: T, values: V) => ReactNode;
|
||||
|
||||
/**
|
||||
* createValidator returns a Validator that returns given `error` when `condition` is falsey.
|
||||
*/
|
||||
export function createValidator<T = any, V = any>(
|
||||
condition: (v: T, values: V) => boolean,
|
||||
error: ReactNode
|
||||
): Validator<T, V> {
|
||||
return (v, values) => (condition(v, values) ? undefined : error);
|
||||
}
|
||||
|
||||
/**
|
||||
* composeValidators returns a Validator that chains the given validators
|
||||
* and runs them in sequence until one validator fails and returns an error.
|
||||
*/
|
||||
export function composeValidators<T = any, V = any>(
|
||||
...validators: Array<Validator<T, V>>
|
||||
) {
|
||||
return (v: T, values: V) =>
|
||||
validators.reduce(
|
||||
(error, validator) => error || validator(v, values),
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* required is a Validator that checks that the value is truthy.
|
||||
*/
|
||||
export const required = createValidator(v => !!v, VALIDATION_REQUIRED());
|
||||
@@ -0,0 +1,2 @@
|
||||
// TODO: (@cvle) Extract useful common types into its own package.
|
||||
export { Diff, Omit, Overwrite, PropTypesOf } from "talk-ui/types";
|
||||
@@ -0,0 +1,8 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
extends: "../.babelrc.js",
|
||||
plugins: [
|
||||
["babel-plugin-relay", { artifactDirectory: path.resolve(__dirname, "./__generated__") }]
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { Center } from "talk-ui/components";
|
||||
|
||||
import AssetListContainer from "../containers/AssetListContainer";
|
||||
import PostCommentFormContainer from "../containers/PostCommentFormContainer";
|
||||
import StreamContainer from "../containers/StreamContainer";
|
||||
import Logo from "./Logo";
|
||||
|
||||
export interface AppProps {
|
||||
assets?: any | null;
|
||||
asset?: {
|
||||
id: string;
|
||||
isClosed: boolean;
|
||||
comments: any | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
const App: StatelessComponent<AppProps> = props => {
|
||||
if (props.assets) {
|
||||
return <AssetListContainer assets={props.assets} />;
|
||||
}
|
||||
if (props.asset) {
|
||||
return (
|
||||
<Center>
|
||||
<Logo gutterBottom />
|
||||
<StreamContainer comments={props.asset.comments} />
|
||||
<PostCommentFormContainer assetID={props.asset.id} />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
return <div>Asset not found </div>;
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
export interface AssetListProps {
|
||||
assets: ReadonlyArray<{ id: string; title: string | null }>;
|
||||
}
|
||||
|
||||
const AssetList: StatelessComponent<AssetListProps> = props => {
|
||||
return (
|
||||
<div>
|
||||
{props.assets.map(asset => <div key={asset.id}>{asset.title}</div>)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssetList;
|
||||
@@ -0,0 +1,11 @@
|
||||
.root {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.gutterBottom {
|
||||
margin-bottom: calc(2px * $spacing-unit);
|
||||
}
|
||||
|
||||
.author {
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import cn from "classnames";
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { Typography } from "talk-ui/components";
|
||||
|
||||
import * as styles from "./Comment.css";
|
||||
|
||||
export interface CommentProps {
|
||||
className?: string;
|
||||
author: {
|
||||
username: string;
|
||||
} | null;
|
||||
body: string | null;
|
||||
gutterBottom?: boolean;
|
||||
}
|
||||
|
||||
const Comment: StatelessComponent<CommentProps> = props => {
|
||||
const rootClassName = cn(styles.root, props.className, {
|
||||
[styles.gutterBottom]: props.gutterBottom,
|
||||
});
|
||||
return (
|
||||
<div className={rootClassName}>
|
||||
<Typography className={styles.author} gutterBottom>
|
||||
{props.author && props.author.username}
|
||||
</Typography>
|
||||
<Typography>{props.body}</Typography>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Comment;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { Typography } from "talk-ui/components";
|
||||
|
||||
export interface LogoProps {
|
||||
className?: string;
|
||||
gutterBottom?: boolean;
|
||||
}
|
||||
|
||||
const Logo: StatelessComponent<LogoProps> = props => {
|
||||
return (
|
||||
<Localized id="stream-logo">
|
||||
<Typography variant="heading1" gutterBottom={props.gutterBottom}>
|
||||
Talk NEO
|
||||
</Typography>
|
||||
</Localized>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logo;
|
||||
@@ -0,0 +1,12 @@
|
||||
.textarea {
|
||||
composes: body1 from "talk-ui/shared/typography.css";
|
||||
|
||||
display: block;
|
||||
height: 100px;
|
||||
width: 400px;
|
||||
margin-bottom: calc(2px * $spacing-unit);
|
||||
}
|
||||
|
||||
.postButton {
|
||||
float: right;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
|
||||
import { OnSubmit } from "talk-framework/lib/form";
|
||||
import { required } from "talk-framework/lib/validation";
|
||||
import { Button, Typography } from "talk-ui/components";
|
||||
|
||||
import * as styles from "./PostCommentForm.css";
|
||||
|
||||
interface FormProps {
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface PostCommentFormProps {
|
||||
onSubmit: OnSubmit<FormProps>;
|
||||
}
|
||||
|
||||
const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
|
||||
<Form onSubmit={props.onSubmit}>
|
||||
{({ handleSubmit, submitting }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<Field name="body" validate={required}>
|
||||
{({ input, meta }) => (
|
||||
<div>
|
||||
<textarea
|
||||
className={styles.textarea}
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
/>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<Typography align="right" color="error" gutterBottom>
|
||||
{meta.error || meta.submitError}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<Localized id="postCommentForm-submit">
|
||||
<Button className={styles.postButton} disabled={submitting} primary>
|
||||
Post
|
||||
</Button>
|
||||
</Localized>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
|
||||
export default PostCommentForm;
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import CommentContainer from "../containers/CommentContainer";
|
||||
|
||||
export interface StreamProps {
|
||||
comments: ReadonlyArray<{ id: string }>;
|
||||
}
|
||||
|
||||
const Stream: StatelessComponent<StreamProps> = props => {
|
||||
return (
|
||||
<div>
|
||||
{props.comments.map(comment => (
|
||||
<CommentContainer key={comment.id} data={comment} gutterBottom />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Stream;
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { AppContainer as Data } from "talk-stream/__generated__/AppContainer.graphql";
|
||||
|
||||
import App from "../components/App";
|
||||
|
||||
interface InnerProps {
|
||||
data: Data;
|
||||
}
|
||||
|
||||
const AppContainer: StatelessComponent<InnerProps> = props => {
|
||||
return <App {...props.data} />;
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<{ data: Data }>(
|
||||
graphql`
|
||||
fragment AppContainer on Query
|
||||
@argumentDefinitions(
|
||||
assetID: { type: "ID!" }
|
||||
showAssetList: { type: "Boolean!" }
|
||||
) {
|
||||
assets @include(if: $showAssetList) {
|
||||
...AssetListContainer_assets
|
||||
}
|
||||
asset(id: $assetID) @skip(if: $showAssetList) {
|
||||
id
|
||||
isClosed
|
||||
comments {
|
||||
...StreamContainer_comments
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
)(AppContainer);
|
||||
|
||||
export type AppContainerProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { AssetListContainer_assets as Data } from "talk-stream/__generated__/AssetListContainer_assets.graphql";
|
||||
|
||||
import AssetList from "../components/AssetList";
|
||||
|
||||
interface InnerProps {
|
||||
assets: Data;
|
||||
}
|
||||
|
||||
const AssetListContainer: StatelessComponent<InnerProps> = props => {
|
||||
const assets = props.assets.edges.map(edge => edge.node);
|
||||
return <AssetList assets={assets} />;
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<{ assets: Data }>(
|
||||
graphql`
|
||||
fragment AssetListContainer_assets on AssetsConnection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
)(AssetListContainer);
|
||||
|
||||
export type AssetListContainerProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,29 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import withFragmentContainer from "talk-framework/lib/relay/withFragmentContainer";
|
||||
import { Omit, PropTypesOf } from "talk-framework/types";
|
||||
import { CommentContainer as Data } from "talk-stream/__generated__/CommentContainer.graphql";
|
||||
|
||||
import Comment, { CommentProps } from "../components/Comment";
|
||||
|
||||
type InnerProps = { data: Data } & Omit<CommentProps, keyof Data>;
|
||||
|
||||
const CommentContainer: StatelessComponent<InnerProps> = props => {
|
||||
const { data, ...rest } = props;
|
||||
return <Comment {...rest} {...props.data} />;
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<{ data: Data }>(
|
||||
graphql`
|
||||
fragment CommentContainer on Comment {
|
||||
author {
|
||||
username
|
||||
}
|
||||
body
|
||||
}
|
||||
`
|
||||
)(CommentContainer);
|
||||
|
||||
export type CommentContainerProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { BadUserInputError } from "talk-framework/lib/errors";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import PostCommentForm, {
|
||||
PostCommentFormProps,
|
||||
} from "../components/PostCommentForm";
|
||||
import { CreateCommentMutation, withCreateCommentMutation } from "../mutations";
|
||||
|
||||
interface InnerProps {
|
||||
createComment: CreateCommentMutation;
|
||||
assetID: string;
|
||||
}
|
||||
|
||||
class PostCommentFormContainer extends Component<InnerProps> {
|
||||
private onSubmit: PostCommentFormProps["onSubmit"] = async (input, form) => {
|
||||
try {
|
||||
await this.props.createComment({
|
||||
assetID: this.props.assetID,
|
||||
...input,
|
||||
});
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
if (error instanceof BadUserInputError) {
|
||||
return error.invalidArgsLocalized;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
public render() {
|
||||
return <PostCommentForm onSubmit={this.onSubmit} />;
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withCreateCommentMutation(PostCommentFormContainer);
|
||||
export type PostCommentFormContainerProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { StreamContainer_comments as Data } from "talk-stream/__generated__/StreamContainer_comments.graphql";
|
||||
|
||||
import Stream from "../components/Stream";
|
||||
|
||||
interface InnerProps {
|
||||
comments: Data;
|
||||
}
|
||||
|
||||
const StreamContainer: StatelessComponent<InnerProps> = props => {
|
||||
const comments = props.comments.edges.map(edge => edge.node);
|
||||
return <Stream comments={comments} />;
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<{ comments: Data }>(
|
||||
graphql`
|
||||
fragment StreamContainer_comments on CommentsConnection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...CommentContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
)(StreamContainer);
|
||||
|
||||
export type StreamContainerProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html prefix="og: http://ogp.me/ns#">
|
||||
|
||||
<head>
|
||||
<title>Relay Experiments</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app" aria-role="application" onclick="void(0)"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
TalkContext,
|
||||
TalkContextProvider,
|
||||
} from "talk-framework/lib/bootstrap";
|
||||
|
||||
import { initLocalState } from "./local";
|
||||
import localesData from "./locales";
|
||||
import AppQuery from "./queries/AppQuery";
|
||||
|
||||
// This is called when the context is first initialized.
|
||||
async function init({ relayEnvironment }: TalkContext) {
|
||||
await initLocalState(relayEnvironment);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Bootstrap our context.
|
||||
const context = await createContext({
|
||||
init,
|
||||
localesData,
|
||||
userLocales: navigator.languages,
|
||||
});
|
||||
|
||||
const Index: StatelessComponent = () => (
|
||||
<TalkContextProvider value={context}>
|
||||
<AppQuery />
|
||||
</TalkContextProvider>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Index />, document.getElementById("app"));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* This files contains the various types and ids of the local schema.
|
||||
*/
|
||||
|
||||
export const NETWORK_TYPE = "Network";
|
||||
export const NETWORK_ID = "client:root.local.network";
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as initLocalState } from "./initLocalState";
|
||||
export * from "./constants";
|
||||
@@ -0,0 +1,39 @@
|
||||
import qs from "query-string";
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
createAndRetain,
|
||||
LOCAL_ID,
|
||||
LOCAL_TYPE,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import { NETWORK_ID, NETWORK_TYPE } from "./constants";
|
||||
|
||||
/**
|
||||
* Initializes the local state, before we start the App.
|
||||
*/
|
||||
export default async function initLocalState(environment: Environment) {
|
||||
commitLocalUpdate(environment, s => {
|
||||
const root = s.getRoot();
|
||||
|
||||
// Create the Local Record which is the Root for the client states.
|
||||
const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE);
|
||||
|
||||
// Parse query params
|
||||
const query = qs.parse(location.search);
|
||||
if (query.assetID) {
|
||||
localRecord.setValue(query.assetID, "assetID");
|
||||
}
|
||||
|
||||
// Create network Record
|
||||
const networkRecord = createAndRetain(
|
||||
environment,
|
||||
s,
|
||||
NETWORK_ID,
|
||||
NETWORK_TYPE
|
||||
);
|
||||
networkRecord.setValue(false, "isOffline");
|
||||
localRecord.setLinkedRecord(networkRecord, "network");
|
||||
root.setLinkedRecord(localRecord, "local");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Extend graph with local types
|
||||
type Network {
|
||||
isOffline: Boolean!
|
||||
}
|
||||
|
||||
type Local {
|
||||
network: Network!
|
||||
assetID: String
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
local: Local!
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* The actual content of this file is being generated by our `locales-loader`.
|
||||
* Please check `./src/loaders` and the webpack config for more information.
|
||||
*
|
||||
* This file only represents the types that gets exported.
|
||||
*/
|
||||
|
||||
import { LocalesData } from "talk-framework/lib/i18n";
|
||||
export default {} as LocalesData;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
import { Omit } from "talk-framework/types";
|
||||
import {
|
||||
CreateCommentMutationResponse,
|
||||
CreateCommentMutationVariables,
|
||||
} from "talk-stream/__generated__/CreateCommentMutation.graphql";
|
||||
|
||||
export type CreateCommentInput = Omit<
|
||||
CreateCommentMutationVariables["input"],
|
||||
"clientMutationId"
|
||||
>;
|
||||
|
||||
const mutation = graphql`
|
||||
mutation CreateCommentMutation($input: CreateCommentInput!) {
|
||||
createComment(input: $input) {
|
||||
comment {
|
||||
id
|
||||
author {
|
||||
username
|
||||
}
|
||||
body
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: CreateCommentInput) {
|
||||
return commitMutationPromiseNormalized<
|
||||
CreateCommentMutationResponse["createComment"],
|
||||
CreateCommentMutationVariables
|
||||
>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
const payload = store.getRootField("createComment");
|
||||
if (payload) {
|
||||
const newRecord = payload.getLinkedRecord("comment")!;
|
||||
const root = store.getRoot();
|
||||
const records = root.getLinkedRecords("comments");
|
||||
if (!records) {
|
||||
throw new Error("Unexpected cache state");
|
||||
}
|
||||
|
||||
root.setLinkedRecords([...records, newRecord], "comments");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const withCreateCommentMutation = createMutationContainer(
|
||||
"createComment",
|
||||
commit
|
||||
);
|
||||
|
||||
export type CreateCommentMutation = (
|
||||
input: CreateCommentInput
|
||||
) => Promise<CreateCommentMutationResponse["createComment"]>;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
|
||||
import { NETWORK_ID } from "../local";
|
||||
|
||||
export interface SetNetworkStatusInput {
|
||||
isOffline: boolean;
|
||||
}
|
||||
|
||||
export type SetNetworkStatusMutation = (input: SetNetworkStatusInput) => void;
|
||||
|
||||
async function commit(environment: Environment, input: SetNetworkStatusInput) {
|
||||
return commitLocalUpdate(environment, store => {
|
||||
const record = store.get(NETWORK_ID)!;
|
||||
record.setValue(input.isOffline, "isOffline");
|
||||
});
|
||||
}
|
||||
|
||||
export const withSetNetworkStatusMutation = createMutationContainer(
|
||||
"setNetworkStatus",
|
||||
commit
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./CreateCommentMutation";
|
||||
export * from "./SetNetworkStatusMutation";
|
||||
@@ -0,0 +1,60 @@
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import {
|
||||
graphql,
|
||||
QueryRenderer,
|
||||
ReadyState,
|
||||
withLocalStateContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
import {
|
||||
AppQueryResponse,
|
||||
AppQueryVariables,
|
||||
} from "talk-stream/__generated__/AppQuery.graphql";
|
||||
import { AppQueryLocal as Local } from "talk-stream/__generated__/AppQueryLocal.graphql";
|
||||
|
||||
import AppContainer from "../containers/AppContainer";
|
||||
|
||||
const render = ({ error, props }: ReadyState<AppQueryResponse>) => {
|
||||
if (error) {
|
||||
return <div>{error.message}</div>;
|
||||
}
|
||||
if (props) {
|
||||
return <AppContainer data={props} />;
|
||||
}
|
||||
return <div>Loading</div>;
|
||||
};
|
||||
|
||||
interface InnerProps {
|
||||
local: Local;
|
||||
}
|
||||
|
||||
const AppQuery: StatelessComponent<InnerProps> = props => {
|
||||
return (
|
||||
<QueryRenderer<AppQueryVariables, AppQueryResponse>
|
||||
query={graphql`
|
||||
query AppQuery($showAssetList: Boolean!, $assetID: ID!) {
|
||||
...AppContainer
|
||||
@arguments(showAssetList: $showAssetList, assetID: $assetID)
|
||||
}
|
||||
`}
|
||||
variables={{
|
||||
// We cast `null` to any due to restrictions of the current graphql syntax.
|
||||
assetID: props.local.assetID || (null as any),
|
||||
// TODO: This is set to false, as server does not support querying assets yet.
|
||||
showAssetList: !props.local.assetID && false,
|
||||
}}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withLocalStateContainer<Local>(
|
||||
graphql`
|
||||
fragment AppQueryLocal on Local {
|
||||
assetID
|
||||
}
|
||||
`
|
||||
)(AppQuery);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"module": "esnext",
|
||||
"jsx": "preserve",
|
||||
"noEmit": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": ["dom", "es7", "scripthost", "es2015", "esnext.asynciterable"],
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"talk-admin/*": [
|
||||
"./admin/*"
|
||||
],
|
||||
"talk-stream/*": [
|
||||
"./stream/*"
|
||||
],
|
||||
"talk-framework/*": [
|
||||
"./framework/*"
|
||||
],
|
||||
"talk-ui/*": [
|
||||
"./ui/*"
|
||||
],
|
||||
"talk-common/*": [
|
||||
"../common/*"
|
||||
],
|
||||
"talk-locales/*": [
|
||||
"../../locales/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx",
|
||||
"./**/*.d.ts",
|
||||
"../../types/**/*.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../tslint.json",
|
||||
"tslint-react"
|
||||
],
|
||||
"rules": {
|
||||
"jsx-no-multiline-js": false,
|
||||
"jsx-boolean-value": [true, "never"]
|
||||
},
|
||||
"jsRules": {
|
||||
"jsx-no-multiline-js": false,
|
||||
"jsx-boolean-value": [true, "never"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# UI Kit
|
||||
|
||||
The UI Kit provides the building blocks to create a Rich Graphical User Interface.
|
||||
|
||||
## What should be inside the UI Kit
|
||||
|
||||
- The UI Kit focuses on providing presentational building blocks that is not specific to any domain or product.
|
||||
- The UI Kit should be completely independent from the rest of the codebase.
|
||||
@@ -0,0 +1,10 @@
|
||||
.root {
|
||||
composes: buttonReset from "talk-ui/shared/buttonReset.css";
|
||||
}
|
||||
|
||||
.keyboardFocus {
|
||||
outline-width: 3px;
|
||||
outline-color: Highlight;
|
||||
outline-color: -webkit-focus-ring-color;
|
||||
outline-style: auto;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: BaseButton
|
||||
menu: UI Kit
|
||||
---
|
||||
|
||||
import { Playground, PropsTable } from 'docz'
|
||||
import BaseButton from './BaseButton'
|
||||
|
||||
# BaseButton
|
||||
|
||||
`BaseButton` strips away browser specific styling and unifies the look of the `button` and the `a` tag.
|
||||
It detects a focus that came from the keyboard rather than mouse or touch and styles the button using
|
||||
the `className` provided in the `classes.keyboardFocus` property.
|
||||
|
||||
When used as a `button` tag the default `type` is set to `button` instead of the standard `submit` in order
|
||||
to avoid unintended form submissions.
|
||||
|
||||
## Basic usage
|
||||
<Playground>
|
||||
<BaseButton>Push Me</BaseButton>
|
||||
</Playground>
|
||||
|
||||
Instead of an `button` tag, we can render an `a` tag instead:
|
||||
|
||||
<Playground>
|
||||
<BaseButton anchor>Push Me</BaseButton>
|
||||
</Playground>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import cn from "classnames";
|
||||
import React from "react";
|
||||
import { ButtonHTMLAttributes, StatelessComponent } from "react";
|
||||
|
||||
import { withKeyboardFocus, withStyles } from "talk-ui/hocs";
|
||||
import { PropTypesOf } from "talk-ui/types";
|
||||
|
||||
import * as styles from "./BaseButton.css";
|
||||
|
||||
interface InnerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
/** If set renders an anchor tag instead */
|
||||
anchor?: boolean;
|
||||
|
||||
/**
|
||||
* This prop can be used to add custom classnames.
|
||||
* It is handled by the `withStyles `HOC.
|
||||
*/
|
||||
classes: typeof styles;
|
||||
|
||||
/** This is passed by the `withKeyboardFocus` HOC */
|
||||
keyboardFocus: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A button whose styling is stripped off to a minimum and supports
|
||||
* keyboard focus. It is the base for our other buttons.
|
||||
*/
|
||||
const BaseButton: StatelessComponent<InnerProps> = ({
|
||||
anchor,
|
||||
className,
|
||||
classes,
|
||||
keyboardFocus,
|
||||
type: typeProp,
|
||||
...rest
|
||||
}) => {
|
||||
let Element = "button";
|
||||
if (anchor) {
|
||||
Element = "a";
|
||||
}
|
||||
|
||||
let type = typeProp;
|
||||
if (anchor && type) {
|
||||
// tslint:disable:next-line: no-console
|
||||
console.warn(
|
||||
"BaseButton used as anchor does not support the `type` property"
|
||||
);
|
||||
} else if (type === undefined) {
|
||||
// Default to button
|
||||
type = "button";
|
||||
}
|
||||
|
||||
const rootClassName = cn(classes.root, className, {
|
||||
[classes.keyboardFocus]: keyboardFocus,
|
||||
});
|
||||
|
||||
return <Element {...rest} className={rootClassName} />;
|
||||
};
|
||||
|
||||
const enhanced = withStyles(styles)(withKeyboardFocus(BaseButton));
|
||||
export type BaseButtonProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./BaseButton";
|
||||
export { default } from "./BaseButton";
|
||||
@@ -0,0 +1,78 @@
|
||||
.root {
|
||||
composes: button from "talk-ui/shared/typography.css";
|
||||
padding: 5px 20px;
|
||||
border-radius: $round-corners;
|
||||
background-color: transparent;
|
||||
/* TODO: hover styles for the default button */
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background-color: $palette-primary-main;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background-color: $palette-primary-light;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $palette-primary-lighter;
|
||||
}
|
||||
|
||||
&.invert {
|
||||
background-color: transparent;
|
||||
border: 1px solid $palette-primary-main;
|
||||
color: $palette-primary-main;
|
||||
|
||||
&:hover {
|
||||
border-color: $palette-primary-light;
|
||||
color: $palette-primary-light;
|
||||
}
|
||||
|
||||
&:active {
|
||||
border-color: $palette-primary-lighter;
|
||||
color: $palette-primary-lighter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background-color: $palette-secondary-main;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background-color: $palette-secondary-light;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $palette-secondary-lighter;
|
||||
}
|
||||
|
||||
&.invert {
|
||||
background-color: transparent;
|
||||
border: 1px solid $palette-secondary-main;
|
||||
color: $palette-secondary-main;
|
||||
|
||||
&:hover {
|
||||
border-color: $palette-secondary-light;
|
||||
color: $palette-secondary-light;
|
||||
}
|
||||
|
||||
&:active {
|
||||
border-color: $palette-secondary-lighter;
|
||||
color: $palette-secondary-lighter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This seems to be the best way to target modern touch device browsers.
|
||||
*/
|
||||
@media (-moz-touch-enabled: 1), (pointer:coarse) {
|
||||
/* TODO: Remove hover styles */
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Button
|
||||
menu: UI Kit
|
||||
---
|
||||
|
||||
import { Playground } from 'docz'
|
||||
import Button from './Button'
|
||||
|
||||
# Button
|
||||
|
||||
## Basic usage
|
||||
<Playground>
|
||||
<Button style={{marginRight: "10px"}}>Push Me</Button>
|
||||
<Button style={{marginRight: "10px"}} anchor>I'm an Anchor Tag</Button>
|
||||
<Button style={{marginRight: "10px"}} primary>Primary</Button>
|
||||
<Button style={{marginRight: "10px"}} secondary>Secondary</Button>
|
||||
<Button style={{marginTop: "10px"}} primary fullWidth>Full Width</Button>
|
||||
<Button style={{marginTop: "10px"}} primary invert fullWidth>Full Width Invert</Button>
|
||||
</Playground>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import cn from "classnames";
|
||||
import { pick } from "lodash";
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
|
||||
import { withStyles } from "talk-ui/hocs";
|
||||
import { PropTypesOf } from "talk-ui/types";
|
||||
|
||||
import BaseButton, { BaseButtonProps } from "../BaseButton";
|
||||
import * as styles from "./Button.css";
|
||||
|
||||
// This should extend from BaseButton instead but we can't because of this bug
|
||||
// TODO: add bug link.
|
||||
interface InnerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
/**
|
||||
* This prop can be used to add custom classnames.
|
||||
* It is handled by the `withStyles `HOC.
|
||||
*/
|
||||
classes: typeof styles & Partial<BaseButtonProps["classes"]>;
|
||||
|
||||
/** If set renders a full width button */
|
||||
fullWidth?: boolean;
|
||||
|
||||
/** If set renders a button with inverted borders */
|
||||
invert?: boolean;
|
||||
|
||||
/** If set renders a button with primary colors */
|
||||
primary?: boolean;
|
||||
|
||||
/** If set renders a button with secondary colors */
|
||||
secondary?: boolean;
|
||||
}
|
||||
|
||||
class Button extends React.Component<InnerProps> {
|
||||
public render() {
|
||||
const {
|
||||
classes,
|
||||
className,
|
||||
fullWidth,
|
||||
invert,
|
||||
primary,
|
||||
secondary,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
const rootClassName = cn(classes.root, className, {
|
||||
[classes.invert]: invert,
|
||||
[classes.fullWidth]: fullWidth,
|
||||
[classes.primary]: primary,
|
||||
[classes.secondary]: secondary,
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseButton
|
||||
className={rootClassName}
|
||||
classes={pick(classes, "keyboardFocus")}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withStyles(styles)(Button);
|
||||
export type ButtonProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./Button";
|
||||
export { default } from "./Button";
|
||||
@@ -0,0 +1,6 @@
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import cn from "classnames";
|
||||
import * as React from "react";
|
||||
import { ReactNode, StatelessComponent } from "react";
|
||||
|
||||
import { withStyles } from "talk-ui/hocs";
|
||||
import { PropTypesOf } from "talk-ui/types";
|
||||
|
||||
import * as styles from "./Center.css";
|
||||
|
||||
interface InnerProps {
|
||||
/**
|
||||
* This prop can be used to add custom classnames.
|
||||
* It is handled by the `withStyles `HOC.
|
||||
*/
|
||||
classes: Partial<typeof styles>;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Center: StatelessComponent<InnerProps> = props => {
|
||||
return (
|
||||
<div className={cn(props.className, props.classes.root)}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withStyles(styles)(Center);
|
||||
export type CenterProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./Center";
|
||||
export { default } from "./Center";
|
||||
@@ -0,0 +1,94 @@
|
||||
.root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.heading1 {
|
||||
composes: heading1 from "talk-ui/shared/typography.css";
|
||||
}
|
||||
|
||||
.heading2 {
|
||||
composes: heading2 from "talk-ui/shared/typography.css";
|
||||
}
|
||||
|
||||
.heading3 {
|
||||
composes: heading3 from "talk-ui/shared/typography.css";
|
||||
}
|
||||
|
||||
.heading4 {
|
||||
composes: heading4 from "talk-ui/shared/typography.css";
|
||||
}
|
||||
|
||||
.subtitle1 {
|
||||
composes: subtitle1 from "talk-ui/shared/typography.css";
|
||||
}
|
||||
|
||||
.subtitle2 {
|
||||
composes: subtitle2 from "talk-ui/shared/typography.css";
|
||||
}
|
||||
|
||||
.body1 {
|
||||
composes: body1 from "talk-ui/shared/typography.css";
|
||||
}
|
||||
|
||||
.body2 {
|
||||
composes: body2 from "talk-ui/shared/typography.css";
|
||||
}
|
||||
|
||||
.button {
|
||||
composes: button from "talk-ui/shared/typography.css";
|
||||
}
|
||||
|
||||
.overline {
|
||||
composes: overline from "talk-ui/shared/typography.css";
|
||||
}
|
||||
|
||||
.alignLeft {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.alignCenter {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alignRight {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.alignJustify {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.noWrap {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.gutterBottom {
|
||||
margin-bottom: 0.35em;
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
margin-bottom: calc(2px * $spacing-unit);
|
||||
}
|
||||
|
||||
.colorInherit {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.colorPrimary {
|
||||
color: $palette-primary-main;
|
||||
}
|
||||
|
||||
.colorSecondary {
|
||||
color: $palette-secondary-main;
|
||||
}
|
||||
|
||||
.colorTextSecondary {
|
||||
color: $palette-text-secondary;
|
||||
}
|
||||
|
||||
.colorError {
|
||||
color: $palette-error-main;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import cn from "classnames";
|
||||
import React from "react";
|
||||
import { HTMLAttributes, ReactNode, StatelessComponent } from "react";
|
||||
|
||||
import { withStyles } from "talk-ui/hocs";
|
||||
import { PropTypesOf } from "talk-ui/types";
|
||||
|
||||
import * as styles from "./Typography.css";
|
||||
|
||||
type Variant =
|
||||
| "heading1"
|
||||
| "heading2"
|
||||
| "heading3"
|
||||
| "heading4"
|
||||
| "subtitle1"
|
||||
| "subtitle2"
|
||||
| "body1"
|
||||
| "body2"
|
||||
| "button";
|
||||
|
||||
// Based on Typography Component of Material UI.
|
||||
// https://github.com/mui-org/material-ui/blob/303199d39b42a321d28347d8440d69166f872f27/packages/material-ui/src/Typography/Typography.js
|
||||
|
||||
interface InnerProps extends HTMLAttributes<any> {
|
||||
/**
|
||||
* Set the text-align on the component.
|
||||
*/
|
||||
align?: "inherit" | "left" | "center" | "right" | "justify";
|
||||
/**
|
||||
* The content of the component.
|
||||
*/
|
||||
children: ReactNode;
|
||||
/**
|
||||
* Override or extend the styles applied to the component.
|
||||
*/
|
||||
classes: typeof styles;
|
||||
/**
|
||||
* Convenient prop to override the root styling.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* The color of the component. It supports those theme colors that make sense for this component.
|
||||
*/
|
||||
color?:
|
||||
| "inherit"
|
||||
| "primary"
|
||||
| "textSecondary"
|
||||
| "secondary"
|
||||
| "error"
|
||||
| "default";
|
||||
/**
|
||||
* The component used for the root node.
|
||||
* Either a string to use a DOM element or a component.
|
||||
* By default, it maps the variant to a good default headline component.
|
||||
*/
|
||||
component?: React.ComponentType<any> | string;
|
||||
/**
|
||||
* If `true`, the text will have a bottom margin.
|
||||
*/
|
||||
gutterBottom?: boolean;
|
||||
/**
|
||||
* We are empirically mapping the variant property to a range of different DOM element types.
|
||||
* For instance, h1 to h6. If you wish to change that mapping, you can provide your own.
|
||||
* Alternatively, you can use the `component` property.
|
||||
*/
|
||||
headlineMapping?: { [P in Variant]?: React.ComponentType<any> | string };
|
||||
/**
|
||||
* If `true`, the text will not wrap, but instead will truncate with an ellipsis.
|
||||
*/
|
||||
noWrap?: boolean;
|
||||
/**
|
||||
* If `true`, the text will have a bottom margin.
|
||||
*/
|
||||
paragraph?: boolean;
|
||||
/**
|
||||
* Applies the theme typography styles.
|
||||
*/
|
||||
variant?: Variant;
|
||||
}
|
||||
|
||||
const Typography: StatelessComponent<InnerProps> = props => {
|
||||
const {
|
||||
align,
|
||||
classes,
|
||||
className,
|
||||
color,
|
||||
component,
|
||||
gutterBottom,
|
||||
headlineMapping,
|
||||
noWrap,
|
||||
paragraph,
|
||||
variant,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const rootClassName = cn(
|
||||
classes.root,
|
||||
classes[variant!],
|
||||
{
|
||||
[classes.colorPrimary]: color === "primary",
|
||||
[classes.colorSecondary]: color === "secondary",
|
||||
[classes.noWrap]: noWrap,
|
||||
[classes.gutterBottom]: gutterBottom,
|
||||
[classes.paragraph]: paragraph,
|
||||
[classes.alignLeft]: align === "left",
|
||||
[classes.alignCenter]: align === "center",
|
||||
[classes.alignRight]: align === "right",
|
||||
[classes.alignJustify]: align === "justify",
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
const Component =
|
||||
component || (paragraph ? "p" : headlineMapping![variant!]) || "span";
|
||||
|
||||
return <Component className={rootClassName} {...rest} />;
|
||||
};
|
||||
|
||||
Typography.defaultProps = {
|
||||
align: "inherit",
|
||||
color: "default",
|
||||
gutterBottom: false,
|
||||
headlineMapping: {
|
||||
heading1: "h1",
|
||||
heading2: "h1",
|
||||
heading3: "h1",
|
||||
heading4: "h1",
|
||||
subtitle1: "h2",
|
||||
subtitle2: "h3",
|
||||
body1: "p",
|
||||
body2: "aside",
|
||||
},
|
||||
noWrap: false,
|
||||
paragraph: false,
|
||||
variant: "body1",
|
||||
};
|
||||
|
||||
const enhanced = withStyles(styles)(Typography);
|
||||
export type CenterProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./Typography";
|
||||
export { default } from "./Typography";
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as BaseButton } from "./BaseButton";
|
||||
export { default as Button } from "./Button";
|
||||
export { default as Center } from "./Center";
|
||||
export { default as Typography } from "./Typography";
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as withStyles } from "./withStyles";
|
||||
export { default as withKeyboardFocus } from "./withKeyboardFocus";
|
||||
@@ -0,0 +1,64 @@
|
||||
import * as React from "react";
|
||||
import { FocusEvent, MouseEvent } from "react";
|
||||
import { hoistStatics } from "recompose";
|
||||
|
||||
interface InjectedProps {
|
||||
onFocus?: React.EventHandler<FocusEvent<any>>;
|
||||
onBlur?: React.EventHandler<FocusEvent<any>>;
|
||||
onMouseDown?: React.EventHandler<MouseEvent<any>>;
|
||||
keyboardFocus?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* withKeyboardFocus provides a property `keyboardFocus: boolean`
|
||||
* to indicate a focus on the element, that wasn't triggered by mouse
|
||||
* or touch.
|
||||
*/
|
||||
export default hoistStatics<InjectedProps>(
|
||||
<T extends InjectedProps>(WrappedComponent: React.ComponentType<T>) => {
|
||||
class WithKeyboardFocus extends React.Component<any> {
|
||||
public state = {
|
||||
keyboardFocus: false,
|
||||
lastMouseDownTime: 0,
|
||||
};
|
||||
|
||||
private handleFocus: React.EventHandler<FocusEvent<any>> = event => {
|
||||
if (this.props.onFocus) {
|
||||
this.props.onFocus(event);
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
if (now - this.state.lastMouseDownTime > 750) {
|
||||
this.setState({ keyboardFocus: true });
|
||||
}
|
||||
};
|
||||
|
||||
private handleBlur: React.EventHandler<FocusEvent<any>> = event => {
|
||||
if (this.props.onBlur) {
|
||||
this.props.onBlur(event);
|
||||
}
|
||||
this.setState({ keyboardFocus: false });
|
||||
};
|
||||
|
||||
private handleMouseDown: React.EventHandler<MouseEvent<any>> = event => {
|
||||
if (this.props.onMouseDown) {
|
||||
this.props.onMouseDown(event);
|
||||
}
|
||||
this.setState({ lastMouseDownTime: new Date().getTime() });
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
keyboardFocus={this.state.keyboardFocus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return WithKeyboardFocus as React.ComponentType<any>;
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
DefaultingInferableComponentEnhancer,
|
||||
withPropsOnChange,
|
||||
} from "recompose";
|
||||
|
||||
/**
|
||||
* withStyles provides a property `classes: object` that
|
||||
* includes the classNames from `styles` and extensions from the
|
||||
* property `classes`.
|
||||
*/
|
||||
function withStyles<T>(
|
||||
styles: T
|
||||
): DefaultingInferableComponentEnhancer<{ classes?: Partial<T> }> {
|
||||
const classes = { ...(styles as any) };
|
||||
return withPropsOnChange<any, any>(["classes"], props => {
|
||||
if (props.classes) {
|
||||
Object.keys(props.classes).forEach(k => {
|
||||
if (classes[k]) {
|
||||
classes[k] += ` ${props.classes[k]}`;
|
||||
} else if (process.env.NODE_ENV !== "production") {
|
||||
// tslint:disable:next-line: no-console
|
||||
console.warn("Extending non existant className", k);
|
||||
}
|
||||
});
|
||||
}
|
||||
return { classes };
|
||||
});
|
||||
}
|
||||
|
||||
export default withStyles;
|
||||
@@ -0,0 +1,34 @@
|
||||
.buttonReset {
|
||||
|
||||
/* reset button */
|
||||
user-select: none;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
border: none;
|
||||
touch-action: manipulation;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
|
||||
/* Unify anchor and button. */
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
align-items: flex-start;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
background: transparent;
|
||||
font-size: inherit;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important;
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&:-moz-focusring {
|
||||
color: transparent;
|
||||
textshadow: 0 0 0 #000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
.heading1 {
|
||||
font-size: calc(24rem / $rem-base);
|
||||
font-weight: $font-weight-medium;
|
||||
font-family: "Manuale";
|
||||
line-height: calc(32em / 24);
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
color: $palette-text-primary;
|
||||
}
|
||||
|
||||
.heading2 {
|
||||
font-size: calc(20rem / $rem-base);
|
||||
font-weight: $font-weight-medium;
|
||||
font-family: "Manuale";
|
||||
line-height: calc(24em / 20);
|
||||
color: $palette-text-primary;
|
||||
}
|
||||
|
||||
.heading3 {
|
||||
font-size: calc(18rem / $rem-base);
|
||||
font-weight: $font-weight-medium;
|
||||
font-family: "Manuale";
|
||||
line-height: calc(20em / 18);
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
color: $palette-text-primary;
|
||||
}
|
||||
|
||||
.heading4 {
|
||||
font-size: calc(16rem / $rem-base);
|
||||
font-weight: $font-weight-medium;
|
||||
font-family: "Manuale";
|
||||
line-height: calc(18em / 16);
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
color: $palette-text-primary;
|
||||
}
|
||||
|
||||
.subtitle1 {}
|
||||
|
||||
.subtitle2 {}
|
||||
|
||||
.body2 {}
|
||||
|
||||
.body1 {
|
||||
font-size: calc(16rem / $rem-base);
|
||||
font-weight: $font-weight-regular;
|
||||
font-family: "Source Sans Pro";
|
||||
line-height: calc(18em / 16);
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
color: $palette-text-primary;
|
||||
}
|
||||
|
||||
.button {
|
||||
color: $palette-text-secondary;
|
||||
font-family: "Source Sans Pro";
|
||||
font-weight: $font-weight-medium;
|
||||
font-size: 16px;
|
||||
letter-spacing: calc(0.57em / 16);
|
||||
}
|
||||
|
||||
.overline {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user