mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
268
Commits
v5.0.0-beta.1
...
v5.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca811a3496 | ||
|
|
10719bd874 | ||
|
|
bb23c80004 | ||
|
|
2491445579 | ||
|
|
3642c642d6 | ||
|
|
fce7b08ca0 | ||
|
|
aa9dcb4e04 | ||
|
|
9e9c016f4b | ||
|
|
40b247350b | ||
|
|
2945184b07 | ||
|
|
a55472e086 | ||
|
|
033433549a | ||
|
|
2ef07429c2 | ||
|
|
b83d737530 | ||
|
|
18346d1683 | ||
|
|
ce4a3408fc | ||
|
|
2dbba52fbd | ||
|
|
59b8dfccda | ||
|
|
3394628f27 | ||
|
|
8ee74b7d24 | ||
|
|
f85af8e88f | ||
|
|
9d6592feef | ||
|
|
53c87118db | ||
|
|
bdc9c5f05c | ||
|
|
e334c3266a | ||
|
|
a31ee3bc96 | ||
|
|
a7eb757be7 | ||
|
|
4038fcbcd6 | ||
|
|
5714f441bb | ||
|
|
ac96e5e312 | ||
|
|
e91da1c9a3 | ||
|
|
e816f8e019 | ||
|
|
121469ade5 | ||
|
|
1d6e9e38b0 | ||
|
|
626d46d813 | ||
|
|
24915be712 | ||
|
|
044cbd39f1 | ||
|
|
cd6a1db85b | ||
|
|
8bee5ae1f3 | ||
|
|
153c910de7 | ||
|
|
990a70d6f9 | ||
|
|
9aa39bf842 | ||
|
|
d169d3dd51 | ||
|
|
b38a4f117b | ||
|
|
ea71230b90 | ||
|
|
2dac5f29c1 | ||
|
|
9df473178b | ||
|
|
1f7fdb0d00 | ||
|
|
3bfcc509d2 | ||
|
|
b0e0ba6633 | ||
|
|
0fbfdac846 | ||
|
|
8d08382aea | ||
|
|
8409dbb4ea | ||
|
|
bae88f476e | ||
|
|
918ba10867 | ||
|
|
e7ab0b27f9 | ||
|
|
191687335b | ||
|
|
33487be0ce | ||
|
|
e56793f2fb | ||
|
|
6fe4646755 | ||
|
|
dd45f46b19 | ||
|
|
4a1492e88d | ||
|
|
3a4eae87ad | ||
|
|
fa3d442b36 | ||
|
|
6edb411175 | ||
|
|
6dbc724f31 | ||
|
|
d0df6bc849 | ||
|
|
808b355a27 | ||
|
|
53fa5f43e5 | ||
|
|
c045f52daa | ||
|
|
b3b26bd9f3 | ||
|
|
0af2fec31b | ||
|
|
139c1eb01f | ||
|
|
941a385d53 | ||
|
|
e61e62a238 | ||
|
|
a5c3e94751 | ||
|
|
5c1eb57d3e | ||
|
|
7b5b260a18 | ||
|
|
1c24060f94 | ||
|
|
4e4986fc03 | ||
|
|
37e39ff8c5 | ||
|
|
fe2d78f1f7 | ||
|
|
43844bca18 | ||
|
|
6fb772944c | ||
|
|
b3064c89de | ||
|
|
e4bfbbc1f8 | ||
|
|
25c7f89b31 | ||
|
|
921461008e | ||
|
|
64f102e6d4 | ||
|
|
741739bc16 | ||
|
|
2cc32d4332 | ||
|
|
867f0993cc | ||
|
|
24a27ca958 | ||
|
|
d03b180a30 | ||
|
|
72ef65577b | ||
|
|
8ef366f132 | ||
|
|
3114727410 | ||
|
|
a89dc4675a | ||
|
|
2559d1c0e8 | ||
|
|
ea3c1e3b58 | ||
|
|
457644c45b | ||
|
|
1b9a72c2f0 | ||
|
|
47834a82bb | ||
|
|
a8976a2141 | ||
|
|
718bd2758f | ||
|
|
721b2e7d67 | ||
|
|
f9e1bf144e | ||
|
|
e86e9d2c68 | ||
|
|
d031df38f6 | ||
|
|
8d368b03ec | ||
|
|
3e64aa1193 | ||
|
|
f84c3e4a60 | ||
|
|
36225e443b | ||
|
|
ed8be75774 | ||
|
|
f8cf4e94d3 | ||
|
|
1a69f48087 | ||
|
|
e0ab799c84 | ||
|
|
aa8e1e2d7a | ||
|
|
d2af78d3d7 | ||
|
|
9bd38db61a | ||
|
|
ac203607ae | ||
|
|
e49905c5ad | ||
|
|
15d41f1ae8 | ||
|
|
60a1f43d51 | ||
|
|
3c9ebcf1a1 | ||
|
|
e35978096f | ||
|
|
04c56b3fb5 | ||
|
|
5bf4f22931 | ||
|
|
1550377f39 | ||
|
|
efea0e8e1c | ||
|
|
0fad1070a6 | ||
|
|
ac1ed45e4b | ||
|
|
a696ef9be5 | ||
|
|
aa32fcaea6 | ||
|
|
fcf3640adc | ||
|
|
46f0d08cf7 | ||
|
|
fc039f8186 | ||
|
|
7e7ce9fbd0 | ||
|
|
0e37a474fa | ||
|
|
ffecd5e981 | ||
|
|
07df7ac182 | ||
|
|
2edabf44c7 | ||
|
|
1c75b7659d | ||
|
|
6d803b0179 | ||
|
|
f3e615a610 | ||
|
|
b9f9e9b2d4 | ||
|
|
7809cd3d68 | ||
|
|
add5224338 | ||
|
|
8dae0feb52 | ||
|
|
ee93267d97 | ||
|
|
0f02b05689 | ||
|
|
3baf4e96f5 | ||
|
|
8d91bd7dd1 | ||
|
|
d400c1518c | ||
|
|
d9ad1b2ee4 | ||
|
|
035ee73edd | ||
|
|
43b6a2cdcd | ||
|
|
635e740fc0 | ||
|
|
c37ee12e57 | ||
|
|
e76fddd13c | ||
|
|
abb793b16a | ||
|
|
aa873559d0 | ||
|
|
0296041ce8 | ||
|
|
1a62203d45 | ||
|
|
0eddf9619f | ||
|
|
fda9f40616 | ||
|
|
8d78677154 | ||
|
|
7900b02c40 | ||
|
|
956014b53f | ||
|
|
86bba73919 | ||
|
|
c245e9ba74 | ||
|
|
e3f24811fc | ||
|
|
11fc3697ed | ||
|
|
22188e7c31 | ||
|
|
4c65d43954 | ||
|
|
ff964b54a3 | ||
|
|
3f307002e2 | ||
|
|
b41ddbc22f | ||
|
|
c979dfd7d5 | ||
|
|
1f07c23dea | ||
|
|
3694794b01 | ||
|
|
5df2de6afc | ||
|
|
4e548e8fbf | ||
|
|
fdc6afec42 | ||
|
|
836a2267bf | ||
|
|
290ceee8e9 | ||
|
|
1c5c82286e | ||
|
|
3d9c94d36a | ||
|
|
3a8e4e6429 | ||
|
|
7a8a9f425a | ||
|
|
b108faed5e | ||
|
|
7b7cec5258 | ||
|
|
8e130aa5d4 | ||
|
|
c069bbe923 | ||
|
|
06a3e4e0a7 | ||
|
|
0c5cdc9f3c | ||
|
|
1dba761b40 | ||
|
|
14e225e26f | ||
|
|
4f640826de | ||
|
|
b1732f8a00 | ||
|
|
fc464bd7c9 | ||
|
|
eabf1b28a2 | ||
|
|
0c97ba0f4a | ||
|
|
d73bdc7eec | ||
|
|
bdb57aef7f | ||
|
|
d312d380ae | ||
|
|
bc0b0d0339 | ||
|
|
f95b705585 | ||
|
|
b5b9cb7e2f | ||
|
|
0b20542f05 | ||
|
|
9e55ea66fa | ||
|
|
9a191b44ba | ||
|
|
e7745a85aa | ||
|
|
da1fa9c9fc | ||
|
|
0754ceb803 | ||
|
|
6fe7d56836 | ||
|
|
633998307d | ||
|
|
414a4c2a42 | ||
|
|
e77103d872 | ||
|
|
51b142035e | ||
|
|
76033118e5 | ||
|
|
e72b15c505 | ||
|
|
3576b6a943 | ||
|
|
413f3e2f1e | ||
|
|
0e247ba383 | ||
|
|
4ab8b09be1 | ||
|
|
05f9daec2e | ||
|
|
972f4b702d | ||
|
|
3cd7bf655e | ||
|
|
9d1f03115f | ||
|
|
f8cf34e34d | ||
|
|
5311681333 | ||
|
|
662f5ce314 | ||
|
|
3947b143cb | ||
|
|
05c8f06ed8 | ||
|
|
f320e5521b | ||
|
|
25a7945408 | ||
|
|
d30ced8142 | ||
|
|
812649153e | ||
|
|
5b64d3d14b | ||
|
|
d4b99a2a57 | ||
|
|
ed4e5fa2a8 | ||
|
|
2cff4572cf | ||
|
|
8cfced464b | ||
|
|
39d7540987 | ||
|
|
593d94ddbb | ||
|
|
1cf2f61bfb | ||
|
|
132ffdb2dc | ||
|
|
6bd3e093b3 | ||
|
|
1302ea1918 | ||
|
|
4ce7512c4b | ||
|
|
7beab6419e | ||
|
|
e7151806ea | ||
|
|
dce60c5eab | ||
|
|
8618ef0b96 | ||
|
|
6da97c57d7 | ||
|
|
3ea696fcc8 | ||
|
|
ab466510f3 | ||
|
|
3ba10fd15f | ||
|
|
898bc0de06 | ||
|
|
1794086683 | ||
|
|
d4b8e5ef70 | ||
|
|
df57b4eb17 | ||
|
|
945bd7f2b0 | ||
|
|
9972253056 | ||
|
|
2b878b224b | ||
|
|
ea6f42c256 | ||
|
|
98795d161d |
@@ -1,10 +1,13 @@
|
||||
# job_environment will setup the environment for any job being executed.
|
||||
job_environment: &job_environment
|
||||
NODE_ENV: test
|
||||
WEBPACK_MAX_CORES: 4
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
|
||||
# job_defaults applies all the defaults for each job.
|
||||
job_defaults: &job_defaults
|
||||
working_directory: ~/coralproject/talk
|
||||
resource_class: large
|
||||
docker:
|
||||
- image: circleci/node:10
|
||||
environment:
|
||||
@@ -86,7 +89,7 @@ jobs:
|
||||
name: Perform testing
|
||||
# We're running these tests in band to avoid errors where the circleci
|
||||
# test runner runs out of RAM trying to run them all in parallel.
|
||||
command: npm run test -- --ci --runInBand --testResultsProcessor="jest-junit"
|
||||
command: npm run test -- --ci --runInBand --reporters=default --reporters=jest-junit
|
||||
- store_test_results:
|
||||
path: reports/junit
|
||||
- store_artifacts:
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
*.d.ts
|
||||
*.graphql.ts
|
||||
**/__generated__/**
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
const typescriptEslintRecommended = require('@typescript-eslint/eslint-plugin/dist/configs/eslint-recommended').default.overrides[0];
|
||||
const typescriptRecommended = require('@typescript-eslint/eslint-plugin/dist/configs/recommended.json');
|
||||
const typescriptRecommendedTypeChecking = require('@typescript-eslint/eslint-plugin/dist/configs/recommended-requiring-type-checking.json');
|
||||
const typescriptEslintPrettier = require('eslint-config-prettier/@typescript-eslint');
|
||||
const react = require('eslint-plugin-react').configs.recommended;
|
||||
const jsxA11y = require('eslint-plugin-jsx-a11y').configs.recommended;
|
||||
const reactPrettier = require('eslint-config-prettier/react');
|
||||
|
||||
const typescriptOverrides = {
|
||||
files: ["*.ts", "*.tsx"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
"@typescript-eslint",
|
||||
"@typescript-eslint/tslint",
|
||||
"react",
|
||||
"jsx-a11y",
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
}
|
||||
},
|
||||
rules: Object.assign(
|
||||
typescriptEslintRecommended.rules,
|
||||
typescriptRecommended.rules,
|
||||
typescriptEslintPrettier.rules,
|
||||
react.rules,
|
||||
jsxA11y.rules,
|
||||
reactPrettier.rules,
|
||||
{
|
||||
"@typescript-eslint/adjacent-overload-signatures": "error",
|
||||
// TODO: (cvle) change `readonly` param to `array-simple` when upgraded typescript.
|
||||
"@typescript-eslint/array-type": ["error", { "default": "array-simple", "readonly": "generic"}],
|
||||
"@typescript-eslint/ban-types": "error",
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
"@typescript-eslint/consistent-type-assertions": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": "error",
|
||||
"@typescript-eslint/class-name-casing": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": [
|
||||
"error",
|
||||
{
|
||||
"overrides": {
|
||||
"constructors": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/indent": "off",
|
||||
"@typescript-eslint/interface-name-prefix": "error",
|
||||
"@typescript-eslint/member-delimiter-style": "off",
|
||||
"@typescript-eslint/no-empty-function": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-parameter-properties": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", {"args": "none", "ignoreRestSiblings": true}],
|
||||
"@typescript-eslint/no-use-before-define": "off", // TODO: (cvle) Should be on?
|
||||
"@typescript-eslint/no-use-before-declare": "off",
|
||||
"@typescript-eslint/no-var-requires": "error",
|
||||
"@typescript-eslint/prefer-for-of": "error",
|
||||
"@typescript-eslint/prefer-function-type": "error",
|
||||
"@typescript-eslint/prefer-namespace-keyword": "error",
|
||||
"@typescript-eslint/triple-slash-reference": "error",
|
||||
"@typescript-eslint/type-annotation-spacing": "off",
|
||||
"@typescript-eslint/unified-signatures": "error",
|
||||
// (cvle) disabled, because the way we use labels in our code cause to many
|
||||
// false positives.
|
||||
"jsx-a11y/label-has-associated-control": "off",
|
||||
"react/display-name": "error",
|
||||
"react/prop-types": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
let typescriptTypeCheckingOverrides = {
|
||||
files: ["*.ts", "*.tsx"],
|
||||
parserOptions: {
|
||||
project: ["tsconfig.json", "./src/tsconfig.json", "./src/core/client/tsconfig.json"],
|
||||
// TODO: (cvle) this is a workaround, see: https://github.com/typescript-eslint/typescript-eslint/issues/1091.
|
||||
createDefaultProgram: true,
|
||||
},
|
||||
rules: Object.assign(
|
||||
typescriptRecommendedTypeChecking.rules,
|
||||
{
|
||||
"@typescript-eslint/tslint/config": ["error", {
|
||||
"rules": {
|
||||
"ordered-imports": {
|
||||
"options": {
|
||||
"import-sources-order": "case-insensitive",
|
||||
"module-source-path": "full",
|
||||
"named-imports-order": "case-insensitive",
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
"@typescript-eslint/require-await": "off",
|
||||
"@typescript-eslint/no-misused-promises": "off",
|
||||
"@typescript-eslint/unbound-method": "off", // 10.10.19: (cvle) seems to give false positive.
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
const jestOverrides = {
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
files: ["test/**/*.ts", "test/**/*.tsx"],
|
||||
globals: {
|
||||
"expectAndFail": "readonly",
|
||||
"fail": "readonly",
|
||||
},
|
||||
};
|
||||
|
||||
// Setup the overrides.
|
||||
const overrides = [jestOverrides, typescriptOverrides];
|
||||
// Skip type information to make it faster!
|
||||
if (process.env.FAST_LINT !== "true") {
|
||||
overrides.push(typescriptTypeCheckingOverrides);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
overrides,
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:jsdoc/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
parserOptions: {
|
||||
"ecmaVersion": 2018,
|
||||
},
|
||||
rules: {
|
||||
"arrow-body-style": "off",
|
||||
"arrow-parens": [
|
||||
"off",
|
||||
"as-needed",
|
||||
],
|
||||
"camelcase": "off",
|
||||
"complexity": "off",
|
||||
"constructor-super": "error",
|
||||
"spaced-comment": ["error", "always"],
|
||||
"curly": "error",
|
||||
"dot-notation": "error",
|
||||
"eol-last": "off",
|
||||
"eqeqeq": "error",
|
||||
"guard-for-in": "error",
|
||||
"jsdoc/require-jsdoc": "off",
|
||||
"jsdoc/require-returns": "off",
|
||||
"jsdoc/require-param": "off",
|
||||
"jsdoc/require-param-type": "off",
|
||||
"jsdoc/require-returns-type": "off",
|
||||
"linebreak-style": "off",
|
||||
"max-classes-per-file": [
|
||||
"error",
|
||||
1,
|
||||
],
|
||||
"member-ordering": "off",
|
||||
"new-parens": "off",
|
||||
"newline-per-chained-call": "off",
|
||||
"no-bitwise": "error",
|
||||
"no-caller": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-console": "error",
|
||||
"no-debugger": "error",
|
||||
"no-empty": "error",
|
||||
"no-eval": "error",
|
||||
"no-extra-semi": "off",
|
||||
"no-fallthrough": "error",
|
||||
"no-invalid-this": "off",
|
||||
"no-irregular-whitespace": "off",
|
||||
"no-multiple-empty-lines": "off",
|
||||
"no-new-wrappers": "error",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-shadow": "error",
|
||||
"no-throw-literal": "error",
|
||||
"no-undef": "off",
|
||||
"no-undef-init": "error",
|
||||
"no-unsafe-finally": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"no-unused-labels": "error",
|
||||
"no-unused-vars": ["error", {"args": "none", "ignoreRestSiblings": true}],
|
||||
"no-var": "error",
|
||||
"object-shorthand": "error",
|
||||
"one-var": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
"prefer-const": "error",
|
||||
"quote-props": "off",
|
||||
"radix": "error",
|
||||
"require-atomic-updates": "off",
|
||||
"space-before-function-paren": "off",
|
||||
"sort-imports": "off",
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": "off",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report to help us improve Coral
|
||||
labels: "bug"
|
||||
---
|
||||
|
||||
<!--
|
||||
Please note that but submitting a bug report, you agree to our Code of Conduct: http://code-of-conduct.voxmedia.com/
|
||||
|
||||
When submitting a bug report, provide as much detail as possible, including a screenshot or copy-paste of any related error messages, logs, or other output that might be related. Places to look for information include your browser console, server console, and network logs. The more information you can give the better.
|
||||
|
||||
Please help us by doing the following steps before logging an issue:
|
||||
* Search for other similar issues: https://github.com/coralproject/talk/search
|
||||
* Read our docs: https://docs.coralproject.net
|
||||
|
||||
Please fill in the *entire* template below.
|
||||
-->
|
||||
|
||||
**Expected behavior:**
|
||||
|
||||
**Actual behavior:**
|
||||
|
||||
**Related Issues:** <!-- Did you find other bugs that looked similar? -->
|
||||
|
||||
**Versions:**
|
||||
|
||||
- NodeJS:
|
||||
- NPM:
|
||||
- MongoDB:
|
||||
- Redis:
|
||||
- Browser:
|
||||
- OS:
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a feature idea for Coral
|
||||
labels: "feature idea"
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Please note that but submitting a feature request, you agree to our Code of Conduct: http://code-of-conduct.voxmedia.com/
|
||||
|
||||
Please help us by doing the following steps before logging an issue:
|
||||
* Search to see if there are similar requests and ideas that are already logged: https://github.com/coralproject/talk/search
|
||||
|
||||
-->
|
||||
|
||||
## Search Terms
|
||||
|
||||
<!-- List of keywords you searched for before creating this issue. Write them down here so that others can find this suggestion more easily -->
|
||||
|
||||
## Suggestion
|
||||
|
||||
<!-- A summary of what you'd like to see added or changed -->
|
||||
|
||||
## Use Cases
|
||||
|
||||
<!--
|
||||
What do you want to use this for?
|
||||
What shortcomings exist with current approaches?
|
||||
-->
|
||||
@@ -0,0 +1,14 @@
|
||||
<!--
|
||||
Thank you for submitting a pull request! Please note that by contributing to Coral, you agree to our Code of Conduct: http://code-of-conduct.voxmedia.com/
|
||||
|
||||
Before submitting your PR, please verify that:
|
||||
* [ ] Your code is up-to-date with the base branch
|
||||
* [ ] You've successfully run `npm run test` locally
|
||||
|
||||
Refer to CONTRIBUTING.MD for more details.
|
||||
https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md
|
||||
-->
|
||||
|
||||
## What does this PR do?
|
||||
|
||||
## How do I test this PR?
|
||||
@@ -21,3 +21,4 @@ dist
|
||||
*.css.d.ts
|
||||
__generated__
|
||||
README.md.orig
|
||||
persisted-queries.json
|
||||
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"kumar-harsh.graphql-for-vscode",
|
||||
"editorconfig.editorconfig",
|
||||
"ms-azuretools.vscode-cosmosdb",
|
||||
"mike-co.import-sorter"
|
||||
]
|
||||
}
|
||||
Vendored
+11
-1
@@ -7,7 +7,7 @@
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"name": "Launch Development Server",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"args": [
|
||||
"-r",
|
||||
@@ -18,10 +18,20 @@
|
||||
],
|
||||
"env": {
|
||||
"TS_NODE_PROJECT": "${workspaceFolder}/src/tsconfig.json",
|
||||
"TS_NODE_FILES": "true",
|
||||
"TS_NODE_TRANSPILE_ONLY": "true",
|
||||
// Set to 1 here beacuse clustering gets all weird with logging + output.
|
||||
"CONCURRENCY": "1"
|
||||
},
|
||||
"outputCapture": "std"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach to Node Process",
|
||||
"processId": "${command:PickProcess}",
|
||||
"restart": true,
|
||||
"protocol": "inspector"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+43
-5
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"files.associations": {
|
||||
"*.css": "postcss"
|
||||
},
|
||||
@@ -11,9 +10,48 @@
|
||||
"**/.DS_Store": true,
|
||||
".vs": true
|
||||
},
|
||||
"tslint.exclude": "**/node_modules/**",
|
||||
"tslint.autoFixOnSave": true,
|
||||
"tslint.jsEnable": true,
|
||||
"tslint.enable": false,
|
||||
"eslint.validate": [
|
||||
{ "language": "javascript", "autoFix": true },
|
||||
{ "language": "typescript", "autoFix": true },
|
||||
{ "language": "typescriptreact", "autoFix": true }
|
||||
],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"postcss.validate": false
|
||||
"postcss.validate": false,
|
||||
"javascript.preferences.importModuleSpecifier": "non-relative",
|
||||
"importSorter.importStringConfiguration.quoteMark": "double",
|
||||
"importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.count": 80,
|
||||
"importSorter.sortConfiguration.customOrderingRules.rules": [
|
||||
{
|
||||
"type": "importMember",
|
||||
"regex": "^$",
|
||||
"orderLevel": 70,
|
||||
"disableSort": true
|
||||
},
|
||||
{
|
||||
"regex": "__generated__",
|
||||
"orderLevel": 40
|
||||
},
|
||||
{
|
||||
"regex": "^coral-",
|
||||
"orderLevel": 30
|
||||
},
|
||||
{
|
||||
"regex": "\\.css$",
|
||||
"orderLevel": 60,
|
||||
"disableSort": true
|
||||
},
|
||||
{
|
||||
"regex": "^[.]",
|
||||
"orderLevel": 50
|
||||
},
|
||||
],
|
||||
"importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.type": "newLineEachExpressionAfterCountLimitExceptIfOnlyOne",
|
||||
"importSorter.importStringConfiguration.trailingComma": "multiLine",
|
||||
"importSorter.importStringConfiguration.tabSize": 2,
|
||||
"eslint.enable": true,
|
||||
"importSorter.generalConfiguration.exclude": [
|
||||
"d\\.ts$",
|
||||
"__generated__"
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Vox Media Code of Conduct
|
||||
|
||||
## Introduction
|
||||
|
||||
This code of conduct governs the environment of the Vox Product team. We created it not because we anticipate bad behavior, but because we believe that articulating our values and obligations to one another reinforces the already exceptional level of respect among the team and because having a code provides us with clear avenues to correct our culture should it ever stray from that course. We make this code public in the hopes of contributing to the ongoing conversation about inclusion in the tech, design, and media communities and encourage other teams to fork it and make it their own. To our team, we commit to enforce and evolve this code as our team grows.
|
||||
|
||||
Read the rest here: http://code-of-conduct.voxmedia.com/
|
||||
@@ -0,0 +1,66 @@
|
||||
# Contributor's Guide
|
||||
|
||||
Welcome! We are very excited that you are interested in contributing to Coral.
|
||||
|
||||
This document is a companion to help you approach contributing. If it does not
|
||||
do so, please [let us know how we can improve it](https://github.com/coralproject/talk/issues)!
|
||||
|
||||
By contributing to this project you agree to the
|
||||
[Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
## What should I Contribute?
|
||||
|
||||
There are at least three ways to contribute to Coral:
|
||||
|
||||
- Writing Code
|
||||
- Providing Translations
|
||||
|
||||
## Writing Code
|
||||
|
||||
Conversation surrounding contributions begins in
|
||||
[issues](https://github.com/coralproject/talk/issues).
|
||||
|
||||
### When should I create an issue?
|
||||
|
||||
File an issue as soon as you have an idea of something you'd like to contribute.
|
||||
We would love to hear what you're thinking and help refine the idea to make it
|
||||
into the Coral ecosystem.
|
||||
|
||||
Please file issues if you would like to contribute to Coral.
|
||||
|
||||
### What should I include?
|
||||
|
||||
Coral has adopted an iterative, agile development philosophy. All contributions
|
||||
that make it into the Coral repository should start with a user story in this
|
||||
form:
|
||||
|
||||
> As a [type of Coral user] I'd like to [do something] so that I can [get some result/value].
|
||||
|
||||
This exercise does two things:
|
||||
|
||||
- allows us to ground our technical choices in a clear, simple product need.
|
||||
- expresses that product need in a way that doesn't imply a specific technical
|
||||
solution allowing for debate as to the best way to solve the problem.
|
||||
|
||||
Please feel free to provide as much detail as possible when filing the issue but
|
||||
please do keep the initial issue specific to one need and try to avoid including
|
||||
technical or design solutions.
|
||||
|
||||
If you have a specific technical or design solution in mind, please submit it as
|
||||
the first comment on the thread.
|
||||
|
||||
## Localization
|
||||
|
||||
We use the [fluent](http://projectfluent.org/) library and store our
|
||||
translations in [FTL](http://projectfluent.org/fluent/guide/) files in
|
||||
`src/locales/` and `src/core/server/locales/`.
|
||||
|
||||
Strings are added or removed from localization bundles in the translation files
|
||||
as needed. Strings **MUST NOT** be _changed_ after they've been committed and
|
||||
pushed to master. Changing a string requires creating a new ID with a new name
|
||||
(preferably descriptive instead of incremented) and deletion of the obsolete ID.
|
||||
It's often useful to add a comment above the string with info about how and
|
||||
where the string is used.
|
||||
|
||||
Once a language has enough coverage, it should be added to
|
||||
`src/core/common/helpers/i18n/locales.ts`.
|
||||
@@ -1,244 +1,41 @@
|
||||
# talk 
|
||||
# Coral 
|
||||
|
||||
Online comments are broken. Our open-source commenting platform, Talk, rethinks
|
||||
Online comments are broken. Our open-source commenting platform, Coral, rethinks
|
||||
how moderation, comment display, and conversation function, creating the
|
||||
opportunity for safer, smarter discussions around your work.
|
||||
[Read more about Talk here](https://coralproject.net/talk).
|
||||
[Read more about Coral here](https://coralproject.net/talk).
|
||||
|
||||
Built with <3 by The Coral Project, a part of [Vox Media](https://product.voxmedia.com/).
|
||||
|
||||
Preview Talk easily by running Talk via a Heroku App:
|
||||
Preview Coral easily by running Coral via a Heroku App:
|
||||
|
||||
[](https://heroku.com/deploy?template=https://github.com/coralproject/talk/tree/next)
|
||||
[](https://heroku.com/deploy?template=https://github.com/coralproject/talk)
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table of Contents
|
||||
|
||||
- [Requirements](#requirements)
|
||||
- [Running](#running)
|
||||
- [Docker](#docker)
|
||||
- [Source](#source)
|
||||
- [Development](#development)
|
||||
- [Design Language System (UI Components)](#design-language-system-ui-components)
|
||||
- [Configuration](#configuration)
|
||||
- [Documentation](#documentation)
|
||||
- [Pre-Launch Guide](#pre-launch-guide)
|
||||
- [More Resources](#more-resources)
|
||||
- [License](#license)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Requirements
|
||||
## Documentation
|
||||
|
||||
- MongoDB >=3.6
|
||||
- Redis >=3.2
|
||||
- NodeJS >=10
|
||||
- NPM >=6.7
|
||||
You can get started with Coral using our [Documentation](https://docs.coralproject.net/talk/).
|
||||
|
||||
## Running
|
||||
## Pre-Launch Guide
|
||||
|
||||
You can install Talk using Docker or via Source. We recommend Docker, as it
|
||||
provides the easiest deployment solution going forward, as all the dependencies
|
||||
are baked and shipped with the provided
|
||||
[coralproject/talk:next](https://hub.docker.com/r/coralproject/talk) image.
|
||||
When v5 releases to master, you'll be able to select it using
|
||||
`coralproject/talk:5`.
|
||||
You’ve installed Talk on your server, and you’re preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Talk Community Guide](https://coralproject.net/blog/youve-installed-talk-now-what/).
|
||||
|
||||
### Docker
|
||||
## More Resources
|
||||
|
||||
The easiest way to get started with Talk is through our published Docker image
|
||||
and provided example `docker-compose.yml` file. The following assumes that you
|
||||
have Docker and Docker Compose installed on your local machine:
|
||||
|
||||
- Install Docker: https://docs.docker.com/install/
|
||||
- Install Docker Compose: https://docs.docker.com/compose/install/ (this is typically included in the Docker Desktop editions already)
|
||||
|
||||
```bash
|
||||
# Create directories to persist the data in MongoDB and Redis.
|
||||
mkdir -p data/{mongo,redis}
|
||||
|
||||
# Create the `docker-compose.yml` file to get started.
|
||||
cat > docker-compose.yml <<EOF
|
||||
version: "2"
|
||||
services:
|
||||
talk:
|
||||
image: coralproject/talk:next
|
||||
restart: always
|
||||
ports:
|
||||
- "127.0.0.1:3000:5000"
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
environment:
|
||||
- MONGODB_URI=mongodb://mongo:27017/talk
|
||||
- REDIS_URI=redis://redis:6379
|
||||
- SIGNING_SECRET=<replace me with something secret>
|
||||
mongo:
|
||||
image: mongo:3.6
|
||||
volumes:
|
||||
- ./data/mongo:/data/db
|
||||
redis:
|
||||
image: redis:3.2
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
EOF
|
||||
|
||||
# Start up Talk using Docker.
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Then head on over to http://localhost:3000 to install Talk!
|
||||
|
||||
### Source
|
||||
|
||||
Talk requires NodeJS >=10, we recommend using `nvm` to help manage node
|
||||
versions: https://github.com/creationix/nvm.
|
||||
|
||||
```bash
|
||||
# Clone and cd into the Talk directory.
|
||||
git clone https://github.com/coralproject/talk.git
|
||||
cd talk
|
||||
|
||||
# Install dependencies.
|
||||
npm install
|
||||
|
||||
# Build the application dependencies.
|
||||
# This might take a while.
|
||||
npm run build
|
||||
```
|
||||
|
||||
This should output all the compiled application code to `./dist`.
|
||||
|
||||
Running Talk with default settings assumes that you have:
|
||||
|
||||
- MongoDB >=3.6 running on `127.0.0.1:27017`
|
||||
- Redis >=3.2 running on `127.0.0.1:6379`
|
||||
|
||||
If you don't already have these databases running, you can execute the following
|
||||
assuming you have Docker installed on your local machine:
|
||||
|
||||
```bash
|
||||
docker run -d -p 27017:27017 --restart always --name mongo mongo:3.6
|
||||
docker run -d -p 6379:6379 --restart always --name redis redis:3.2
|
||||
```
|
||||
|
||||
Then start Talk with:
|
||||
|
||||
```bash
|
||||
# Start the server in production mode.
|
||||
npm run start
|
||||
```
|
||||
|
||||
Then head on over to http://localhost:3000 to install Talk!
|
||||
|
||||
### Development
|
||||
|
||||
Running Talk for development is very similar to installing Talk via Source as
|
||||
described above.
|
||||
|
||||
Talk requires NodeJS >=10, we recommend using `nvm` to help manage node
|
||||
versions: https://github.com/creationix/nvm.
|
||||
|
||||
```bash
|
||||
# Clone and cd into the Talk directory.
|
||||
git clone https://github.com/coralproject/talk.git
|
||||
cd talk
|
||||
|
||||
# Install dependencies.
|
||||
npm install
|
||||
```
|
||||
|
||||
Running Talk with default settings assumes that you have:
|
||||
|
||||
- MongoDB >=3.6 running on `127.0.0.1:27017`
|
||||
- Redis >=3.2 running on `127.0.0.1:6379`
|
||||
|
||||
If you don't already have these databases running, you can execute the following
|
||||
assuming you have Docker installed on your local machine:
|
||||
|
||||
```bash
|
||||
docker run -d -p 27017:27017 --restart always --name mongo mongo:3.6
|
||||
docker run -d -p 6379:6379 --restart always --name redis redis:3.2
|
||||
```
|
||||
|
||||
Then start Talk with:
|
||||
|
||||
```bash
|
||||
# Run the server in development mode in order to facilitate auto-restarting and
|
||||
# rebuilding when file changes are detected. This might take a while to fully run.
|
||||
npm run watch
|
||||
```
|
||||
|
||||
When the client code has been built, navigate to http://localhost:8080/install.html
|
||||
to start the installation wizard. **Note: Ensure `localhost:8080` is used in the permitted domains list.**
|
||||
|
||||
To see the comment stream goto http://localhost:8080/.
|
||||
|
||||
To run linting and tests use the following commands:
|
||||
|
||||
```bash
|
||||
# Run the linters.
|
||||
npm run lint
|
||||
|
||||
# Run our unit and integration tests.
|
||||
npm run test
|
||||
```
|
||||
|
||||
#### Design Language System (UI Components)
|
||||
|
||||
We use [docz](https://docz.site) to document and develop our Design Language System. To start docz run:
|
||||
|
||||
```bash
|
||||
# Make sure CSS types are generated.
|
||||
# This is not required when `npm run watch` is already running.
|
||||
npm run generate:css-types
|
||||
|
||||
# Run docz in development.
|
||||
npm run docz -- dev
|
||||
```
|
||||
|
||||
After compilation has finished you can access docz at http://localhost:3030/.
|
||||
|
||||
## Configuration
|
||||
|
||||
The following environment variables can be set to configure the Talk Server. You
|
||||
can expose them in your shell via `export NODE_ENV=development` or by placing
|
||||
the variables in a `.env` file in the root of the project in a simple
|
||||
`NODE_ENV=development` format delimited by newlines.
|
||||
|
||||
- `NODE_ENV` - Can be one of `production` or `development`. All production
|
||||
deployments should use `production`. Defaults to `production` when ran with
|
||||
`npm run start` and `development` when run with `npm run start:development`.
|
||||
- `PORT` - The port to listen for HTTP and Websocket requests. (Default `3000`)
|
||||
- `MONGODB_URI` - The MongoDB database URI to connect to.
|
||||
(Default `mongodb://127.0.0.1:27017/talk`)
|
||||
- `REDIS_URI` - The Redis database URI to connect to.
|
||||
(Default `redis://127.0.0.1:6379`)
|
||||
- `SIGNING_SECRET` - The shared secret to use to sign JSON Web Tokens (JWT) with
|
||||
the selected signing algorithm. 🚨 **Don't forget to set this variable!** 🚨
|
||||
(Default: `keyboard cat`)
|
||||
- `SIGNING_ALGORITHM` - The signing algorithm to use for signing JWT's.
|
||||
(Default `HS256`).
|
||||
- `LOGGING_LEVEL` - The logging level that can be set to one of `fatal`,
|
||||
`error`, `warn`, `info`, `debug`, or `trace`. (Default `info`)
|
||||
- `STATIC_URI` - The URI that static assets can be accessed from. This URI can
|
||||
be to a proxy that uses this Talk server on `PORT` as the upstream. Disabled
|
||||
by default.
|
||||
- `DISABLE_TENANT_CACHING` - When `true`, all tenants will be loaded from the
|
||||
database when needed rather than keeping a in-memory copy in sync via
|
||||
published events on Redis. (Default `false`)
|
||||
- `DISABLE_MONGODB_AUTOINDEXING` - When `true`, Talk will not perform indexing
|
||||
operations when it starts up. This can be desired when you've already
|
||||
installed Talk on the target MongoDB, but want to improve start performance.
|
||||
(Default `false`)
|
||||
- `LOCALE` - Specify the default locale to use for all requests without a locale
|
||||
specified. (Default `en-US`)
|
||||
- `ENABLE_GRAPHIQL` - When `true`, it will enable the `/graphiql` even in
|
||||
production, use with care. (Default `false`)
|
||||
- `CONCURRENCY` - The number of worker nodes to spawn to handle web traffic,
|
||||
this should be tied to the number of CPU's available. (Default
|
||||
`os.cpus().length`)
|
||||
- `DEV_PORT` - The port where the Webpack Development server is running on.
|
||||
(Default `8080`)
|
||||
- [Our Blog](https://coralproject.net/blog)
|
||||
- [Community Guides for Journalism](https://guides.coralproject.net/)
|
||||
- [More About Us](https://coralproject.net/)
|
||||
|
||||
## License
|
||||
|
||||
Talk is released under the [Apache License, v2.0](/LICENSE).
|
||||
Coral is released under the [Apache License, v2.0](/LICENSE).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Talk",
|
||||
"name": "Coral",
|
||||
"description": "A better commenting experience from Vox Media.",
|
||||
"env": {
|
||||
"REWRITE_ENV": {
|
||||
@@ -7,8 +7,7 @@
|
||||
"value": "REDIS_URI:REDIS_URL,MONGODB_URI:MONGO_URI"
|
||||
},
|
||||
"SIGNING_SECRET": {
|
||||
"description":
|
||||
"The shared secret to use to sign JSON Web Tokens (JWT) with the selected signing algorithm.",
|
||||
"description": "The shared secret to use to sign JSON Web Tokens (JWT) with the selected signing algorithm.",
|
||||
"generator": "secret"
|
||||
},
|
||||
"CONCURRENCY": {
|
||||
@@ -16,13 +15,16 @@
|
||||
"value": "1"
|
||||
},
|
||||
"LOCALE": {
|
||||
"description":
|
||||
"Specify the default locale to use for all requests without a locale specified",
|
||||
"description": "Specify the default locale to use for all requests without a locale specified",
|
||||
"value": "en-US"
|
||||
},
|
||||
"ENABLE_GRAPHIQL": {
|
||||
"description": "When true, this will enable the GraphiQL routes",
|
||||
"value": "false"
|
||||
},
|
||||
"TRUST_PROXY": {
|
||||
"description": "When set to 1, it instructs Coral to trust up to one proxy, this is needed for authentication support",
|
||||
"value": "1"
|
||||
}
|
||||
},
|
||||
"addons": [
|
||||
|
||||
+2
-4
@@ -9,6 +9,7 @@ const lodashOptimizations =
|
||||
process.env.WEBPACK === "true" ? ["use-lodash-es", "lodash"] : [];
|
||||
|
||||
module.exports = {
|
||||
babelrcRoots: ["./src/core/client/*"],
|
||||
env: {
|
||||
production: {
|
||||
plugins: [...lodashOptimizations],
|
||||
@@ -18,10 +19,7 @@ module.exports = {
|
||||
},
|
||||
test: {
|
||||
presets: [
|
||||
[
|
||||
"@babel/env",
|
||||
{ targets: "last 2 versions, ie 11", modules: "commonjs" },
|
||||
],
|
||||
["@babel/env", { targets: { node: "current" } }],
|
||||
"@babel/react",
|
||||
],
|
||||
plugins: ["dynamic-import-node"],
|
||||
|
||||
@@ -10,8 +10,7 @@ module.exports = {
|
||||
"<rootDir>/src/core/build/polyfills.js",
|
||||
"<rootDir>/src/core/client/test/setup.ts",
|
||||
],
|
||||
setupTestFrameworkScriptFile:
|
||||
"<rootDir>/src/core/client/test/setupTestFramework.ts",
|
||||
setupFilesAfterEnv: ["<rootDir>/src/core/client/test/setupTestFramework.ts"],
|
||||
testMatch: ["**/*.spec.{js,jsx,mjs,ts,tsx}"],
|
||||
testEnvironment: "node",
|
||||
testURL: "http://localhost",
|
||||
@@ -27,19 +26,25 @@ module.exports = {
|
||||
"[/\\\\]node_modules[/\\\\](?!(fluent|react-relay-network-modern)[/\\\\]).+\\.(js|jsx|mjs|ts|tsx)$",
|
||||
],
|
||||
moduleNameMapper: {
|
||||
"^talk-admin/(.*)$": "<rootDir>/src/core/client/admin/$1",
|
||||
"^talk-auth/(.*)$": "<rootDir>/src/core/client/auth/$1",
|
||||
"^talk-ui/(.*)$": "<rootDir>/src/core/client/ui/$1",
|
||||
"^talk-stream/(.*)$": "<rootDir>/src/core/client/stream/$1",
|
||||
"^talk-framework/(.*)$": "<rootDir>/src/core/client/framework/$1",
|
||||
"^talk-common/(.*)$": "<rootDir>/src/core/common/$1",
|
||||
"^coral-account/(.*)$": "<rootDir>/src/core/client/account/$1",
|
||||
"^coral-admin/(.*)$": "<rootDir>/src/core/client/admin/$1",
|
||||
"^coral-auth/(.*)$": "<rootDir>/src/core/client/auth/$1",
|
||||
"^coral-count/(.*)$": "<rootDir>/src/core/client/count/$1",
|
||||
"^coral-ui/(.*)$": "<rootDir>/src/core/client/ui/$1",
|
||||
"^coral-stream/(.*)$": "<rootDir>/src/core/client/stream/$1",
|
||||
"^coral-framework/(.*)$": "<rootDir>/src/core/client/framework/$1",
|
||||
"^coral-common/(.*)$": "<rootDir>/src/core/common/$1",
|
||||
"^coral-test/(.*)$": "<rootDir>/src/core/client/test/$1",
|
||||
},
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node", "ftl"],
|
||||
snapshotSerializers: ["enzyme-to-json/serializer"],
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
useBabelrc: true,
|
||||
tsConfigFile: path.resolve(__dirname, "tsconfig.jest.json"),
|
||||
tsConfigFile: path.resolve(
|
||||
__dirname,
|
||||
"../../src/core/client/tsconfig.json"
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
displayName: "server",
|
||||
rootDir: "../../",
|
||||
@@ -12,13 +14,14 @@ module.exports = {
|
||||
"^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest",
|
||||
},
|
||||
moduleNameMapper: {
|
||||
"^talk-server/(.*)$": "<rootDir>/src/core/server/$1",
|
||||
"^talk-common/(.*)$": "<rootDir>/src/core/common/$1",
|
||||
"^coral-server/(.*)$": "<rootDir>/src/core/server/$1",
|
||||
"^coral-common/(.*)$": "<rootDir>/src/core/common/$1",
|
||||
},
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
useBabelrc: true,
|
||||
tsConfigFile: path.resolve(__dirname, "../../src/tsconfig.json"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"module": "es2015"
|
||||
}
|
||||
}
|
||||
+55
-11
@@ -27,7 +27,24 @@ const config: Config = {
|
||||
"**/test/**/*",
|
||||
"core/**/*.spec.*",
|
||||
],
|
||||
executor: new CommandExecutor("npm run generate:relay-stream", {
|
||||
executor: new CommandExecutor("npm run --silent generate:relay:stream", {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
generateRelayAccount: {
|
||||
paths: [
|
||||
"core/client/account/**/*.ts",
|
||||
"core/client/account/**/*.tsx",
|
||||
"core/client/account/**/*.graphql",
|
||||
"core/server/**/*.graphql",
|
||||
],
|
||||
ignore: [
|
||||
"core/**/*.d.ts",
|
||||
"core/**/*.graphql.ts",
|
||||
"**/test/**/*",
|
||||
"core/**/*.spec.*",
|
||||
],
|
||||
executor: new CommandExecutor("npm run generate:relay:account", {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
@@ -44,7 +61,7 @@ const config: Config = {
|
||||
"**/test/**/*",
|
||||
"core/**/*.spec.*",
|
||||
],
|
||||
executor: new CommandExecutor("npm run generate:relay-admin", {
|
||||
executor: new CommandExecutor("npm run --silent generate:relay:admin", {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
@@ -61,7 +78,7 @@ const config: Config = {
|
||||
"**/test/**/*",
|
||||
"core/**/*.spec.*",
|
||||
],
|
||||
executor: new CommandExecutor("npm run generate:relay-auth", {
|
||||
executor: new CommandExecutor("npm run --silent generate:relay:auth", {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
@@ -78,40 +95,67 @@ const config: Config = {
|
||||
"**/test/**/*",
|
||||
"core/**/*.spec.*",
|
||||
],
|
||||
executor: new CommandExecutor("npm run generate:relay-install", {
|
||||
executor: new CommandExecutor("npm run --silent generate:relay:install", {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
generateCSSTypes: {
|
||||
paths: ["**/*.css"],
|
||||
executor: new CommandExecutor("npm run generate:css-types", {
|
||||
executor: new CommandExecutor("npm run --silent generate:css-types", {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
runServer: {
|
||||
paths: ["core/**/*.ts", "core/locales/**/*.ftl"],
|
||||
paths: ["core/server/locales/**/*.ftl"],
|
||||
ignore: ["core/client/**/*"],
|
||||
executor: new LongRunningExecutor("npm run start:development"),
|
||||
executor: new LongRunningExecutor("npm run --silent start:development"),
|
||||
},
|
||||
runServerWithoutClientRoutes: {
|
||||
paths: ["core/server/locales/**/*.ftl"],
|
||||
ignore: ["core/client/**/*"],
|
||||
executor: new LongRunningExecutor(
|
||||
"DISABLE_CLIENT_ROUTES=true npm run --silent start:development"
|
||||
),
|
||||
},
|
||||
runServerLint: {
|
||||
paths: ["core/**/*.ts"],
|
||||
ignore: ["core/client/**/*"],
|
||||
executor: new LongRunningExecutor("npm run --silent lint:server"),
|
||||
},
|
||||
runServerSyntaxCheck: {
|
||||
paths: ["core/**/*.ts"],
|
||||
ignore: ["core/client/**/*"],
|
||||
executor: new LongRunningExecutor("npm run --silent tscheck:server"),
|
||||
},
|
||||
runWebpackDevServer: {
|
||||
paths: [],
|
||||
executor: new LongRunningExecutor("npm run start:webpackDevServer"),
|
||||
executor: new LongRunningExecutor(
|
||||
"npm run --silent start:webpackDevServer"
|
||||
),
|
||||
},
|
||||
runDocz: {
|
||||
paths: [],
|
||||
executor: new LongRunningExecutor("npm run docz -- dev"),
|
||||
executor: new LongRunningExecutor("npm run --silent docz -- dev"),
|
||||
},
|
||||
},
|
||||
defaultSet: "client",
|
||||
sets: {
|
||||
server: ["generateSchemaTypes", "runServer"],
|
||||
client: [
|
||||
server: [
|
||||
"generateSchemaTypes",
|
||||
"runServer",
|
||||
"runServerLint",
|
||||
"runServerSyntaxCheck",
|
||||
],
|
||||
client: [
|
||||
"runServerWithoutClientRoutes",
|
||||
"runServerLint",
|
||||
"runServerSyntaxCheck",
|
||||
"runWebpackDevServer",
|
||||
"generateCSSTypes",
|
||||
"generateRelayStream",
|
||||
"generateRelayAuth",
|
||||
"generateRelayInstall",
|
||||
"generateRelayAccount",
|
||||
"generateRelayAdmin",
|
||||
"generateSchemaTypes",
|
||||
],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import errorOverlayMiddleware from "react-dev-utils/errorOverlayMiddleware";
|
||||
import evalSourceMapMiddleware from "react-dev-utils/evalSourceMapMiddleware";
|
||||
import ignoredFiles from "react-dev-utils/ignoredFiles";
|
||||
import noopServiceWorkerMiddleware from "react-dev-utils/noopServiceWorkerMiddleware";
|
||||
import { Configuration } from "webpack-dev-server";
|
||||
@@ -16,6 +17,12 @@ export default function({
|
||||
publicPath,
|
||||
}: WebpackDevServerConfig): Configuration {
|
||||
return {
|
||||
stats: {
|
||||
// https://github.com/TypeStrong/ts-loader#transpileonly-boolean-defaultfalse
|
||||
// Using transpilation only without typechecks gives warnings when we reexport types.
|
||||
// We can ignore them here.
|
||||
warningsFilter: /export .* was not found in/,
|
||||
},
|
||||
// Enable gzip compression of generated files.
|
||||
compress: true,
|
||||
// Silence WebpackDevServer's own logs since they're generally not useful.
|
||||
@@ -61,19 +68,31 @@ export default function({
|
||||
historyApiFallback: {
|
||||
disableDotRule: true,
|
||||
rewrites: [
|
||||
{ from: /^\/account/, to: "/account.html" },
|
||||
{ from: /^\/admin/, to: "/admin.html" },
|
||||
{ from: /^\/embed\/stream/, to: "/stream.html" },
|
||||
{ from: /^\/embed\/auth$/, to: "/auth.html" },
|
||||
{ from: /^\/embed\/auth\/callback/, to: "/auth-callback.html" },
|
||||
{ from: /^\/install/, to: "/install.html" },
|
||||
],
|
||||
},
|
||||
public: allowedHost,
|
||||
index: "embed.html",
|
||||
proxy: {
|
||||
// Proxy websocket connections.
|
||||
"/api/graphql/live": {
|
||||
target: `ws://localhost:${serverPort}`,
|
||||
ws: true,
|
||||
},
|
||||
// Proxy to the graphql server.
|
||||
"/api": {
|
||||
target: `http://localhost:${serverPort}`,
|
||||
},
|
||||
},
|
||||
before(app: any) {
|
||||
before(app, server) {
|
||||
// This lets us fetch source contents from webpack for the error overlay
|
||||
app.use(evalSourceMapMiddleware(server));
|
||||
|
||||
// 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
|
||||
|
||||
@@ -5,36 +5,90 @@ const path = require("path");
|
||||
const fs = require("fs");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
const extensions = [".ts", ".tsx", ".js"];
|
||||
// TODO: There is some weird issue with including paths.ts here
|
||||
const postCSSConfigPath = "./src/core/build/postcss.config";
|
||||
const isProduction = process.NODE_ENV === "production";
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
|
||||
const styleLoader = {
|
||||
loader: require.resolve("style-loader"),
|
||||
options: {
|
||||
sourceMap: true,
|
||||
hmr: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "Talk 5.0",
|
||||
title: "Coral 5.0",
|
||||
source: "./src",
|
||||
typescript: true,
|
||||
host: process.env.HOST || "0.0.0.0",
|
||||
port: parseInt(process.env.DOCZ_PORT, 10) || 3030,
|
||||
codeSandbox: false, // Too large to create code sandboxes..
|
||||
modifyBundlerConfig: config => {
|
||||
config.entry.app.push(
|
||||
`${appDirectory}/src/core/client/ui/theme/variables.css.ts`
|
||||
);
|
||||
config.module.rules.push({
|
||||
test: /\.css$/,
|
||||
test: /\.css\.ts$/,
|
||||
use: [
|
||||
require.resolve("style-loader"),
|
||||
styleLoader,
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
modules: true,
|
||||
importLoaders: 1,
|
||||
localIdentName:
|
||||
process.env.NODE_ENV === "production"
|
||||
? "[hash:base64]"
|
||||
: "[name]-[local]-[hash:base64:5]",
|
||||
importLoaders: 2,
|
||||
localIdentName: "[name]-[local]-[hash:base64:5]",
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("postcss-loader"),
|
||||
options: {
|
||||
config: {
|
||||
// TODO: There is some weird issue with including paths.ts here
|
||||
path: "./src/core/build/postcss.config",
|
||||
path: postCSSConfigPath,
|
||||
},
|
||||
parser: "postcss-js",
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("babel-loader"),
|
||||
options: {
|
||||
configFile: false,
|
||||
babelrc: false,
|
||||
presets: [
|
||||
"@babel/typescript",
|
||||
[
|
||||
"@babel/env",
|
||||
{ targets: { node: "10.0.0" }, modules: "commonjs" },
|
||||
],
|
||||
],
|
||||
// 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,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
config.module.rules.push({
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
styleLoader,
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
modules: true,
|
||||
importLoaders: 1,
|
||||
localIdentName: "[name]-[local]-[hash:base64:5]",
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("postcss-loader"),
|
||||
options: {
|
||||
config: {
|
||||
path: postCSSConfigPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,586 @@
|
||||
## Viewer Events
|
||||
_Viewer Events_ are emitted when the viewer performs certain actions.
|
||||
They can be subscribed to using the `events` parameter in
|
||||
`Coral.createStreamEmbed`.
|
||||
```html
|
||||
<script>
|
||||
const CoralStreamEmbed = Coral.createStreamEmbed({
|
||||
events: function(events) {
|
||||
events.onAny(function(eventName, data) {
|
||||
console.log(eventName, data);
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
Example events:
|
||||
- `setMainTab {tab: "PROFILE"}`
|
||||
- `showFeaturedCommentTooltip`
|
||||
- `viewConversation {from: "FEATURED_COMMENTS", commentID: "c45fb5f5-03f9-49a3-a755-488c698ca0df"}`
|
||||
|
||||
### Viewer Network Events
|
||||
|
||||
_Viewer Network Events_ are events that involves a network request and thus can succeed or fail. Succeeding events will have a `.success` appended to the event name while failing events have an `.error` appended to the event name.
|
||||
|
||||
Moreover _Viewer Network Events_ contains the `rtt` field which indicates the time it needed from initiating the request until the _UI_ has been updated with the response data.
|
||||
|
||||
Example events:
|
||||
```
|
||||
createComment.success
|
||||
{
|
||||
body: "Hello world!",
|
||||
storyID: "238b95ec-2b80-43f4-ab68-a6ea1f4e2584",
|
||||
rtt: 307,
|
||||
success: {
|
||||
id: "6fecfb11-4d0f-4edc-89b7-878a9928addd"
|
||||
status: "APPROVED"`
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
createComment.error
|
||||
{
|
||||
body: "Hi!",
|
||||
storyID: "238b95ec-2b80-43f4-ab68-a6ea1f4e2584",
|
||||
rtt: 229,
|
||||
error: {
|
||||
code: "COMMENT_BODY_TOO_SHORT"
|
||||
message: "Comment body must have at least 10 characters."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event List
|
||||
<!-- START docs:events -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN npm run docs:events -->
|
||||
### Index
|
||||
- <a href="#approveComment">approveComment</a>
|
||||
- <a href="#banUser">banUser</a>
|
||||
- <a href="#cancelAccountDeletion">cancelAccountDeletion</a>
|
||||
- <a href="#changeEmail">changeEmail</a>
|
||||
- <a href="#changePassword">changePassword</a>
|
||||
- <a href="#changeUsername">changeUsername</a>
|
||||
- <a href="#closeStory">closeStory</a>
|
||||
- <a href="#copyPermalink">copyPermalink</a>
|
||||
- <a href="#createComment">createComment</a>
|
||||
- <a href="#createCommentFocus">createCommentFocus</a>
|
||||
- <a href="#createCommentReaction">createCommentReaction</a>
|
||||
- <a href="#createCommentReply">createCommentReply</a>
|
||||
- <a href="#editComment">editComment</a>
|
||||
- <a href="#featureComment">featureComment</a>
|
||||
- <a href="#gotoModeration">gotoModeration</a>
|
||||
- <a href="#ignoreUser">ignoreUser</a>
|
||||
- <a href="#loadMoreAllComments">loadMoreAllComments</a>
|
||||
- <a href="#loadMoreFeaturedComments">loadMoreFeaturedComments</a>
|
||||
- <a href="#loadMoreHistoryComments">loadMoreHistoryComments</a>
|
||||
- <a href="#loginPrompt">loginPrompt</a>
|
||||
- <a href="#openSortMenu">openSortMenu</a>
|
||||
- <a href="#openStory">openStory</a>
|
||||
- <a href="#rejectComment">rejectComment</a>
|
||||
- <a href="#removeCommentReaction">removeCommentReaction</a>
|
||||
- <a href="#removeUserIgnore">removeUserIgnore</a>
|
||||
- <a href="#replyCommentFocus">replyCommentFocus</a>
|
||||
- <a href="#reportComment">reportComment</a>
|
||||
- <a href="#requestAccountDeletion">requestAccountDeletion</a>
|
||||
- <a href="#requestDownloadCommentHistory">requestDownloadCommentHistory</a>
|
||||
- <a href="#resendEmailVerification">resendEmailVerification</a>
|
||||
- <a href="#setCommentsOrderBy">setCommentsOrderBy</a>
|
||||
- <a href="#setCommentsTab">setCommentsTab</a>
|
||||
- <a href="#setMainTab">setMainTab</a>
|
||||
- <a href="#setProfileTab">setProfileTab</a>
|
||||
- <a href="#showAbsoluteTimestamp">showAbsoluteTimestamp</a>
|
||||
- <a href="#showAllReplies">showAllReplies</a>
|
||||
- <a href="#showAuthPopup">showAuthPopup</a>
|
||||
- <a href="#showEditEmailDialog">showEditEmailDialog</a>
|
||||
- <a href="#showEditForm">showEditForm</a>
|
||||
- <a href="#showEditPasswordDialog">showEditPasswordDialog</a>
|
||||
- <a href="#showEditUsernameDialog">showEditUsernameDialog</a>
|
||||
- <a href="#showFeaturedCommentTooltip">showFeaturedCommentTooltip</a>
|
||||
- <a href="#showIgnoreUserdDialog">showIgnoreUserdDialog</a>
|
||||
- <a href="#showModerationPopover">showModerationPopover</a>
|
||||
- <a href="#showMoreOfConversation">showMoreOfConversation</a>
|
||||
- <a href="#showMoreReplies">showMoreReplies</a>
|
||||
- <a href="#showReplyForm">showReplyForm</a>
|
||||
- <a href="#showReportPopover">showReportPopover</a>
|
||||
- <a href="#showSharePopover">showSharePopover</a>
|
||||
- <a href="#showUserPopover">showUserPopover</a>
|
||||
- <a href="#signOut">signOut</a>
|
||||
- <a href="#unfeatureComment">unfeatureComment</a>
|
||||
- <a href="#updateNotificationSettings">updateNotificationSettings</a>
|
||||
- <a href="#updateStorySettings">updateStorySettings</a>
|
||||
- <a href="#viewConversation">viewConversation</a>
|
||||
- <a href="#viewFullDiscussion">viewFullDiscussion</a>
|
||||
- <a href="#viewNewComments">viewNewComments</a>
|
||||
|
||||
### Events
|
||||
- <a id="approveComment">**approveComment.success**, **approveComment.error**</a>: This event is emitted when the viewer approves a comment.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="banUser">**banUser.success**, **banUser.error**</a>: This event is emitted when the viewer bans a user.
|
||||
```ts
|
||||
{
|
||||
userID: string;
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="cancelAccountDeletion">**cancelAccountDeletion.success**, **cancelAccountDeletion.error**</a>: This event is emitted when the viewer cancels the account deletion.
|
||||
```ts
|
||||
{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="changeEmail">**changeEmail.success**, **changeEmail.error**</a>: This event is emitted when the viewer changes its email.
|
||||
```ts
|
||||
{
|
||||
oldEmail: string;
|
||||
newEmail: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="changePassword">**changePassword.success**, **changePassword.error**</a>: This event is emitted when the viewer changes its password.
|
||||
```ts
|
||||
{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="changeUsername">**changeUsername.success**, **changeUsername.error**</a>: This event is emitted when the viewer changes its username.
|
||||
```ts
|
||||
{
|
||||
oldUsername: string;
|
||||
newUsername: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="closeStory">**closeStory.success**, **closeStory.error**</a>: This event is emitted when the viewer closes the story.
|
||||
```ts
|
||||
{
|
||||
storyID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="copyPermalink">**copyPermalink**</a>: This event is emitted when the viewer copies the permalink with the button.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="createComment">**createComment.success**, **createComment.error**</a>: This event is emitted when a top level comment is created.
|
||||
```ts
|
||||
{
|
||||
storyID: string;
|
||||
body: string;
|
||||
success: {
|
||||
id: string;
|
||||
status: COMMENT_STATUS;
|
||||
};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="createCommentFocus">**createCommentFocus**</a>: This event is emitted when the viewer focus on the RTE to create a comment.
|
||||
- <a id="createCommentReaction">**createCommentReaction.success**, **createCommentReaction.error**</a>: This event is emitted when the viewer reacts to a comment.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="createCommentReply">**createCommentReply.success**, **createCommentReply.error**</a>: This event is emitted when a comment reply is created.
|
||||
```ts
|
||||
{
|
||||
body: string;
|
||||
parentID: string;
|
||||
success: {
|
||||
id: string;
|
||||
status: COMMENT_STATUS;
|
||||
};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="editComment">**editComment.success**, **editComment.error**</a>: This event is emitted when the viewer edits a comment.
|
||||
```ts
|
||||
{
|
||||
body: string;
|
||||
commentID: string;
|
||||
success: {
|
||||
status: COMMENT_STATUS;
|
||||
};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="featureComment">**featureComment.success**, **featureComment.error**</a>: This event is emitted when the viewer features a comment.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="gotoModeration">**gotoModeration**</a>: This event is emitted when the viewer goes to moderation.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="ignoreUser">**ignoreUser.success**, **ignoreUser.error**</a>: This event is emitted when the viewer ignores a user.
|
||||
```ts
|
||||
{
|
||||
userID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="loadMoreAllComments">**loadMoreAllComments.success**, **loadMoreAllComments.error**</a>: This event is emitted when the viewer loads more top level comments into the comment stream.
|
||||
```ts
|
||||
{
|
||||
storyID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="loadMoreFeaturedComments">**loadMoreFeaturedComments.success**, **loadMoreFeaturedComments.error**</a>: This event is emitted when the viewer loads more featured comments.
|
||||
```ts
|
||||
{
|
||||
storyID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="loadMoreHistoryComments">**loadMoreHistoryComments.success**, **loadMoreHistoryComments.error**</a>: This event is emitted when the viewer loads more top level comments into the history comment stream.
|
||||
```ts
|
||||
{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="loginPrompt">**loginPrompt**</a>: This event is emitted when the viewer does an action that will prompt a login dialog.
|
||||
- <a id="openSortMenu">**openSortMenu**</a>: This event is emitted when the viewer clicks on the sort menu.
|
||||
- <a id="openStory">**openStory.success**, **openStory.error**</a>: This event is emitted when the viewer opens the story.
|
||||
```ts
|
||||
{
|
||||
storyID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="rejectComment">**rejectComment.success**, **rejectComment.error**</a>: This event is emitted when the viewer rejects a comment.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="removeCommentReaction">**removeCommentReaction.success**, **removeCommentReaction.error**</a>: This event is emitted when the viewer removes its reaction from a comment.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="removeUserIgnore">**removeUserIgnore.success**, **removeUserIgnore.error**</a>: This event is emitted when the viewer remove a user from its ignored users list.
|
||||
```ts
|
||||
{
|
||||
userID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="replyCommentFocus">**replyCommentFocus**</a>: This event is emitted when the viewer focus on the RTE to reply to a comment.
|
||||
- <a id="reportComment">**reportComment.success**, **reportComment.error**</a>: This event is emitted when the viewer reports a comment.
|
||||
```ts
|
||||
{
|
||||
reason: string;
|
||||
commentID: string;
|
||||
additionalDetails?: string | undefined;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="requestAccountDeletion">**requestAccountDeletion.success**, **requestAccountDeletion.error**</a>: This event is emitted when the viewer requests to delete its account.
|
||||
```ts
|
||||
{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="requestDownloadCommentHistory">**requestDownloadCommentHistory.success**, **requestDownloadCommentHistory.error**</a>: This event is emitted when the viewer requests to download its comment history.
|
||||
```ts
|
||||
{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="resendEmailVerification">**resendEmailVerification.success**, **resendEmailVerification.error**</a>: This event is emitted when the viewer request another email verification email.
|
||||
```ts
|
||||
{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="setCommentsOrderBy">**setCommentsOrderBy**</a>: This event is emitted when the viewer changes the sort order of the comments.
|
||||
```ts
|
||||
{
|
||||
orderBy: string;
|
||||
}
|
||||
```
|
||||
- <a id="setCommentsTab">**setCommentsTab**</a>: This event is emitted when the viewer changes the tab of the comments tab bar.
|
||||
```ts
|
||||
{
|
||||
tab: string;
|
||||
}
|
||||
```
|
||||
- <a id="setMainTab">**setMainTab**</a>: This event is emitted when the viewer changes the tab of the main tab bar.
|
||||
```ts
|
||||
{
|
||||
tab: string;
|
||||
}
|
||||
```
|
||||
- <a id="setProfileTab">**setProfileTab**</a>: This event is emitted when the viewer changes the tab of the profile tab bar.
|
||||
```ts
|
||||
{
|
||||
tab: string;
|
||||
}
|
||||
```
|
||||
- <a id="showAbsoluteTimestamp">**showAbsoluteTimestamp**</a>: This event is emitted when the viewer clicks on the relative timestamp to show the absolute time.
|
||||
- <a id="showAllReplies">**showAllReplies.success**, **showAllReplies.error**</a>: This event is emitted when the viewer reveals all replies of a comment.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="showAuthPopup">**showAuthPopup**</a>: This event is emitted when the viewer requests the auth popup.
|
||||
```ts
|
||||
{
|
||||
view: string;
|
||||
}
|
||||
```
|
||||
- <a id="showEditEmailDialog">**showEditEmailDialog**</a>: This event is emitted when the viewer opens the edit email dialog.
|
||||
- <a id="showEditForm">**showEditForm**</a>: This event is emitted when the viewer opens the edit form.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="showEditPasswordDialog">**showEditPasswordDialog**</a>: This event is emitted when the viewer opens the edit password dialog.
|
||||
- <a id="showEditUsernameDialog">**showEditUsernameDialog**</a>: This event is emitted when the viewer opens the edit username dialog.
|
||||
- <a id="showFeaturedCommentTooltip">**showFeaturedCommentTooltip**</a>: This event is emitted when the viewer clicks to show the featured comment tooltip.
|
||||
- <a id="showIgnoreUserdDialog">**showIgnoreUserdDialog**</a>: This event is emitted when the viewer opens the ignore user dialog.
|
||||
- <a id="showModerationPopover">**showModerationPopover**</a>: This event is emitted when the viewer opens the moderation popover.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="showMoreOfConversation">**showMoreOfConversation.success**, **showMoreOfConversation.error**</a>: This event is emitted when the viewer reveals more of the parent conversation thread.
|
||||
```ts
|
||||
{
|
||||
commentID: string | null;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="showMoreReplies">**showMoreReplies**</a>: This event is emitted when the viewer reveals new live replies.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
count: number;
|
||||
}
|
||||
```
|
||||
- <a id="showReplyForm">**showReplyForm**</a>: This event is emitted when the viewer opens the reply form.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="showReportPopover">**showReportPopover**</a>: This event is emitted when the viewer opens the report popover.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="showSharePopover">**showSharePopover**</a>: This event is emitted when the viewer opens the share popover.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="showUserPopover">**showUserPopover**</a>: This event is emitted when the viewer clicks on a username which shows the user popover.
|
||||
```ts
|
||||
{
|
||||
userID: string;
|
||||
}
|
||||
```
|
||||
- <a id="signOut">**signOut.success**, **signOut.error**</a>: This event is emitted when the viewer signs out.
|
||||
```ts
|
||||
{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="unfeatureComment">**unfeatureComment.success**, **unfeatureComment.error**</a>: This event is emitted when the viewer unfeatures a comment.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="updateNotificationSettings">**updateNotificationSettings.success**, **updateNotificationSettings.error**</a>: This event is emitted when the viewer updates its notification settings.
|
||||
```ts
|
||||
{
|
||||
onReply?: boolean | null | undefined;
|
||||
onFeatured?: boolean | null | undefined;
|
||||
onStaffReplies?: boolean | null | undefined;
|
||||
onModeration?: boolean | null | undefined;
|
||||
digestFrequency?: "NONE" | "DAILY" | "HOURLY" | null | undefined;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="updateStorySettings">**updateStorySettings.success**, **updateStorySettings.error**</a>: This event is emitted when the viewer updates the story settings.
|
||||
```ts
|
||||
{
|
||||
storyID: string;
|
||||
live?: {
|
||||
enabled?: boolean | null | undefined;
|
||||
} | null | undefined;
|
||||
moderation?: "POST" | "PRE" | null | undefined;
|
||||
premodLinksEnable?: boolean | null | undefined;
|
||||
messageBox?: {
|
||||
enabled?: boolean | null | undefined;
|
||||
icon?: string | null | undefined;
|
||||
content?: string | null | undefined;
|
||||
} | null | undefined;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="viewConversation">**viewConversation**</a>: This event is emitted when the viewer changes to the single conversation view.
|
||||
```ts
|
||||
{
|
||||
from: "FEATURED_COMMENTS" | "COMMENT_STREAM" | "COMMENT_HISTORY";
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="viewFullDiscussion">**viewFullDiscussion**</a>: This event is emitted when the viewer exits the single conversation.
|
||||
```ts
|
||||
{
|
||||
commentID: string | null;
|
||||
}
|
||||
```
|
||||
- <a id="viewNewComments">**viewNewComments**</a>: This event is emitted when the viewer reveals new live comments.
|
||||
```ts
|
||||
{
|
||||
storyID: string;
|
||||
count: number;
|
||||
}
|
||||
```
|
||||
<!-- END docs:events -->
|
||||
+10
-1
@@ -67,9 +67,18 @@ gulp.task("server:static", () =>
|
||||
.pipe(gulp.dest(resolveDistFolder() + "/core/server"))
|
||||
);
|
||||
|
||||
gulp.task("locales", () =>
|
||||
gulp
|
||||
.src(["./src/locales/**/*"])
|
||||
.pipe(gulp.dest(resolveDistFolder() + "/locales"))
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"server",
|
||||
gulp.series("server:schema", gulp.parallel("server:scripts", "server:static"))
|
||||
gulp.series(
|
||||
"server:schema",
|
||||
gulp.parallel("server:scripts", "server:static", "locales")
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("default", gulp.series("clean", "server"));
|
||||
|
||||
Generated
+14707
-7028
File diff suppressed because it is too large
Load Diff
+200
-143
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coralproject/talk",
|
||||
"version": "5.0.0-beta.1",
|
||||
"version": "5.3.2",
|
||||
"author": "The Coral Project",
|
||||
"homepage": "https://coralproject.net/",
|
||||
"sideEffects": [
|
||||
@@ -12,31 +12,40 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0",
|
||||
"npm": ">=6.7.0"
|
||||
"npm": ">=6.9.0"
|
||||
},
|
||||
"bugs": "https://github.com/coralproject/talk/issues",
|
||||
"contributors": "https://github.com/coralproject/talk/graphs/contributors",
|
||||
"contributors": [
|
||||
"https://github.com/coralproject/talk/graphs/contributors"
|
||||
],
|
||||
"description": "A better commenting experience from Mozilla, The Washington Post, and The New York Times.",
|
||||
"scripts": {
|
||||
"build": "npm-run-all generate --parallel build:*",
|
||||
"build:client": "ts-node ./scripts/build.ts",
|
||||
"build": "NODE_ENV=production npm-run-all generate-persist --parallel build:client build:server",
|
||||
"build:development": "NODE_ENV=development npm-run-all generate --parallel build:client build:server",
|
||||
"build:client": "ts-node --transpile-only ./scripts/build.ts",
|
||||
"build:server": "gulp server",
|
||||
"migration:create": "ts-node --transpile-only ./scripts/migration/create.ts",
|
||||
"doctoc": "doctoc --title='## Table of Contents' --github README.md",
|
||||
"generate": "npm-run-all --parallel generate:*",
|
||||
"docs:events": "ts-node ./scripts/generateEventDocs.ts ./src/core/client/stream/events.ts ./events.md",
|
||||
"generate": "npm-run-all generate:css-types generate:schema generate:relay",
|
||||
"generate-persist": "npm-run-all generate:css-types generate:schema generate:relay-persist",
|
||||
"generate:css-types": "tcm src/core/client/",
|
||||
"generate:relay-stream": "ts-node ./scripts/compileRelay --src ./src/core/client/stream --schema tenant",
|
||||
"generate:relay-auth": "ts-node ./scripts/compileRelay --src ./src/core/client/auth --schema tenant",
|
||||
"generate:relay-install": "ts-node ./scripts/compileRelay --src ./src/core/client/install --schema tenant",
|
||||
"generate:relay-admin": "ts-node ./scripts/compileRelay --src ./src/core/client/admin --schema tenant",
|
||||
"generate:relay": "npm-run-all --parallel generate:relay:*",
|
||||
"generate:relay-persist": "npm-run-all --parallel 'generate:relay:* -- --persist'",
|
||||
"generate:relay:stream": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/stream --schema tenant",
|
||||
"generate:relay:account": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/account --schema tenant",
|
||||
"generate:relay:auth": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/auth --schema tenant",
|
||||
"generate:relay:install": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/install --schema tenant",
|
||||
"generate:relay:admin": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/admin --schema tenant",
|
||||
"generate:schema": "node ./scripts/generateSchemaTypes.js",
|
||||
"docz": "docz",
|
||||
"start": "NODE_ENV=production node dist/index.js",
|
||||
"start:development": "NODE_ENV=development CONCURRENCY=${CONCURRENCY:-2} TS_NODE_PROJECT=./src/tsconfig.json node -r ts-node/register -r tsconfig-paths/register ./src/index.ts",
|
||||
"start:webpackDevServer": "ts-node ./scripts/start.ts",
|
||||
"start:development": "NODE_ENV=development CONCURRENCY=${CONCURRENCY:-1} TS_NODE_PROJECT=./src/tsconfig.json ts-node-dev --inspect --transpile-only --no-notify -r tsconfig-paths/register ./src/index.ts",
|
||||
"start:webpackDevServer": "ts-node --transpile-only ./scripts/start.ts",
|
||||
"lint": "npm-run-all --parallel lint:* tscheck:*",
|
||||
"lint:server": "tslint --project ./src/tsconfig.json",
|
||||
"lint:client": "tslint --project ./src/core/client/tsconfig.json",
|
||||
"lint:scripts": "tslint --project ./tsconfig.json",
|
||||
"lint:server": "eslint 'src/**/*.{js,ts,tsx}' --ignore-pattern 'src/core/client/**'",
|
||||
"lint:client": "eslint 'src/core/client/**/*.{js,ts,tsx}'",
|
||||
"lint:scripts": "eslint 'scripts/**/*.{js,ts,tsx}'",
|
||||
"lint:graphql": "graphql-schema-linter src/core/server/graph/tenant/schema/schema.graphql",
|
||||
"lint-fix": "npm run lint:server -- --fix && npm run lint:client -- --fix && npm run lint:scripts -- --fix",
|
||||
"test": "node scripts/test.js --env=jsdom",
|
||||
@@ -44,61 +53,70 @@
|
||||
"tscheck:server": "tsc --project ./src/tsconfig.json --noEmit",
|
||||
"tscheck:client": "tsc --project ./src/core/client/tsconfig.json --noEmit",
|
||||
"tscheck:scripts": "tsc --project ./tsconfig.json --noEmit",
|
||||
"watch": "NODE_ENV=development ts-node ./scripts/watcher/bin/watcher.ts --config ./config/watcher.ts"
|
||||
"watch": "NODE_ENV=development ts-node --transpile-only ./scripts/watcher/bin/watcher.ts --config ./config/watcher.ts"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@coralproject/bunyan-prettystream": "^0.1.4",
|
||||
"@metascraper/helpers": "^5.7.21",
|
||||
"akismet-api": "^4.2.0",
|
||||
"apollo-server-express": "^2.1.0",
|
||||
"apollo-server-express": "^2.8.1",
|
||||
"archiver": "^3.0.3",
|
||||
"basic-auth": "^2.0.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bull": "^3.4.4",
|
||||
"bull": "^3.8.1",
|
||||
"bunyan": "^1.8.12",
|
||||
"cheerio": "^1.0.0-rc.2",
|
||||
"consolidate": "0.14.0",
|
||||
"content-security-policy-builder": "^2.0.0",
|
||||
"convict": "^4.3.1",
|
||||
"cookie": "^0.4.0",
|
||||
"cookie-parser": "^1.4.4",
|
||||
"cors": "^2.8.4",
|
||||
"cron": "^1.7.1",
|
||||
"csv-stringify": "^5.3.0",
|
||||
"dataloader": "^1.4.0",
|
||||
"dompurify": "^1.0.8",
|
||||
"dompurify": "^2.0.7",
|
||||
"dotenv": "^6.0.0",
|
||||
"dotenv-expand": "^4.2.0",
|
||||
"env-rewrite": "^1.0.2",
|
||||
"express": "^4.16.3",
|
||||
"express-static-gzip": "^0.3.2",
|
||||
"farce": "^0.2.6",
|
||||
"fluent": "^0.10.0",
|
||||
"found": "^0.3.21",
|
||||
"found-relay": "^0.3.1",
|
||||
"fluent-dom": "^0.4.1",
|
||||
"fs-extra": "^6.0.1",
|
||||
"graphql": "^0.13.2",
|
||||
"graphql-config": "^2.0.1",
|
||||
"graphql-extensions": "^0.2.1",
|
||||
"graphql-fields": "^1.1.0",
|
||||
"graphql-playground-html": "^1.6.0",
|
||||
"graphql-redis-subscriptions": "^2.1.0",
|
||||
"graphql-subscriptions": "^1.1.0",
|
||||
"graphql-tools": "^3.0.5",
|
||||
"html-minifier": "^3.5.21",
|
||||
"html-to-text": "^4.0.0",
|
||||
"ioredis": "^3.2.2",
|
||||
"ioredis": "^4.9.0",
|
||||
"joi": "^13.4.0",
|
||||
"jsdom": "^11.12.0",
|
||||
"jsdom": "^15.0.0",
|
||||
"jsonwebtoken": "^8.3.0",
|
||||
"juice": "^5.2.0",
|
||||
"jwks-rsa": "^1.3.0",
|
||||
"linkify-it": "^2.1.0",
|
||||
"linkifyjs": "^2.1.7",
|
||||
"lodash": "^4.17.10",
|
||||
"luxon": "^1.3.1",
|
||||
"metascraper-author": "^3.11.8",
|
||||
"metascraper-date": "^3.11.4",
|
||||
"metascraper-description": "^3.11.8",
|
||||
"metascraper-image": "^3.11.8",
|
||||
"metascraper-title": "^3.11.8",
|
||||
"mongodb": "^3.2.1",
|
||||
"mongodb-core": "^3.2.3",
|
||||
"keymaster": "^1.6.2",
|
||||
"linkifyjs": "^2.1.8",
|
||||
"lodash": "^4.17.15",
|
||||
"lru-cache": "^5.1.1",
|
||||
"luxon": "^1.12.0",
|
||||
"metascraper-author": "^5.8.0",
|
||||
"metascraper-description": "^5.8.0",
|
||||
"metascraper-image": "^5.8.0",
|
||||
"metascraper-title": "^5.8.0",
|
||||
"mongodb": "^3.2.7",
|
||||
"mongodb-core": "^3.2.7",
|
||||
"ms": "^2.1.1",
|
||||
"node-fetch": "^2.2.0",
|
||||
"nodemailer": "^4.6.7",
|
||||
"nunjucks": "^3.1.3",
|
||||
"on-finished": "^2.3.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport-facebook": "^2.1.1",
|
||||
"passport-google-oauth2": "^0.1.6",
|
||||
@@ -107,226 +125,265 @@
|
||||
"passport-strategy": "^1.0.0",
|
||||
"performance-now": "^2.1.0",
|
||||
"permit": "^0.2.4",
|
||||
"prom-client": "^11.3.0",
|
||||
"proxy-agent": "^3.1.1",
|
||||
"querystringify": "^2.1.0",
|
||||
"react-relay-network-modern": "^2.4.0",
|
||||
"source-map-support": "^0.5.10",
|
||||
"react-helmet": "^5.2.1",
|
||||
"source-map-support": "^0.5.12",
|
||||
"stack-utils": "^1.0.2",
|
||||
"striptags": "^3.1.1",
|
||||
"throng": "^4.0.0",
|
||||
"tlds": "^1.203.1",
|
||||
"uuid": "^3.3.2",
|
||||
"tsscmp": "^1.0.6",
|
||||
"url-regex": "^5.0.0",
|
||||
"uuid": "^3.3.3",
|
||||
"verror": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.2.0",
|
||||
"@babel/core": "^7.4.5",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.2.0",
|
||||
"@babel/polyfill": "^7.0.0",
|
||||
"@babel/preset-env": "^7.2.0",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.4.4",
|
||||
"@babel/polyfill": "^7.4.4",
|
||||
"@babel/preset-env": "^7.4.5",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@coralproject/rte": "^0.10.13",
|
||||
"@babel/preset-typescript": "^7.3.3",
|
||||
"@coralproject/npm-run-all": "^4.1.5",
|
||||
"@coralproject/rte": "^0.11.0",
|
||||
"@intervolga/optimize-cssnano-plugin": "^1.0.6",
|
||||
"@types/agent-base": "^4.2.0",
|
||||
"@types/archiver": "^3.0.0",
|
||||
"@types/basic-auth": "^1.1.2",
|
||||
"@types/bcryptjs": "^2.4.1",
|
||||
"@types/bull": "^3.3.16",
|
||||
"@types/bull": "^3.5.12",
|
||||
"@types/bunyan": "^1.8.4",
|
||||
"@types/case-sensitive-paths-webpack-plugin": "^2.1.2",
|
||||
"@types/cheerio": "^0.22.8",
|
||||
"@types/chokidar": "^1.7.5",
|
||||
"@types/classnames": "^2.2.4",
|
||||
"@types/classnames": "^2.2.7",
|
||||
"@types/commander": "^2.12.2",
|
||||
"@types/compression-webpack-plugin": "^0.4.2",
|
||||
"@types/common-tags": "^1.8.0",
|
||||
"@types/compression-webpack-plugin": "^2.0.0",
|
||||
"@types/consolidate": "0.0.34",
|
||||
"@types/convict": "^4.2.0",
|
||||
"@types/cookie": "^0.3.3",
|
||||
"@types/cookie-parser": "^1.4.1",
|
||||
"@types/cors": "^2.8.4",
|
||||
"@types/cron": "^1.7.1",
|
||||
"@types/cross-spawn": "^6.0.0",
|
||||
"@types/dompurify": "0.0.33",
|
||||
"@types/dotenv": "^4.0.3",
|
||||
"@types/enzyme": "^3.1.15",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.3",
|
||||
"@types/eventemitter2": "^4.1.0",
|
||||
"@types/express": "^4.16.0",
|
||||
"@types/flat": "0.0.28",
|
||||
"@types/fs-extra": "^5.0.4",
|
||||
"@types/graphql": "^0.13.3",
|
||||
"@types/html-minifier": "^3.5.2",
|
||||
"@types/html-to-text": "^1.4.31",
|
||||
"@types/html-webpack-plugin": "^3.2.0",
|
||||
"@types/ioredis": "^3.2.12",
|
||||
"@types/jest": "^23.1.5",
|
||||
"@types/ioredis": "^4.0.10",
|
||||
"@types/jest": "^24.0.13",
|
||||
"@types/joi": "^13.0.8",
|
||||
"@types/jsdom": "^11.12.0",
|
||||
"@types/jsdom": "^12.2.3",
|
||||
"@types/jsonwebtoken": "^7.2.7",
|
||||
"@types/linkify-it": "^2.0.4",
|
||||
"@types/linkifyjs": "^2.1.0",
|
||||
"@types/linkifyjs": "^2.1.1",
|
||||
"@types/lodash": "^4.14.118",
|
||||
"@types/lru-cache": "^5.1.0",
|
||||
"@types/luxon": "^1.12.0",
|
||||
"@types/marked": "^0.6.0",
|
||||
"@types/mini-css-extract-plugin": "^0.2.0",
|
||||
"@types/mongodb": "^3.1.22",
|
||||
"@types/ms": "^0.7.30",
|
||||
"@types/node": "^10.5.2",
|
||||
"@types/node-fetch": "^2.1.2",
|
||||
"@types/node-fetch": "^2.3.3",
|
||||
"@types/nodemailer": "^4.6.2",
|
||||
"@types/nunjucks": "^3.0.0",
|
||||
"@types/nunjucks": "^3.1.1",
|
||||
"@types/object-diff": "0.0.0",
|
||||
"@types/on-finished": "^2.3.1",
|
||||
"@types/passport": "^0.4.6",
|
||||
"@types/passport-facebook": "^2.1.8",
|
||||
"@types/passport-local": "^1.0.33",
|
||||
"@types/passport-oauth2": "^1.4.5",
|
||||
"@types/passport-strategy": "^0.2.33",
|
||||
"@types/permit": "^0.2.1",
|
||||
"@types/prop-types": "^15.5.8",
|
||||
"@types/react": "^16.8.10",
|
||||
"@types/react-copy-to-clipboard": "^4.2.5",
|
||||
"@types/react-dom": "^16.8.3",
|
||||
"@types/react-relay": "^1.3.9",
|
||||
"@types/react": "^16.8.15",
|
||||
"@types/react-copy-to-clipboard": "^4.2.6",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"@types/react-helmet": "^5.0.10",
|
||||
"@types/react-relay": "^1.3.14",
|
||||
"@types/react-responsive": "^3.0.1",
|
||||
"@types/react-test-renderer": "^16.8.1",
|
||||
"@types/react-transition-group": "^2.0.14",
|
||||
"@types/recompose": "^0.26.5",
|
||||
"@types/relay-runtime": "^1.3.6",
|
||||
"@types/relay-runtime": "^1.3.12",
|
||||
"@types/sane": "^2.0.0",
|
||||
"@types/shallow-equals": "^1.0.0",
|
||||
"@types/simplemde": "^1.11.7",
|
||||
"@types/sinon": "^5.0.1",
|
||||
"@types/source-map-support": "^0.4.1",
|
||||
"@types/sinon": "^7.0.11",
|
||||
"@types/source-map-support": "^0.5.0",
|
||||
"@types/stack-utils": "^1.0.1",
|
||||
"@types/throng": "^4.0.2",
|
||||
"@types/tlds": "^1.199.0",
|
||||
"@types/uuid": "^3.4.3",
|
||||
"@types/uuid": "^3.4.4",
|
||||
"@types/verror": "^1.10.3",
|
||||
"@types/vinyl": "^2.0.2",
|
||||
"@types/webpack": "^4.4.7",
|
||||
"@types/webpack": "^4.4.31",
|
||||
"@types/webpack-assets-manifest": "^3.0.0",
|
||||
"@types/webpack-bundle-analyzer": "^2.13.0",
|
||||
"@types/webpack-dev-server": "^2.9.5",
|
||||
"@types/webpack-bundle-analyzer": "^2.13.1",
|
||||
"@types/webpack-dev-server": "^3.1.5",
|
||||
"@types/ws": "^5.1.2",
|
||||
"autoprefixer": "^8.6.5",
|
||||
"@typescript-eslint/eslint-plugin": "2.3.3",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "2.3.3",
|
||||
"@typescript-eslint/parser": "2.3.3",
|
||||
"acorn": "^6.1.1",
|
||||
"ansi-styles": "^3.2.0",
|
||||
"autoprefixer": "^9.5.1",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^23.6.0",
|
||||
"babel-loader": "^8.0.4",
|
||||
"babel-jest": "^24.8.0",
|
||||
"babel-loader": "^8.0.5",
|
||||
"babel-plugin-dynamic-import-node": "^2.2.0",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"babel-plugin-module-resolver": "^3.1.1",
|
||||
"babel-plugin-relay": "^1.7.0",
|
||||
"babel-plugin-module-resolver": "^3.2.0",
|
||||
"babel-plugin-relay": "^4.0.0",
|
||||
"babel-plugin-use-lodash-es": "^0.2.0",
|
||||
"babel-preset-react-optimize": "^1.0.1",
|
||||
"bowser": "^1.9.4",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.1.2",
|
||||
"chalk": "^2.4.1",
|
||||
"chokidar": "^2.0.4",
|
||||
"classnames": "^2.2.5",
|
||||
"commander": "^2.16.0",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.2.0",
|
||||
"chalk": "^2.4.2",
|
||||
"chokidar": "^3.0.0",
|
||||
"classnames": "^2.2.6",
|
||||
"commander": "^2.20.0",
|
||||
"comment-json": "^1.1.3",
|
||||
"compression-webpack-plugin": "^1.1.11",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"common-tags": "^1.8.0",
|
||||
"compression-webpack-plugin": "^2.0.0",
|
||||
"copy-webpack-plugin": "^5.0.3",
|
||||
"cross-spawn": "^6.0.5",
|
||||
"css-loader": "^1.0.1",
|
||||
"del": "^3.0.0",
|
||||
"del": "^4.1.1",
|
||||
"doctoc": "^1.4.0",
|
||||
"docz": "^0.13.7",
|
||||
"docz-theme-default": "^0.13.7",
|
||||
"enzyme": "^3.8.0",
|
||||
"enzyme-adapter-react-16": "^1.7.1",
|
||||
"enzyme": "^3.9.0",
|
||||
"enzyme-adapter-react-16": "^1.12.1",
|
||||
"enzyme-to-json": "^3.3.5",
|
||||
"eslint": "^6.5.1",
|
||||
"eslint-config-prettier": "^6.3.0",
|
||||
"eslint-plugin-jsdoc": "^15.9.7",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-react": "^7.15.1",
|
||||
"eventemitter2": "^5.0.1",
|
||||
"final-form": "^4.8.1",
|
||||
"farce": "^0.2.6",
|
||||
"final-form": "4.11.0",
|
||||
"flat": "^4.1.0",
|
||||
"fluent-intl-polyfill": "^0.1.0",
|
||||
"fluent-langneg": "^0.1.0",
|
||||
"fluent-react": "^0.8.3",
|
||||
"fluent-langneg": "^0.1.1",
|
||||
"fluent-react": "^0.8.4",
|
||||
"fork-ts-checker-webpack-plugin": "^1.5.0",
|
||||
"found": "^0.4.0-alpha.17",
|
||||
"found-relay": "^0.4.0-alpha.8",
|
||||
"graphql-schema-linter": "^0.2.0",
|
||||
"graphql-schema-typescript": "^1.2.9",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-cli": "^2.0.1",
|
||||
"gulp-sourcemaps": "^2.6.4",
|
||||
"gulp-typescript": "^5.0.0-alpha.3",
|
||||
"gulp-cli": "^2.2.0",
|
||||
"gulp-sourcemaps": "^2.6.5",
|
||||
"gulp-typescript": "^5.0.1",
|
||||
"html-webpack-plugin": "^4.0.0-beta.5",
|
||||
"husky": "^1.1.0",
|
||||
"intersection-observer": "^0.5.1",
|
||||
"jest": "^23.4.1",
|
||||
"jest-junit": "^5.1.0",
|
||||
"jest-localstorage-mock": "^2.2.0",
|
||||
"jest-mock-console": "^0.4.0",
|
||||
"lint-staged": "^7.3.0",
|
||||
"loader-utils": "^1.1.0",
|
||||
"lodash-es": "^4.17.11",
|
||||
"marked": "^0.6.0",
|
||||
"husky": "^2.2.0",
|
||||
"intersection-observer": "^0.6.0",
|
||||
"jest": "^24.8.0",
|
||||
"jest-junit": "^6.4.0",
|
||||
"jest-localstorage-mock": "^2.4.0",
|
||||
"jest-mock-console": "^1.0.0",
|
||||
"lint-staged": "^8.1.6",
|
||||
"loader-utils": "^1.2.3",
|
||||
"lodash-es": "^4.17.14",
|
||||
"marked": "^0.7.0",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"mini-css-extract-plugin": "^0.5.0",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"mini-css-extract-plugin": "^0.6.0",
|
||||
"object-diff": "0.0.4",
|
||||
"postcss-advanced-variables": "^3.0.0",
|
||||
"postcss-css-variables": "^0.11.0",
|
||||
"postcss-flexbugs-fixes": "^4.1.0",
|
||||
"postcss-font-magician": "^2.2.1",
|
||||
"postcss-import": "^11.1.0",
|
||||
"postcss-js": "^2.0.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-mixins": "^6.2.1",
|
||||
"postcss-nested": "^4.1.1",
|
||||
"postcss-prepend-imports": "^1.0.1",
|
||||
"postcss-preset-env": "^6.5.0",
|
||||
"prettier": "^1.16.4",
|
||||
"prettier": "^1.18.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"pstree.remy": "^1.1.0",
|
||||
"pstree.remy": "^1.1.6",
|
||||
"pym.js": "^1.3.2",
|
||||
"raw-loader": "^0.5.1",
|
||||
"react": "^16.8.4",
|
||||
"react": "^16.9.0-alpha.0",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-dev-utils": "6.0.0-next.3e165448",
|
||||
"react-dom": "^16.8.4",
|
||||
"react-final-form": "^3.6.4",
|
||||
"react-dev-utils": "^9.0.0",
|
||||
"react-dom": "^16.9.0-alpha.0",
|
||||
"react-error-overlay": "^5.1.6",
|
||||
"react-final-form": "4.0.2",
|
||||
"react-popper": "^1.3.2",
|
||||
"react-relay": "^1.7.0-rc.1",
|
||||
"react-responsive": "^5.0.0",
|
||||
"react-test-renderer": "^16.8.4",
|
||||
"react-relay": "^4.0.0",
|
||||
"react-relay-network-modern": "^4.0.4",
|
||||
"react-responsive": "^7.0.0",
|
||||
"react-test-renderer": "^16.9.0-alpha.0",
|
||||
"react-timeago": "^4.1.9",
|
||||
"react-transition-group": "^2.5.0",
|
||||
"react-transition-group": "^2.9.0",
|
||||
"react-with-state-props": "^2.0.4",
|
||||
"recompose": "^0.27.1",
|
||||
"relay-compiler": "^1.7.0-rc.1",
|
||||
"relay-compiler-language-typescript": "^1.1.0",
|
||||
"relay-local-schema": "^0.7.0",
|
||||
"relay-runtime": "^1.7.0-rc.1",
|
||||
"sane": "^4.0.2",
|
||||
"recompose": "0.27.1",
|
||||
"relay-compiler": "^4.0.0",
|
||||
"relay-compiler-language-typescript": "^4.1.0",
|
||||
"relay-local-schema": "^0.8.0",
|
||||
"relay-runtime": "^4.0.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sane": "^4.1.0",
|
||||
"shallow-equals": "^1.0.0",
|
||||
"simplemde": "^1.11.2",
|
||||
"simulant": "^0.2.2",
|
||||
"sinon": "^6.1.5",
|
||||
"sinon": "^7.3.2",
|
||||
"sockjs-client": "^1.3.0",
|
||||
"strip-ansi": "^5.2.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"terser-webpack-plugin": "^1.1.0",
|
||||
"timekeeper": "^2.1.2",
|
||||
"ts-jest": "^23.0.0",
|
||||
"ts-loader": "^4.4.2",
|
||||
"ts-node": "^6.2.0",
|
||||
"tsconfig-paths": "^3.4.2",
|
||||
"tsconfig-paths-webpack-plugin": "^3.1.4",
|
||||
"tslint": "^5.15.0",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"tslint-loader": "^3.6.0",
|
||||
"tslint-plugin-prettier": "^2.0.1",
|
||||
"tslint-react": "^4.0.0",
|
||||
"typed-css-modules": "^0.3.4",
|
||||
"typeface-manuale": "0.0.54",
|
||||
"typeface-source-sans-pro": "0.0.54",
|
||||
"subscriptions-transport-ws": "^0.9.16",
|
||||
"terser-webpack-plugin": "^1.2.3",
|
||||
"thread-loader": "^2.1.2",
|
||||
"timekeeper": "^2.2.0",
|
||||
"ts-jest": "<23.10.0",
|
||||
"ts-loader": "^6.0.0",
|
||||
"ts-node": "^8.1.0",
|
||||
"ts-node-dev": "^1.0.0-pre.37",
|
||||
"tsconfig-paths": "^3.8.0",
|
||||
"tsconfig-paths-webpack-plugin": "^3.2.0",
|
||||
"tslint": "^5.20.0",
|
||||
"typed-css-modules": "^0.4.2",
|
||||
"typeface-manuale": "^0.0.71",
|
||||
"typeface-source-sans-pro": "^0.0.54",
|
||||
"typescript": "3.3.4000",
|
||||
"typescript-snapshots-plugin": "^1.2.0",
|
||||
"wait-for-expect": "^1.1.0",
|
||||
"webpack": "^4.27.1",
|
||||
"typescript-snapshots-plugin": "^1.6.0",
|
||||
"wait-for-expect": "^1.1.1",
|
||||
"webpack": "^4.30.0",
|
||||
"webpack-assets-manifest": "^3.1.1",
|
||||
"webpack-bundle-analyzer": "^3.0.3",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.1.14",
|
||||
"webpack-hot-client": "^4.1.1",
|
||||
"whatwg-fetch": "^2.0.4"
|
||||
"webpack-bundle-analyzer": "^3.3.2",
|
||||
"webpack-cli": "^3.3.2",
|
||||
"webpack-dev-server": "3.2.1",
|
||||
"whatwg-fetch": "^3.0.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
"pre-commit": "FAST_LINT=true lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{j,t}s{,x}": [
|
||||
"tslint"
|
||||
"eslint"
|
||||
],
|
||||
"src/core/server/graph/tenant/schema/schema.graphql": [
|
||||
"graphql-schema-linter"
|
||||
],
|
||||
"{src/core/client/stream/events.ts,scripts/generateEventDocs.ts,events.md}": [
|
||||
"npm run docs:events -- --verify"
|
||||
]
|
||||
},
|
||||
"bundlesize": [
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* This file is taken from create-react-app.
|
||||
* We disabled tty support, removed unessential parts to us and
|
||||
* added support for `warningsFilter` and Multi Webpack Config.
|
||||
*
|
||||
* TODO: (cvle) The `forkTsCheckerWebpackPlugin` hooks are not working atm.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const address = require("address");
|
||||
const url = require("url");
|
||||
const chalk = require("chalk");
|
||||
const clearConsole = require("react-dev-utils/clearConsole");
|
||||
const formatWebpackMessages = require("react-dev-utils/formatWebpackMessages");
|
||||
const typescriptFormatter = require("react-dev-utils/typescriptFormatter");
|
||||
const forkTsCheckerWebpackPlugin = require("react-dev-utils/ForkTsCheckerWebpackPlugin");
|
||||
|
||||
// (cvle): Changed to false as we are sharing the tty with other processes.
|
||||
// const isInteractive = process.stdout.isTTY && false;
|
||||
const isInteractive = false;
|
||||
|
||||
function prepareUrls(protocol, host, port) {
|
||||
const formatUrl = hostname =>
|
||||
url.format({
|
||||
protocol,
|
||||
hostname,
|
||||
port,
|
||||
pathname: "/",
|
||||
});
|
||||
const prettyPrintUrl = hostname =>
|
||||
url.format({
|
||||
protocol,
|
||||
hostname,
|
||||
port: chalk.bold(port),
|
||||
pathname: "/",
|
||||
});
|
||||
|
||||
const isUnspecifiedHost = host === "0.0.0.0" || host === "::";
|
||||
let prettyHost;
|
||||
let lanUrlForConfig;
|
||||
let lanUrlForTerminal;
|
||||
if (isUnspecifiedHost) {
|
||||
prettyHost = "localhost";
|
||||
try {
|
||||
// This can only return an IPv4 address
|
||||
lanUrlForConfig = address.ip();
|
||||
if (lanUrlForConfig) {
|
||||
// Check if the address is a private ip
|
||||
// https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
|
||||
if (
|
||||
/^10[.]|^172[.](1[6-9]|2[0-9]|3[0-1])[.]|^192[.]168[.]/.test(
|
||||
lanUrlForConfig
|
||||
)
|
||||
) {
|
||||
// Address is private, format it for later use
|
||||
lanUrlForTerminal = prettyPrintUrl(lanUrlForConfig);
|
||||
} else {
|
||||
// Address is not private, so we will discard it
|
||||
lanUrlForConfig = undefined;
|
||||
}
|
||||
}
|
||||
} catch (_e) {
|
||||
// ignored
|
||||
}
|
||||
} else {
|
||||
prettyHost = host;
|
||||
}
|
||||
const localUrlForTerminal = prettyPrintUrl(prettyHost);
|
||||
const localUrlForBrowser = formatUrl(prettyHost);
|
||||
return {
|
||||
lanUrlForConfig,
|
||||
lanUrlForTerminal,
|
||||
localUrlForTerminal,
|
||||
localUrlForBrowser,
|
||||
};
|
||||
}
|
||||
|
||||
function printInstructions(appName, urls, useYarn) {
|
||||
console.log();
|
||||
console.log(`You can now view ${chalk.bold(appName)} in the browser.`);
|
||||
console.log();
|
||||
|
||||
if (urls.lanUrlForTerminal) {
|
||||
console.log(
|
||||
` ${chalk.bold("Local:")} ${urls.localUrlForTerminal}`
|
||||
);
|
||||
console.log(
|
||||
` ${chalk.bold("On Your Network:")} ${urls.lanUrlForTerminal}`
|
||||
);
|
||||
} else {
|
||||
console.log(` ${urls.localUrlForTerminal}`);
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log("Note that the development build is not optimized.");
|
||||
console.log(
|
||||
`To create a production build, use ` +
|
||||
`${chalk.cyan(`${useYarn ? "yarn" : "npm run"} build`)}.`
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
function createCompiler({
|
||||
appName,
|
||||
config,
|
||||
devSocket,
|
||||
urls,
|
||||
useYarn,
|
||||
useTypeScript,
|
||||
webpack,
|
||||
}) {
|
||||
// "Compiler" is a low-level interface to Webpack.
|
||||
// It lets us listen to some events and provide our own custom messages.
|
||||
let compiler;
|
||||
try {
|
||||
compiler = webpack(config);
|
||||
} catch (err) {
|
||||
console.log(chalk.red("Failed to compile."));
|
||||
console.log();
|
||||
console.log(err.message || err);
|
||||
console.log();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// "invalid" event fires when you have changed a file, and Webpack is
|
||||
// recompiling a bundle. WebpackDevServer takes care to pause serving the
|
||||
// bundle, so if you refresh, it'll wait instead of serving the old one.
|
||||
// "invalid" is short for "bundle invalidated", it doesn't imply any errors.
|
||||
compiler.hooks.invalid.tap("invalid", () => {
|
||||
if (isInteractive) {
|
||||
clearConsole();
|
||||
}
|
||||
console.log("Compiling...");
|
||||
});
|
||||
|
||||
let isFirstCompile = true;
|
||||
const tsMessagesPromises = [];
|
||||
|
||||
if (useTypeScript) {
|
||||
compiler.compilers.forEach(singleCompiler => {
|
||||
let tsMessagesPromise;
|
||||
let tsMessagesResolver;
|
||||
singleCompiler.hooks.beforeCompile.tap("beforeCompile", () => {
|
||||
tsMessagesPromise = new Promise(resolve => {
|
||||
tsMessagesResolver = msgs => resolve(msgs);
|
||||
});
|
||||
tsMessagesPromises.push(tsMessagesPromise);
|
||||
});
|
||||
|
||||
const tsCheckerHooks = forkTsCheckerWebpackPlugin.getCompilerHooks(
|
||||
singleCompiler
|
||||
);
|
||||
|
||||
// now access hooks just like before
|
||||
// TODO: (cvle) this doesn't work currently.
|
||||
tsCheckerHooks.waiting.tap("yourListenerName", () => {
|
||||
console.log("waiting for typecheck results");
|
||||
});
|
||||
|
||||
// TODO: (cvle) this doesn't work currently.
|
||||
tsCheckerHooks.receive.tap(
|
||||
"afterTypeScriptCheck",
|
||||
(diagnostics, lints) => {
|
||||
console.log("RECEIVED");
|
||||
const allMsgs = [...diagnostics, ...lints];
|
||||
const format = message =>
|
||||
`${message.file}\n${typescriptFormatter(message, true)}`;
|
||||
|
||||
tsMessagesResolver({
|
||||
errors: allMsgs.filter(msg => msg.severity === "error").map(format),
|
||||
warnings: allMsgs
|
||||
.filter(msg => msg.severity === "warning")
|
||||
.map(format),
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// "done" event fires when Webpack has finished recompiling the bundle.
|
||||
// Whether or not you have warnings or errors, you will get this event.
|
||||
compiler.hooks.done.tap("done", async stats => {
|
||||
if (isInteractive) {
|
||||
clearConsole();
|
||||
}
|
||||
|
||||
// We have switched off the default Webpack output in WebpackDevServer
|
||||
// options so we are going to "massage" the warnings and errors and present
|
||||
// them in a readable focused way.
|
||||
// We only construct the warnings and errors for speed:
|
||||
// https://github.com/facebook/create-react-app/issues/4492#issuecomment-421959548
|
||||
const statOptions = {
|
||||
all: false,
|
||||
warnings: true,
|
||||
errors: true,
|
||||
...config.stats,
|
||||
};
|
||||
if (Array.isArray(config)) {
|
||||
statOptions.children = config.map(c => c.stats || {});
|
||||
}
|
||||
const statsData = stats.toJson(statOptions);
|
||||
|
||||
if (useTypeScript && statsData.errors.length === 0) {
|
||||
const delayedMsg = setTimeout(() => {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
"Files successfully emitted, waiting for typecheck results..."
|
||||
)
|
||||
);
|
||||
}, 100);
|
||||
|
||||
// Get back all tscheck results.
|
||||
const results = await Promise.all(tsMessagesPromises);
|
||||
clearTimeout(delayedMsg);
|
||||
|
||||
results.forEach(msgs => {
|
||||
statsData.errors.push(...msgs.errors);
|
||||
statsData.warnings.push(...msgs.warnings);
|
||||
|
||||
// Push errors and warnings into compilation result
|
||||
// to show them after page refresh triggered by user.
|
||||
stats.compilation.errors.push(...msgs.errors);
|
||||
stats.compilation.warnings.push(...msgs.warnings);
|
||||
|
||||
if (msgs.errors.length > 0) {
|
||||
devSocket.errors(msgs.errors);
|
||||
} else if (msgs.warnings.length > 0) {
|
||||
devSocket.warnings(msgs.warnings);
|
||||
}
|
||||
});
|
||||
|
||||
if (isInteractive) {
|
||||
clearConsole();
|
||||
}
|
||||
}
|
||||
|
||||
const messages = formatWebpackMessages(statsData);
|
||||
const isSuccessful = !messages.errors.length && !messages.warnings.length;
|
||||
if (isSuccessful) {
|
||||
console.log(chalk.green("Compiled successfully!"));
|
||||
}
|
||||
if (isSuccessful && (isInteractive || isFirstCompile)) {
|
||||
printInstructions(appName, urls, useYarn);
|
||||
}
|
||||
isFirstCompile = false;
|
||||
|
||||
// If errors exist, only show errors.
|
||||
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;
|
||||
}
|
||||
console.log(chalk.red("Failed to compile.\n"));
|
||||
console.log(messages.errors.join("\n\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Show warnings if no errors were found.
|
||||
if (messages.warnings.length) {
|
||||
console.log(chalk.yellow("Compiled with warnings.\n"));
|
||||
console.log(messages.warnings.join("\n\n"));
|
||||
|
||||
// Teach some ESLint tricks.
|
||||
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"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return compiler;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createCompiler,
|
||||
prepareUrls,
|
||||
};
|
||||
+16
-8
@@ -17,14 +17,16 @@ import paths from "../config/paths";
|
||||
import config from "../src/core/build/config";
|
||||
import createWebpackConfig from "../src/core/build/createWebpackConfig";
|
||||
|
||||
// tslint:disable: no-console
|
||||
|
||||
// Enforce environment to be production.
|
||||
config.validate().set("env", "production");
|
||||
/* eslint-disable no-console */
|
||||
|
||||
process.env.WEBPACK = "true";
|
||||
process.env.BABEL_ENV = "production";
|
||||
process.env.NODE_ENV = "production";
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || "production";
|
||||
process.env.BABEL_ENV = process.env.NODE_ENV;
|
||||
|
||||
// Enforce environment to be NODE_ENV.
|
||||
config.validate().set("env", process.env.NODE_ENV);
|
||||
|
||||
const isProduction = 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
|
||||
@@ -102,7 +104,11 @@ measureFileSizesBeforeBuild(paths.appDistStatic)
|
||||
|
||||
// Create the production build and print the deployment instructions.
|
||||
function build(previousFileSizes: any) {
|
||||
console.log("Creating an optimized production build...");
|
||||
if (isProduction) {
|
||||
console.log("Creating an optimized production build...");
|
||||
} else {
|
||||
console.log("Creating development build...");
|
||||
}
|
||||
const webpackConfig = createWebpackConfig(config);
|
||||
const compiler = webpack(webpackConfig);
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -110,7 +116,9 @@ function build(previousFileSizes: any) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
const messages = formatWebpackMessages(stats.toJson({}));
|
||||
const messages = formatWebpackMessages(
|
||||
stats.toJson({ children: webpackConfig.map(c => c.stats || {}) as any })
|
||||
);
|
||||
if (messages.errors.length) {
|
||||
// Only keep the first error. Others are often indicative
|
||||
// of the same problem, but confuse the reader with noise.
|
||||
|
||||
+31
-6
@@ -2,7 +2,7 @@
|
||||
|
||||
import program from "commander";
|
||||
import spawn from "cross-spawn";
|
||||
import fs from "fs";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
|
||||
const config = JSON.parse(
|
||||
@@ -14,33 +14,34 @@ program
|
||||
.usage("--src ./src/core/client/stream --schema tenant")
|
||||
.option("--src <folder>", "Find gql recursively in this folder")
|
||||
.option("--schema <schema>", "Identifier of schema")
|
||||
.option("--persist", "Use persisted queries")
|
||||
.description("Compile relay gql data")
|
||||
.parse(process.argv);
|
||||
|
||||
if (!program.schema) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Schema identifier not provided");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!program.src) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Src not provided");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!config.projects) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Missing projects key in .graphqconfig");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!config.projects[program.schema]) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Project ${program.schema} not found in .graphqconfig`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!config.projects[program.schema].schemaPath) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`SchemaPath for project ${program.schema} not found in .graphqconfig`
|
||||
);
|
||||
@@ -59,4 +60,28 @@ const args = [
|
||||
config.projects[program.schema].schemaPath,
|
||||
];
|
||||
|
||||
// Set the persisted query path.
|
||||
const persist = program.persist
|
||||
? `${program.src}/persisted-queries.json`
|
||||
: null;
|
||||
if (persist) {
|
||||
args.push("--persist-output", persist);
|
||||
}
|
||||
|
||||
spawn.sync("relay-compiler", args, { stdio: "inherit" });
|
||||
|
||||
if (persist) {
|
||||
if (fs.existsSync(persist)) {
|
||||
// Create the new filename.
|
||||
const name = path.basename(program.src);
|
||||
const generated = "./src/core/server/graph/common/persisted/__generated__";
|
||||
|
||||
// Create the generated directory if it doesn't exist.
|
||||
fs.ensureDirSync(generated);
|
||||
|
||||
// Copy the file over to the destination directory.
|
||||
fs.copySync(persist, `${generated}/${name}.json`, {
|
||||
overwrite: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
-9
@@ -12,7 +12,7 @@ deploy_tag() {
|
||||
# v5.0.0-beta.1 will be tagged with 5.0.0-beta.1
|
||||
# v5.0.0 will be tagged with 5, 5.0, 5.0.0
|
||||
#
|
||||
if [ -n "$(echo $CIRCLE_TAG | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$')" ]
|
||||
if [[ -n "$(echo $CIRCLE_TAG | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$')" ]]
|
||||
then
|
||||
major=$(echo ${CIRCLE_TAG/#v} | cut -d. -f1)
|
||||
minor=$(echo ${CIRCLE_TAG/#v} | cut -d. -f2)
|
||||
@@ -40,14 +40,6 @@ deploy_tag() {
|
||||
echo "==> pushing $version"
|
||||
docker push coralproject/talk:$version
|
||||
done
|
||||
|
||||
# Deploy latest if we're at master, or deploy this branch if we aren't.
|
||||
if [ "$CIRCLE_BRANCH" = "master" ]
|
||||
then
|
||||
deploy_latest
|
||||
else
|
||||
deploy_branch
|
||||
fi
|
||||
}
|
||||
|
||||
deploy_latest() {
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
import { codeBlock, stripIndent } from "common-tags";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import ts from "typescript";
|
||||
|
||||
interface DocEntry {
|
||||
name: string;
|
||||
docs?: string;
|
||||
type: "ViewerNetworkEvent" | "ViewerEvent";
|
||||
text?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* We use this regexp to find a previous block that we
|
||||
* are going to update in the readme file.
|
||||
*/
|
||||
const BLOCK_REGEXP = /<!-- START docs:events -->(.|\n)*<!-- END docs:events -->/gm;
|
||||
|
||||
/** Build flags that affects AST generation */
|
||||
const buildFlags =
|
||||
// Do not truncate output.
|
||||
ts.NodeBuilderFlags.NoTruncation |
|
||||
// Use multiline object literals format.
|
||||
ts.NodeBuilderFlags.MultilineObjectLiterals;
|
||||
|
||||
/** Generate documentation for all classes in a set of .ts files */
|
||||
function gatherEntries(
|
||||
fileNames: string[],
|
||||
options: ts.CompilerOptions
|
||||
): DocEntry[] {
|
||||
// Build a program using the set of root file names in fileNames
|
||||
const program = ts.createProgram(fileNames, options);
|
||||
|
||||
const printer = ts.createPrinter({
|
||||
noEmitHelpers: true,
|
||||
omitTrailingSemicolon: true,
|
||||
removeComments: false,
|
||||
});
|
||||
|
||||
// Get the checker, we will use it to find more about classes
|
||||
const checker = program.getTypeChecker();
|
||||
|
||||
const data: DocEntry[] = [];
|
||||
|
||||
/** Hold a pointer to the sourcefile we are currently processing. */
|
||||
let currentSourceFile: ts.SourceFile;
|
||||
|
||||
// Visit every sourceFile in the program
|
||||
for (const sourceFile of program.getSourceFiles()) {
|
||||
if (!sourceFile.isDeclarationFile) {
|
||||
currentSourceFile = sourceFile;
|
||||
// Walk the tree to search for classes
|
||||
ts.forEachChild(sourceFile, visit);
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = data.sort((a, b) => {
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
if (b.name > a.name) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return sorted;
|
||||
|
||||
/** visit nodes finding exported events */
|
||||
function visit(node: ts.Node) {
|
||||
// Only consider exported nodes
|
||||
if (!isNodeExported(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ts.isVariableStatement(node)) {
|
||||
if (
|
||||
!node.getFullText().includes("createViewerNetworkEvent") &&
|
||||
!node.getFullText().includes("createViewerEvent")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const firstChild = node.declarationList.declarations[0];
|
||||
if (ts.isVariableDeclaration(firstChild)) {
|
||||
const symbol = checker.getSymbolAtLocation(firstChild.name);
|
||||
if (symbol) {
|
||||
serializeEventSymbol(symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function serializeEventSymbol(symbol: ts.Symbol) {
|
||||
const type = checker.getTypeOfSymbolAtLocation(
|
||||
symbol,
|
||||
symbol.valueDeclaration
|
||||
);
|
||||
const typeNode = checker.typeToTypeNode(type, undefined, buildFlags)!;
|
||||
const typeName = symbol.getName();
|
||||
const entry: DocEntry = {
|
||||
name: typeName,
|
||||
docs: ts.displayPartsToString(symbol.getDocumentationComment(checker)),
|
||||
type: type.getSymbol()!.getName() as DocEntry["type"],
|
||||
};
|
||||
typeNode.forEachChild(ch => {
|
||||
if (ts.isTypeLiteralNode(ch)) {
|
||||
const text = printer.printNode(
|
||||
ts.EmitHint.Unspecified,
|
||||
ch,
|
||||
currentSourceFile
|
||||
);
|
||||
if (text !== "{}") {
|
||||
entry.text = text;
|
||||
}
|
||||
/*
|
||||
Go through each parameter.
|
||||
ch.members.forEach(m => {
|
||||
if (ts.isPropertySignature(m)) {
|
||||
if (ts.isIdentifier(m.name)) {
|
||||
data.parameters[m.name.text] = printer.printNode(
|
||||
ts.EmitHint.Unspecified,
|
||||
m.type!,
|
||||
currentSourceFile
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
}
|
||||
});
|
||||
data.push(entry);
|
||||
}
|
||||
|
||||
/** True if this is visible outside this file, false otherwise */
|
||||
function isNodeExported(node: ts.Node): boolean {
|
||||
return (
|
||||
// eslint-disable-next-line no-bitwise, @typescript-eslint/no-unnecessary-type-assertion
|
||||
(ts.getCombinedModifierFlags(node as ts.Declaration) &
|
||||
ts.ModifierFlags.Export) !==
|
||||
0 ||
|
||||
(!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function prefixLines(text: string, prefix: string) {
|
||||
return text.split("\n").join(`\n${prefix}`);
|
||||
}
|
||||
|
||||
function getEventName(typeName: string) {
|
||||
return (
|
||||
typeName[0].toLocaleLowerCase() +
|
||||
typeName.slice(1, typeName.length - "Event".length)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes "%future added value" from text. This is a placeholder type
|
||||
* added by Relay to help with future proofness.
|
||||
*/
|
||||
function removeFutureAddedValue(text: string) {
|
||||
return text
|
||||
.replace(': "%future added value" | ', ": ")
|
||||
.replace(' | "%future added value"', "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Append or update previous documention in markdownFile.
|
||||
*
|
||||
* @param markdownFile The markdown file we want to inject the docs too.
|
||||
* @param entries data as returned by gatherEntries.
|
||||
*/
|
||||
function emitDocs(markdownFile: string, entries: DocEntry[], verify = false) {
|
||||
const previousContent = fs.existsSync(markdownFile)
|
||||
? fs.readFileSync(markdownFile).toString()
|
||||
: "";
|
||||
const summary = stripIndent`
|
||||
- ${entries
|
||||
.map(
|
||||
e => `<a href="#${getEventName(e.name)}">${getEventName(e.name)}</a>`
|
||||
)
|
||||
.join("\n - ")}
|
||||
`;
|
||||
const list = entries
|
||||
.map(
|
||||
e =>
|
||||
codeBlock`
|
||||
- ${
|
||||
e.type === "ViewerEvent"
|
||||
? `<a id="${getEventName(e.name)}">**${getEventName(e.name)}**</a>`
|
||||
: `<a id="${getEventName(e.name)}">**${getEventName(
|
||||
e.name
|
||||
)}.success**, **${getEventName(e.name)}.error**</a>`
|
||||
}: ${e.docs ? e.docs.replace("\n", " ") : ""}
|
||||
${
|
||||
e.text
|
||||
? codeBlock`
|
||||
\`\`\`ts
|
||||
${removeFutureAddedValue(e.text)}
|
||||
\`\`\`
|
||||
`
|
||||
: ""
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const output = stripIndent`
|
||||
<!-- START docs:events -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN npm run docs:events -->
|
||||
### Index
|
||||
${prefixLines(summary, " ")}
|
||||
|
||||
### Events
|
||||
${prefixLines(list, " ")}
|
||||
<!-- END docs:events -->
|
||||
`;
|
||||
|
||||
let newContent;
|
||||
// Find previous block.
|
||||
if (BLOCK_REGEXP.test(previousContent)) {
|
||||
newContent = previousContent.replace(BLOCK_REGEXP, output);
|
||||
} else {
|
||||
newContent = previousContent + "\n" + output;
|
||||
}
|
||||
if (previousContent === newContent) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`${markdownFile} is up to date`);
|
||||
return;
|
||||
}
|
||||
if (verify) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`${markdownFile} is outdated, please run \`npm run docs:events\``
|
||||
);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(markdownFile, newContent);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Successfully injected documentation into ${markdownFile}`);
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (process.argv.length < 4) {
|
||||
throw new Error("Must provide path to events and a markdown file.");
|
||||
}
|
||||
|
||||
const eventFile = process.argv[2];
|
||||
const markdownFile = process.argv[3];
|
||||
|
||||
// Find tsconfig file.
|
||||
const configFile = ts.findConfigFile(eventFile, fs.existsSync);
|
||||
if (!configFile) {
|
||||
throw new Error("tsconfig file not found");
|
||||
}
|
||||
const configText = fs.readFileSync(configFile).toString();
|
||||
const result = ts.parseConfigFileTextToJson(configFile, configText);
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
// Parse the JSON raw data into actual consumable compiler options.
|
||||
const config = ts.parseJsonConfigFileContent(
|
||||
result.config,
|
||||
ts.sys,
|
||||
path.dirname(configFile)
|
||||
);
|
||||
|
||||
const entries = gatherEntries([eventFile], config.options);
|
||||
emitDocs(markdownFile, entries, process.argv[4] === "--verify");
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,4 +1,3 @@
|
||||
const { Linter, Configuration } = require("tslint");
|
||||
const { generateTSTypesAsString } = require("graphql-schema-typescript");
|
||||
const { getGraphQLConfig } = require("graphql-config");
|
||||
const path = require("path");
|
||||
@@ -18,8 +17,8 @@ async function main() {
|
||||
config: {
|
||||
contextType: "TenantContext",
|
||||
importStatements: [
|
||||
'import TenantContext from "talk-server/graph/tenant/context";',
|
||||
'import { Cursor } from "talk-server/models/helpers/connection";',
|
||||
'import TenantContext from "coral-server/graph/tenant/context";',
|
||||
'import { Cursor } from "coral-server/models/helpers";',
|
||||
],
|
||||
customScalarType: { Cursor: "Cursor", Time: "Date" },
|
||||
},
|
||||
@@ -69,12 +68,12 @@ if (require.main === module) {
|
||||
main()
|
||||
.then(files => {
|
||||
for (const { fileName } of files) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Generated ${fileName}`);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env ts-node
|
||||
|
||||
/**
|
||||
* This script can be invoked via:
|
||||
*
|
||||
* npm run migration:create <migration name>
|
||||
*
|
||||
* To create new database migrations.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import fs from "fs-extra";
|
||||
import lodash from "lodash";
|
||||
import path from "path";
|
||||
|
||||
const templateFilePath = path.resolve(
|
||||
path.join(
|
||||
__dirname,
|
||||
"../../src/core/server/services/migrate/migration_sample.ts"
|
||||
)
|
||||
);
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
if (argv.length !== 1) {
|
||||
console.error("usage: npm run migration:create <migration name>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get the name of the new migration.
|
||||
const name = lodash.snakeCase(argv[0]);
|
||||
|
||||
// Get the version of the new migration.
|
||||
const version = Date.now();
|
||||
|
||||
// Get the filePath of the new migration.
|
||||
const filePath = path.resolve(
|
||||
path.join(
|
||||
__dirname,
|
||||
`../../src/core/server/services/migrate/migrations/${version}_${name}.ts`
|
||||
)
|
||||
);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
console.error(`migration already exists at: ${filePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Write the template out to the file.
|
||||
fs.copyFileSync(templateFilePath, filePath);
|
||||
|
||||
console.log(`created new migration at: ${filePath}`);
|
||||
+26
-7
@@ -6,18 +6,15 @@ import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
createCompiler,
|
||||
prepareUrls,
|
||||
} from "react-dev-utils/WebpackDevServerUtils";
|
||||
import webpack from "webpack";
|
||||
import WebpackDevServer from "webpack-dev-server";
|
||||
import { createCompiler, prepareUrls } from "./WebpackDevServerUtils";
|
||||
|
||||
import createDevServerConfig from "../config/webpackDevServer.config";
|
||||
import config from "../src/core/build/config";
|
||||
import createWebpackConfig from "../src/core/build/createWebpackConfig";
|
||||
|
||||
// tslint:disable: no-console
|
||||
/* eslint-disable no-console */
|
||||
|
||||
// Enforce environment to be development.
|
||||
config.validate().set("env", "development");
|
||||
@@ -52,9 +49,31 @@ if (process.env.HOST) {
|
||||
}
|
||||
|
||||
const urls = prepareUrls("http", HOST, PORT);
|
||||
const webpackConfig = createWebpackConfig(config);
|
||||
const webpackConfig = createWebpackConfig(config, { watch: true });
|
||||
|
||||
const devSocket = {
|
||||
warnings: (warnings: any) =>
|
||||
(devServer as any).sockWrite(
|
||||
(devServer as any).sockets,
|
||||
"warnings",
|
||||
warnings
|
||||
),
|
||||
errors: (errors: any) =>
|
||||
(devServer as any).sockWrite((devServer as any).sockets, "errors", errors),
|
||||
};
|
||||
|
||||
// Create a webpack compiler that is configured with custom messages.
|
||||
const compiler = createCompiler(webpack, webpackConfig, "Talk", urls);
|
||||
const compiler = createCompiler({
|
||||
webpack,
|
||||
config: webpackConfig,
|
||||
appName: "Coral",
|
||||
urls,
|
||||
// useTypeScript: true, /* This is currently not working, as the ForkTsCheckerWebpackPlugin hooks are not triggering for some reason */
|
||||
useTypeScript: false, // This would
|
||||
useYarn: false,
|
||||
devSocket,
|
||||
});
|
||||
|
||||
// Serve webpack assets generated by the compiler over a web sever.
|
||||
const serverConfig = createDevServerConfig({
|
||||
allowedHost: urls.lanUrlForConfig,
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ process.on("unhandledRejection", err => {
|
||||
const paths = require("../config/paths.ts").default;
|
||||
|
||||
const jest = require("jest");
|
||||
let argv = process.argv.slice(2);
|
||||
const argv = process.argv.slice(2);
|
||||
argv.push("--config", paths.appJestConfig);
|
||||
|
||||
// Watch unless on CI or in coverage mode
|
||||
|
||||
@@ -21,8 +21,8 @@ export default class CommandExecutor implements Executor {
|
||||
private args?: ReadonlyArray<string>;
|
||||
private spawnMultiple: boolean;
|
||||
private runOnInit: boolean;
|
||||
private isRunning: boolean = false;
|
||||
private shouldRespawn: boolean = false;
|
||||
private isRunning = false;
|
||||
private shouldRespawn = false;
|
||||
private spawnProcessDebounced?: (() => void) & Cancelable;
|
||||
|
||||
constructor(cmd: string, opts: CommandExecutorOptions = {}) {
|
||||
@@ -67,7 +67,7 @@ export default class CommandExecutor implements Executor {
|
||||
child.on("close", (code: number) => {
|
||||
this.isRunning = false;
|
||||
if (code !== 0 && code !== null) {
|
||||
// tslint:disable-next-line: no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.red(`Command exited with ${code}`));
|
||||
}
|
||||
if (this.shouldRespawn) {
|
||||
|
||||
@@ -17,8 +17,8 @@ export default class LongRunningExecutor implements Executor {
|
||||
private cmd: string;
|
||||
private args?: ReadonlyArray<string>;
|
||||
private process: ChildProcess | null = null;
|
||||
private isRunning: boolean = false;
|
||||
private shouldRestart: boolean = false;
|
||||
private isRunning = false;
|
||||
private shouldRestart = false;
|
||||
private restartDebounced: (() => void) & Cancelable;
|
||||
|
||||
constructor(cmd: string, opts: LongRunningExecutorOptions = {}) {
|
||||
@@ -37,11 +37,11 @@ export default class LongRunningExecutor implements Executor {
|
||||
shell: !this.args,
|
||||
});
|
||||
|
||||
this.process!.on("exit", (code: number) => {
|
||||
this.process.on("exit", (code: number) => {
|
||||
this.isRunning = false;
|
||||
|
||||
if (code !== 0 && code !== null) {
|
||||
// tslint:disable-next-line: no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.red(`Command exited with ${code}`));
|
||||
return;
|
||||
}
|
||||
@@ -81,11 +81,13 @@ export default class LongRunningExecutor implements Executor {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
spawn.sync("kill", [
|
||||
`-${signal}`,
|
||||
this.process!.pid.toString(),
|
||||
...kids.map(p => p.PID.toString()),
|
||||
]);
|
||||
if (kids) {
|
||||
spawn.sync("kill", [
|
||||
`-${signal}`,
|
||||
this.process!.pid.toString(),
|
||||
...kids,
|
||||
]);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ function canUseWatchman(): boolean {
|
||||
try {
|
||||
execSync("watchman --version", { stdio: ["ignore"] });
|
||||
return true;
|
||||
// tslint:disable-next-line:no-empty
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
return false;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export default class SaneWatcher implements Watcher {
|
||||
// Autodetect watchman.
|
||||
if (this.watchman === undefined && canUseWatchman()) {
|
||||
this.watchman = true;
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.grey(`Watchman detected`));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,13 @@ async function run(
|
||||
throw new Error("Config file not specified");
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
let config: any = require(path.resolve(configFile));
|
||||
if (config.__esModule) {
|
||||
config = config.default;
|
||||
}
|
||||
|
||||
try {
|
||||
await watch(config, { only });
|
||||
} catch (err) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
await watch(config, { only });
|
||||
}
|
||||
|
||||
const cmd = program
|
||||
@@ -36,4 +30,8 @@ const cmd = program
|
||||
.description("Run watchers defined in <configFile>")
|
||||
.parse(process.argv);
|
||||
|
||||
run(cmd.args, cmd.opts());
|
||||
run(cmd.args, cmd.opts()).catch(err => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
declare module "pstree.remy" {
|
||||
export default function psTree(
|
||||
pid: number,
|
||||
callback: (err: Error, kids: Array<{ PID: number }>) => void
|
||||
callback: (err: Error, kids: string[]) => void
|
||||
): void;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ async function beginWatch(
|
||||
await executor.onInit();
|
||||
}
|
||||
for await (const filePath of watcher.watch(rootDir, paths, { ignore })) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.cyanBright(`Execute "${key}"`));
|
||||
executor.execute(filePath);
|
||||
}
|
||||
@@ -72,7 +72,7 @@ function filterOnly(
|
||||
}
|
||||
return pickBy(watchers, (value, key) => {
|
||||
if (resolved.indexOf(key) === -1) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.grey(`Disabled watcher "${key}"`));
|
||||
return false;
|
||||
}
|
||||
@@ -98,9 +98,13 @@ export default async function watch(config: Config, options: Options = {}) {
|
||||
}
|
||||
|
||||
for (const key of Object.keys(watchersConfigs)) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.cyanBright(`Start watcher "${key}"`));
|
||||
const watcherConfig = watchersConfigs[key];
|
||||
beginWatch(watcher, key, watcherConfig, rootDir);
|
||||
beginWatch(watcher, key, watcherConfig, rootDir).catch(err => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// This alternative WebpackDevServer combines the functionality of:
|
||||
// https://github.com/webpack/webpack-dev-server/blob/webpack-1/client/index.js
|
||||
// https://github.com/webpack/webpack/blob/webpack-1/hot/dev-server.js
|
||||
|
||||
// It only supports their simplest configuration (hot updates on same server).
|
||||
// It makes some opinionated choices on top, like adding a syntax error overlay
|
||||
// that looks similar to our console output. The error overlay is inspired by:
|
||||
// https://github.com/glenjamin/webpack-hot-middleware
|
||||
|
||||
var SockJS = require("sockjs-client");
|
||||
var stripAnsi = require("strip-ansi");
|
||||
var url = require("url");
|
||||
var launchEditorEndpoint = require("react-dev-utils/launchEditorEndpoint");
|
||||
var formatWebpackMessages = require("react-dev-utils/formatWebpackMessages");
|
||||
var ErrorOverlay = require("react-error-overlay");
|
||||
var { debounce } = require("lodash");
|
||||
|
||||
ErrorOverlay.setEditorHandler(function editorHandler(errorLocation) {
|
||||
// Keep this sync with errorOverlayMiddleware.js
|
||||
fetch(
|
||||
launchEditorEndpoint +
|
||||
"?fileName=" +
|
||||
window.encodeURIComponent(errorLocation.fileName) +
|
||||
"&lineNumber=" +
|
||||
window.encodeURIComponent(errorLocation.lineNumber || 1) +
|
||||
"&colNumber=" +
|
||||
window.encodeURIComponent(errorLocation.colNumber || 1)
|
||||
);
|
||||
});
|
||||
|
||||
// We need to keep track of if there has been a runtime error.
|
||||
// Essentially, we cannot guarantee application state was not corrupted by the
|
||||
// runtime error. To prevent confusing behavior, we forcibly reload the entire
|
||||
// application. This is handled below when we are notified of a compile (code
|
||||
// change).
|
||||
// See https://github.com/facebook/create-react-app/issues/3096
|
||||
var hadRuntimeError = false;
|
||||
ErrorOverlay.startReportingRuntimeErrors({
|
||||
onError: function() {
|
||||
hadRuntimeError = true;
|
||||
},
|
||||
filename: "/static/js/bundle.js",
|
||||
});
|
||||
|
||||
if (module.hot && typeof module.hot.dispose === "function") {
|
||||
module.hot.dispose(function() {
|
||||
// TODO: why do we need this?
|
||||
ErrorOverlay.stopReportingRuntimeErrors();
|
||||
});
|
||||
}
|
||||
|
||||
// Connect to WebpackDevServer via a socket.
|
||||
var connection = new SockJS(
|
||||
url.format({
|
||||
protocol: window.location.protocol,
|
||||
hostname: window.location.hostname,
|
||||
port: window.location.port,
|
||||
// Hardcoded in WebpackDevServer
|
||||
pathname: "/sockjs-node",
|
||||
})
|
||||
);
|
||||
|
||||
// Unlike WebpackDevServer client, we won't try to reconnect
|
||||
// to avoid spamming the console. Disconnect usually happens
|
||||
// when developer stops the server.
|
||||
connection.onclose = function() {
|
||||
if (typeof console !== "undefined" && typeof console.info === "function") {
|
||||
console.info(
|
||||
"The development server has disconnected.\nRefresh the page if necessary."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Remember some state related to hot module replacement.
|
||||
var isFirstCompilation = true;
|
||||
var mostRecentCompilationHash = null;
|
||||
var hasCompileErrors = false;
|
||||
|
||||
function clearOutdatedErrors() {
|
||||
// Clean up outdated compile errors, if any.
|
||||
if (typeof console !== "undefined" && typeof console.clear === "function") {
|
||||
if (hasCompileErrors) {
|
||||
console.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Successful compilation.
|
||||
function handleSuccess() {
|
||||
clearOutdatedErrors();
|
||||
|
||||
var isHotUpdate = !isFirstCompilation;
|
||||
isFirstCompilation = false;
|
||||
hasCompileErrors = false;
|
||||
|
||||
// Attempt to apply hot updates or reload.
|
||||
if (isHotUpdate) {
|
||||
tryApplyUpdates(function onHotUpdateSuccess() {
|
||||
// Only dismiss it when we're sure it's a hot update.
|
||||
// Otherwise it would flicker right before the reload.
|
||||
tryDismissErrorOverlay();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Compilation with warnings (e.g. ESLint).
|
||||
function handleWarnings(warnings) {
|
||||
clearOutdatedErrors();
|
||||
|
||||
var isHotUpdate = !isFirstCompilation;
|
||||
isFirstCompilation = false;
|
||||
hasCompileErrors = false;
|
||||
|
||||
// TODO: remove this workaround when we can upgrade to WebpackDevServer >= v3.3.0,
|
||||
// which includes proper `warningsFilter` support.
|
||||
warnings = warnings.filter(w => !/export .* was not found in/.test(w));
|
||||
|
||||
function printWarnings() {
|
||||
// Print warnings to the console.
|
||||
var formatted = formatWebpackMessages({
|
||||
warnings: warnings,
|
||||
errors: [],
|
||||
});
|
||||
|
||||
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
||||
for (var i = 0; i < formatted.warnings.length; i++) {
|
||||
if (i === 5) {
|
||||
console.warn(
|
||||
"There were more warnings in other files.\n" +
|
||||
"You can find a complete log in the terminal."
|
||||
);
|
||||
break;
|
||||
}
|
||||
console.warn(stripAnsi(formatted.warnings[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printWarnings();
|
||||
|
||||
// Attempt to apply hot updates or reload.
|
||||
if (isHotUpdate) {
|
||||
tryApplyUpdates(function onSuccessfulHotUpdate() {
|
||||
// Only dismiss it when we're sure it's a hot update.
|
||||
// Otherwise it would flicker right before the reload.
|
||||
tryDismissErrorOverlay();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Compilation with errors (e.g. syntax error or missing modules).
|
||||
function handleErrors(errors) {
|
||||
clearOutdatedErrors();
|
||||
|
||||
isFirstCompilation = false;
|
||||
hasCompileErrors = true;
|
||||
|
||||
// "Massage" webpack messages.
|
||||
var formatted = formatWebpackMessages({
|
||||
errors: errors,
|
||||
warnings: [],
|
||||
});
|
||||
|
||||
// Only show the first error.
|
||||
ErrorOverlay.reportBuildError(formatted.errors[0]);
|
||||
|
||||
// Also log them to the console.
|
||||
if (typeof console !== "undefined" && typeof console.error === "function") {
|
||||
for (var i = 0; i < formatted.errors.length; i++) {
|
||||
console.error(stripAnsi(formatted.errors[i]));
|
||||
}
|
||||
}
|
||||
|
||||
// Do not attempt to reload now.
|
||||
// We will reload on next success instead.
|
||||
}
|
||||
|
||||
function tryDismissErrorOverlay() {
|
||||
if (!hasCompileErrors) {
|
||||
ErrorOverlay.dismissBuildError();
|
||||
}
|
||||
}
|
||||
|
||||
// There is a newer version of the code available.
|
||||
function handleAvailableHash(hash) {
|
||||
// Update last known compilation hash.
|
||||
mostRecentCompilationHash = hash;
|
||||
}
|
||||
|
||||
const debouncedReload = debounce(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
|
||||
// Handle messages from the server.
|
||||
connection.onmessage = function(e) {
|
||||
var message = JSON.parse(e.data);
|
||||
switch (message.type) {
|
||||
case "hash":
|
||||
handleAvailableHash(message.data);
|
||||
break;
|
||||
case "still-ok":
|
||||
case "ok":
|
||||
handleSuccess();
|
||||
break;
|
||||
case "content-changed":
|
||||
// Triggered when a file from `contentBase` changed.
|
||||
debouncedReload();
|
||||
break;
|
||||
case "warnings":
|
||||
handleWarnings(message.data);
|
||||
break;
|
||||
case "errors":
|
||||
handleErrors(message.data);
|
||||
break;
|
||||
default:
|
||||
// Do nothing.
|
||||
}
|
||||
};
|
||||
|
||||
// Is there a newer version of this code available?
|
||||
function isUpdateAvailable() {
|
||||
/* globals __webpack_hash__ */
|
||||
// __webpack_hash__ is the hash of the current compilation.
|
||||
// It's a global variable injected by Webpack.
|
||||
return mostRecentCompilationHash !== __webpack_hash__;
|
||||
}
|
||||
|
||||
// Webpack disallows updates in other states.
|
||||
function canApplyUpdates() {
|
||||
return module.hot.status() === "idle";
|
||||
}
|
||||
|
||||
// Attempt to update code on the fly, fall back to a hard reload.
|
||||
function tryApplyUpdates(onHotUpdateSuccess) {
|
||||
if (!module.hot) {
|
||||
// HotModuleReplacementPlugin is not in Webpack configuration.
|
||||
debouncedReload();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isUpdateAvailable() || !canApplyUpdates()) {
|
||||
return;
|
||||
}
|
||||
|
||||
function handleApplyUpdates(err, updatedModules) {
|
||||
if (err || !updatedModules || hadRuntimeError) {
|
||||
debouncedReload();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof onHotUpdateSuccess === "function") {
|
||||
// Maybe we want to do something.
|
||||
onHotUpdateSuccess();
|
||||
}
|
||||
|
||||
if (isUpdateAvailable()) {
|
||||
// While we were updating, there was a new update! Do it again.
|
||||
tryApplyUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
// https://webpack.github.io/docs/hot-module-replacement.html#check
|
||||
var result = module.hot.check(/* autoApply */ true, handleApplyUpdates);
|
||||
|
||||
// // Webpack 2 returns a Promise instead of invoking a callback
|
||||
if (result && result.then) {
|
||||
result.then(
|
||||
function(updatedModules) {
|
||||
handleApplyUpdates(null, updatedModules);
|
||||
},
|
||||
function(err) {
|
||||
handleApplyUpdates(err, null);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import convict from "convict";
|
||||
|
||||
import { LOCALES } from "../common/helpers/i18n/locales";
|
||||
|
||||
const config = convict({
|
||||
env: {
|
||||
doc: "The application environment.",
|
||||
@@ -21,6 +23,12 @@ const config = convict({
|
||||
env: "DEV_PORT",
|
||||
arg: "dev-port",
|
||||
},
|
||||
defaultLocale: {
|
||||
doc: "Specify the default locale to use",
|
||||
format: LOCALES,
|
||||
default: "en-US",
|
||||
env: "LOCALE",
|
||||
},
|
||||
generateReport: {
|
||||
doc: "Generate a report using webpack-bundle-analyzer",
|
||||
format: Boolean,
|
||||
@@ -56,6 +64,13 @@ const config = convict({
|
||||
env: "WEBPACK_TREESHAKE",
|
||||
arg: "enableTreeShake",
|
||||
},
|
||||
maxCores: {
|
||||
doc: "Set maximum of available cores",
|
||||
format: "nat",
|
||||
default: require("os").cpus().length,
|
||||
env: "WEBPACK_MAX_CORES",
|
||||
arg: "maxCores",
|
||||
},
|
||||
});
|
||||
|
||||
export type Config = typeof config;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import OptimizeCssnanoPlugin from "@intervolga/optimize-cssnano-plugin";
|
||||
import bunyan from "bunyan";
|
||||
import CaseSensitivePathsPlugin from "case-sensitive-paths-webpack-plugin";
|
||||
import CompressionPlugin from "compression-webpack-plugin";
|
||||
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
|
||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||
import { identity } from "lodash";
|
||||
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
||||
import path from "path";
|
||||
import typescriptFormatter from "react-dev-utils/typescriptFormatter";
|
||||
import WatchMissingNodeModulesPlugin from "react-dev-utils/WatchMissingNodeModulesPlugin";
|
||||
import TerserPlugin from "terser-webpack-plugin";
|
||||
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
|
||||
@@ -12,7 +15,7 @@ import webpack, { Configuration, Plugin } from "webpack";
|
||||
import WebpackAssetsManifest from "webpack-assets-manifest";
|
||||
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
|
||||
|
||||
// TODO: import form talk-common/version, for some reason this fails currently.
|
||||
// TODO: import form coral-common/version, for some reason this fails currently.
|
||||
// Try again when we have a chance to upgrade typescript.
|
||||
import { version } from "../common/version";
|
||||
import { Config } from "./config";
|
||||
@@ -28,16 +31,26 @@ import paths from "./paths";
|
||||
const filterPlugins = (plugins: Array<Plugin | null>): Plugin[] =>
|
||||
plugins.filter(identity) as Plugin[];
|
||||
|
||||
// Create the build logger.
|
||||
const logger = bunyan.createLogger({
|
||||
name: "coral",
|
||||
level: "debug",
|
||||
});
|
||||
|
||||
interface CreateWebpackOptions {
|
||||
appendPlugins?: any[];
|
||||
watch?: boolean;
|
||||
}
|
||||
|
||||
const publicPath = "/";
|
||||
|
||||
export default function createWebpackConfig(
|
||||
config: Config,
|
||||
{ appendPlugins = [] }: CreateWebpackOptions = {}
|
||||
{ appendPlugins = [], watch = false }: CreateWebpackOptions = {}
|
||||
): Configuration[] {
|
||||
logger.debug({ config: config.toString() }, "loaded configuration");
|
||||
|
||||
const maxCores = config.get("maxCores");
|
||||
const env = createClientEnv(config);
|
||||
const disableSourcemaps = config.get("disableSourcemaps");
|
||||
const generateReport = config.get("generateReport");
|
||||
@@ -59,57 +72,58 @@ export default function createWebpackConfig(
|
||||
};
|
||||
|
||||
/**
|
||||
* ifProduction will only include the nodes if we're in production mode.
|
||||
* ifWatch will only include the nodes if we're in watch mode.
|
||||
*/
|
||||
const ifProduction = isProduction
|
||||
? (...nodes: any[]) => nodes
|
||||
: (...nodes: any[]) => [];
|
||||
const ifWatch = watch ? (...nodes: any[]) => nodes : () => [];
|
||||
|
||||
/**
|
||||
* ifNotProduction will only include the nodes if we're not in production
|
||||
* mode.
|
||||
* ifBuild will only include the nodes if we're in build mode.
|
||||
*/
|
||||
const ifNotProduction = !isProduction
|
||||
? (...nodes: any[]) => nodes
|
||||
: (...nodes: any[]) => [];
|
||||
const ifBuild = !watch ? (...nodes: any[]) => nodes : () => [];
|
||||
|
||||
const styleLoader = {
|
||||
loader: require.resolve("style-loader"),
|
||||
options: {
|
||||
hmr: !isProduction,
|
||||
sourceMap: !disableSourcemaps,
|
||||
hmr: watch,
|
||||
},
|
||||
};
|
||||
|
||||
const localesOptions = {
|
||||
pathToLocales: paths.appLocales,
|
||||
|
||||
// Default locale if non could be negotiated.
|
||||
defaultLocale: "en-US",
|
||||
// Default locale if none was specified.
|
||||
defaultLocale: config.get("defaultLocale"),
|
||||
|
||||
// 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",
|
||||
fallbackLocale: config.get("defaultLocale"),
|
||||
|
||||
// Common fluent files are always included in the locale bundles.
|
||||
commonFiles: ["framework.ftl", "common.ftl"],
|
||||
commonFiles: ["framework.ftl", "common.ftl", "ui.ftl"],
|
||||
|
||||
// Locales that come with the main bundle. Others are loaded on demand.
|
||||
bundled: ["en-US"],
|
||||
bundled: [config.get("defaultLocale")],
|
||||
|
||||
// All available locales can be loadable on demand.
|
||||
// To restrict available locales set:
|
||||
// availableLocales: ["en-US"],
|
||||
// availableLocales: [config.get("defaultLocale")],
|
||||
};
|
||||
|
||||
const additionalPlugins = isProduction
|
||||
? [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "assets/css/[name].[hash].css",
|
||||
chunkFilename: "assets/css/[id].[hash].css",
|
||||
}),
|
||||
const additionalPlugins = [
|
||||
...ifBuild(
|
||||
new MiniCssExtractPlugin({
|
||||
filename: isProduction
|
||||
? "assets/css/[name].[hash].css"
|
||||
: "assets/css/[name].css",
|
||||
chunkFilename: isProduction
|
||||
? "assets/css/[id].[hash].css"
|
||||
: "assets/css/[id].css",
|
||||
}),
|
||||
isProduction &&
|
||||
new OptimizeCssnanoPlugin({
|
||||
sourceMap: true,
|
||||
sourceMap: !disableSourcemaps,
|
||||
cssnanoOptions: {
|
||||
preset: [
|
||||
"default",
|
||||
@@ -121,26 +135,41 @@ export default function createWebpackConfig(
|
||||
],
|
||||
},
|
||||
}),
|
||||
// Pre-compress all the assets as they will be served as is.
|
||||
new CompressionPlugin({}),
|
||||
]
|
||||
: [
|
||||
// Add module names to factory functions so they appear in browser profiler.
|
||||
new webpack.NamedModulesPlugin(),
|
||||
// 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),
|
||||
];
|
||||
// Pre-compress all the assets as they will be served as is.
|
||||
new CompressionPlugin({})
|
||||
),
|
||||
...ifWatch(
|
||||
// Add module names to factory functions so they appear in browser profiler.
|
||||
new webpack.NamedModulesPlugin(),
|
||||
// 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)
|
||||
),
|
||||
];
|
||||
|
||||
const devtool = disableSourcemaps
|
||||
? false
|
||||
: isProduction
|
||||
? // We generate sourcemaps in production. This is slow but gives good results.
|
||||
// You can exclude the *.map files from the build during deployment.
|
||||
"source-map"
|
||||
: // 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.
|
||||
"cheap-module-source-map";
|
||||
|
||||
const baseConfig: Configuration = {
|
||||
stats: {
|
||||
// https://github.com/TypeStrong/ts-loader#transpileonly-boolean-defaultfalse
|
||||
// Using transpilation only without typechecks gives warnings when we reexport types.
|
||||
// We can ignore them here.
|
||||
warningsFilter: /export .* was not found in/,
|
||||
},
|
||||
// Set webpack mode.
|
||||
mode: isProduction ? "production" : "development",
|
||||
optimization: {
|
||||
@@ -182,14 +211,7 @@ export default function createWebpackConfig(
|
||||
}),
|
||||
],
|
||||
},
|
||||
devtool:
|
||||
!disableSourcemaps && isProduction
|
||||
? // We generate sourcemaps in production. This is slow but gives good results.
|
||||
// You can exclude the *.map files from the build during deployment.
|
||||
"source-map"
|
||||
: // 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.
|
||||
"cheap-module-source-map",
|
||||
devtool,
|
||||
// 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.
|
||||
@@ -201,10 +223,10 @@ export default function createWebpackConfig(
|
||||
// Generated JS file names (with nested folders).
|
||||
// There will be one main bundle, and one file per asynchronous chunk.
|
||||
filename: isProduction
|
||||
? "assets/js/[name].[chunkhash:8].js"
|
||||
? "assets/js/[name].[chunkhash].js"
|
||||
: "assets/js/[name].js",
|
||||
chunkFilename: isProduction
|
||||
? "assets/js/[name].[chunkhash:8].chunk.js"
|
||||
? "assets/js/[name].[chunkhash].chunk.js"
|
||||
: "assets/js/[name].chunk.js",
|
||||
// We inferred the "public path" (such as / or /my-project) from homepage.
|
||||
publicPath,
|
||||
@@ -229,26 +251,13 @@ export default function createWebpackConfig(
|
||||
modules: ["node_modules", paths.appLoaders],
|
||||
},
|
||||
module: {
|
||||
strictExportPresence: true,
|
||||
// https://github.com/TypeStrong/ts-loader#transpileonly-boolean-defaultfalse
|
||||
// Using transpilation only without typechecks gives warnings when we reexport types
|
||||
// thus we can't turn on `strictExportPresence` which would turn warnings into errors.
|
||||
strictExportPresence: false,
|
||||
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|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
|
||||
@@ -286,6 +295,22 @@ export default function createWebpackConfig(
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: paths.appAccountLocalesTemplate,
|
||||
use: [
|
||||
// This is the locales loader that loads available locales
|
||||
// from a particular target.
|
||||
{
|
||||
loader: "locales-loader",
|
||||
options: {
|
||||
...localesOptions,
|
||||
// Target specifies the prefix for fluent files to be loaded.
|
||||
// ${target}-xyz.ftl and ${†arget}.ftl are loaded into the locales.
|
||||
target: "account",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: paths.appAdminLocalesTemplate,
|
||||
use: [
|
||||
@@ -331,14 +356,66 @@ export default function createWebpackConfig(
|
||||
loader: require.resolve("url-loader"),
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: "assets/media/[name].[hash:8].[ext]",
|
||||
name: isProduction
|
||||
? "assets/media/[name].[hash].[ext]"
|
||||
: "assets/media/[name].[ext]",
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.css\.ts$/,
|
||||
use: [
|
||||
!watch ? MiniCssExtractPlugin.loader : styleLoader,
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
modules: true,
|
||||
importLoaders: 2,
|
||||
localIdentName: "[name]-[local]-[contenthash]",
|
||||
sourceMap: !disableSourcemaps,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("postcss-loader"),
|
||||
options: {
|
||||
config: {
|
||||
path: paths.appPostCssConfig,
|
||||
},
|
||||
parser: "postcss-js",
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("babel-loader"),
|
||||
options: {
|
||||
configFile: false,
|
||||
babelrc: false,
|
||||
presets: [
|
||||
"@babel/typescript",
|
||||
[
|
||||
"@babel/env",
|
||||
{ targets: { node: "10.0.0" }, modules: "commonjs" },
|
||||
],
|
||||
],
|
||||
// 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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// Process JS with Babel.
|
||||
{
|
||||
test: /\.(ts|tsx)$/,
|
||||
include: paths.appSrc,
|
||||
use: [
|
||||
{
|
||||
loader: "thread-loader",
|
||||
options: {
|
||||
// there should be 1 cpu for the fork-ts-checker-webpack-plugin
|
||||
workers: maxCores - 1,
|
||||
poolTimeout: watch ? Infinity : 500, // set this to Infinity in watch mode - see https://github.com/webpack-contrib/thread-loader
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve("babel-loader"),
|
||||
options: {
|
||||
@@ -349,7 +426,7 @@ export default function createWebpackConfig(
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: "ts-loader",
|
||||
loader: require.resolve("ts-loader"),
|
||||
options: {
|
||||
configFile: paths.appTsconfig,
|
||||
compilerOptions: {
|
||||
@@ -357,12 +434,14 @@ export default function createWebpackConfig(
|
||||
module: "esnext",
|
||||
jsx: "preserve",
|
||||
noEmit: false,
|
||||
sourceMap: !disableSourcemaps,
|
||||
},
|
||||
|
||||
transpileOnly: true,
|
||||
// Overwrites the behavior of `include` and `exclude` to only
|
||||
// include files that are actually being imported and which
|
||||
// are necessary to compile the bundle.
|
||||
onlyCompileBundledFiles: true,
|
||||
happyPackMode: true, // IMPORTANT! use happyPackMode mode to speed-up compilation and reduce errors reported to webpack
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -381,7 +460,7 @@ export default function createWebpackConfig(
|
||||
presets: [
|
||||
[
|
||||
"@babel/env",
|
||||
{ targets: "last 2 versions, ie 11", modules: false },
|
||||
{ targets: "last 2 versions", modules: false },
|
||||
],
|
||||
],
|
||||
cacheDirectory: true,
|
||||
@@ -392,19 +471,19 @@ export default function createWebpackConfig(
|
||||
// "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, and
|
||||
// in development "style" loader enables hot editing of CSS.
|
||||
// When building we use a plugin to extract that CSS to a file, and
|
||||
// in watch mode "style" loader enables hot editing of CSS.
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
isProduction ? MiniCssExtractPlugin.loader : styleLoader,
|
||||
!watch ? MiniCssExtractPlugin.loader : styleLoader,
|
||||
{
|
||||
loader: require.resolve("css-loader"),
|
||||
options: {
|
||||
modules: true,
|
||||
importLoaders: 1,
|
||||
localIdentName: "[name]-[local]-[hash:base64:5]",
|
||||
sourceMap: isProduction && !disableSourcemaps,
|
||||
sourceMap: !disableSourcemaps,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -413,13 +492,14 @@ export default function createWebpackConfig(
|
||||
config: {
|
||||
path: paths.appPostCssConfig,
|
||||
},
|
||||
sourceMap: !disableSourcemaps,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// "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.
|
||||
// When building, 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.
|
||||
{
|
||||
@@ -430,7 +510,14 @@ export default function createWebpackConfig(
|
||||
exclude: [/\.(js|ts|tsx)$/, /\.html$/, /\.json$/],
|
||||
loader: require.resolve("file-loader"),
|
||||
options: {
|
||||
name: "assets/media/[name].[hash:8].[ext]",
|
||||
// Because the resources loaded via CSS can sometimes be loaded
|
||||
// directly from a CSS file, this will ensure that they are
|
||||
// relative to those referencing files.
|
||||
publicPath: (loaderPublicPath: string) =>
|
||||
"../../" + loaderPublicPath,
|
||||
name: isProduction
|
||||
? "assets/media/[name].[hash:8].[ext]"
|
||||
: "assets/media/[name].[ext]",
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -440,6 +527,25 @@ export default function createWebpackConfig(
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
// TODO: (cvle) this should work in build too but for some reasons it terminates the build afterwards.
|
||||
// Preventing from running post build steps.
|
||||
...ifWatch(
|
||||
// We run eslint in a separate process to have a quicker build.
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
eslint: true,
|
||||
typescript: require.resolve("typescript"),
|
||||
async: true,
|
||||
// TODO: (cvle) For some reason if incremental build is turned on it does not find lint errors during initial build.
|
||||
useTypescriptIncrementalApi: false,
|
||||
checkSyntacticErrors: true,
|
||||
tsconfig: paths.appTsconfig,
|
||||
watch: paths.appSrc,
|
||||
// TODO: (cvle) ForkTsCheckerWebpackPlugin are currently not working, so we resort to default reporting.
|
||||
silent: false,
|
||||
// The formatter is normally invoked directly in WebpackDevServerUtils during development
|
||||
formatter: typescriptFormatter,
|
||||
})
|
||||
),
|
||||
// Makes some environment variables available to the JS code, for example:
|
||||
// if (process.env.NODE_ENV === 'development') { ... }. See `./env.js`.
|
||||
new webpack.DefinePlugin(envStringified),
|
||||
@@ -470,7 +576,7 @@ export default function createWebpackConfig(
|
||||
},
|
||||
};
|
||||
|
||||
const devServerEntries = isProduction
|
||||
const devServerEntries = !watch
|
||||
? []
|
||||
: [
|
||||
// Include an alternative client for WebpackDevServer. A client's job is to
|
||||
@@ -483,7 +589,7 @@ export default function createWebpackConfig(
|
||||
// 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"),
|
||||
paths.appWebpackHotDevClient,
|
||||
];
|
||||
|
||||
return [
|
||||
@@ -494,41 +600,52 @@ export default function createWebpackConfig(
|
||||
stream: [
|
||||
// We ship polyfills by default
|
||||
paths.appPolyfill,
|
||||
...ifProduction(paths.appPublicPath),
|
||||
...ifBuild(paths.appPublicPath),
|
||||
...devServerEntries,
|
||||
paths.appStreamIndex,
|
||||
],
|
||||
auth: [
|
||||
// We ship polyfills by default
|
||||
paths.appPolyfill,
|
||||
...ifProduction(paths.appPublicPath),
|
||||
...ifBuild(paths.appPublicPath),
|
||||
...devServerEntries,
|
||||
paths.appAuthIndex,
|
||||
// Remove deactivated entries.
|
||||
],
|
||||
authCallback: [
|
||||
...ifProduction(paths.appPublicPath),
|
||||
...ifBuild(paths.appPublicPath),
|
||||
...devServerEntries,
|
||||
paths.appAuthCallbackIndex,
|
||||
],
|
||||
install: [
|
||||
// We ship polyfills by default
|
||||
paths.appPolyfill,
|
||||
...ifProduction(paths.appPublicPath),
|
||||
...ifBuild(paths.appPublicPath),
|
||||
...devServerEntries,
|
||||
paths.appInstallIndex,
|
||||
],
|
||||
account: [
|
||||
// We ship polyfills by default
|
||||
paths.appPolyfill,
|
||||
...ifBuild(paths.appPublicPath),
|
||||
...devServerEntries,
|
||||
paths.appAccountIndex,
|
||||
],
|
||||
admin: [
|
||||
// We ship polyfills by default
|
||||
paths.appPolyfill,
|
||||
...ifProduction(paths.appPublicPath),
|
||||
...ifBuild(paths.appPublicPath),
|
||||
...devServerEntries,
|
||||
paths.appAdminIndex,
|
||||
],
|
||||
},
|
||||
plugins: filterPlugins([
|
||||
...baseConfig.plugins!,
|
||||
...ifNotProduction(
|
||||
...ifWatch(
|
||||
// This is necessary to emit hot updates
|
||||
new webpack.HotModuleReplacementPlugin({
|
||||
multiStep: true,
|
||||
}),
|
||||
// Generates an `stream.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "stream.html",
|
||||
@@ -557,6 +674,13 @@ export default function createWebpackConfig(
|
||||
chunks: ["install"],
|
||||
inject: "body",
|
||||
}),
|
||||
// Generates an `account.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "account.html",
|
||||
template: paths.appAccountHTML,
|
||||
chunks: ["account"],
|
||||
inject: "body",
|
||||
}),
|
||||
// Generates an `admin.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "admin.html",
|
||||
@@ -565,7 +689,7 @@ export default function createWebpackConfig(
|
||||
inject: "body",
|
||||
})
|
||||
),
|
||||
...ifProduction(
|
||||
...ifBuild(
|
||||
new WebpackAssetsManifest({
|
||||
output: "asset-manifest.json",
|
||||
entrypoints: true,
|
||||
@@ -590,7 +714,6 @@ export default function createWebpackConfig(
|
||||
entry: [
|
||||
/* Use minimal amount of polyfills (for IE) */
|
||||
"intersection-observer", // also for Safari
|
||||
...devServerEntries,
|
||||
paths.appEmbedIndex,
|
||||
],
|
||||
output: {
|
||||
@@ -602,7 +725,7 @@ export default function createWebpackConfig(
|
||||
},
|
||||
plugins: filterPlugins([
|
||||
...baseConfig.plugins!,
|
||||
...ifNotProduction(
|
||||
...ifWatch(
|
||||
// Generates an `embed.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "embed.html",
|
||||
@@ -620,7 +743,7 @@ export default function createWebpackConfig(
|
||||
inject: "head",
|
||||
})
|
||||
),
|
||||
...ifProduction(
|
||||
...ifBuild(
|
||||
new WebpackAssetsManifest({
|
||||
output: "embed-asset-manifest.json",
|
||||
entrypoints: true,
|
||||
@@ -629,5 +752,44 @@ export default function createWebpackConfig(
|
||||
),
|
||||
]),
|
||||
},
|
||||
/* Webpack config for count */
|
||||
{
|
||||
...baseConfig,
|
||||
optimization: {
|
||||
...baseConfig.optimization,
|
||||
// Ensure that we never split the count into chunks.
|
||||
splitChunks: {
|
||||
chunks: "async",
|
||||
},
|
||||
// We can turn on sideEffects here as we don't use
|
||||
// css here and don't run into: https://github.com/webpack/webpack/issues/7094
|
||||
sideEffects: true,
|
||||
},
|
||||
entry: [paths.appCountIndex],
|
||||
output: {
|
||||
...baseConfig.output,
|
||||
// don't hash the count, cache-busting must be completed by the requester
|
||||
// as this lives in a static template on the embed site.
|
||||
filename: "assets/js/count.js",
|
||||
},
|
||||
plugins: filterPlugins([
|
||||
...baseConfig.plugins!,
|
||||
...ifWatch(
|
||||
// Generates an `embed.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "count.html",
|
||||
template: paths.appCountHTML,
|
||||
inject: "body",
|
||||
})
|
||||
),
|
||||
...ifBuild(
|
||||
new WebpackAssetsManifest({
|
||||
output: "count-asset-manifest.json",
|
||||
entrypoints: true,
|
||||
integrity: true,
|
||||
})
|
||||
),
|
||||
]),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ function generateTarget(target, context) {
|
||||
defaultLocale,
|
||||
fallbackLocale,
|
||||
pathToLocales,
|
||||
resourcePath,
|
||||
locales,
|
||||
bundled,
|
||||
} = context;
|
||||
|
||||
@@ -34,10 +34,17 @@ export default {
|
||||
appAuthCallbackHTML: resolveSrc("core/client/auth-callback/index.html"),
|
||||
appAuthCallbackIndex: resolveSrc("core/client/auth-callback/index.ts"),
|
||||
|
||||
appCountHTML: resolveSrc("core/client/count/index.html"),
|
||||
appCountIndex: resolveSrc("core/client/count/index.ts"),
|
||||
|
||||
appInstallHTML: resolveSrc("core/client/install/index.html"),
|
||||
appInstallLocalesTemplate: resolveSrc("core/client/install/locales.ts"),
|
||||
appInstallIndex: resolveSrc("core/client/install/index.tsx"),
|
||||
|
||||
appAccountHTML: resolveSrc("core/client/account/index.html"),
|
||||
appAccountLocalesTemplate: resolveSrc("core/client/account/locales.ts"),
|
||||
appAccountIndex: resolveSrc("core/client/account/index.tsx"),
|
||||
|
||||
appAdminHTML: resolveSrc("core/client/admin/index.html"),
|
||||
appAdminLocalesTemplate: resolveSrc("core/client/admin/locales.ts"),
|
||||
appAdminIndex: resolveSrc("core/client/admin/index.tsx"),
|
||||
@@ -51,4 +58,6 @@ export default {
|
||||
appPublic: resolveApp("public"),
|
||||
appPackageJson: resolveApp("package.json"),
|
||||
appNodeModules: resolveApp("node_modules"),
|
||||
|
||||
appWebpackHotDevClient: resolveApp("scripts/webpackHotDevClient.js"),
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ const paths = require("./paths").default;
|
||||
const autoprefixer = require("autoprefixer");
|
||||
const postcssFontMagician = require("postcss-font-magician");
|
||||
const postcssFlexbugsFixes = require("postcss-flexbugs-fixes");
|
||||
const postcssVariables = require("postcss-css-variables");
|
||||
const postcssPresetEnv = require("postcss-preset-env");
|
||||
const postcssNested = require("postcss-nested");
|
||||
const postcssImport = require("postcss-import");
|
||||
@@ -24,12 +23,6 @@ const flatKebabVariables = mapKeys(
|
||||
(_, k) => kebabCase(k)
|
||||
);
|
||||
|
||||
// These are the default css standard variables.
|
||||
const cssVariables = pickBy(
|
||||
flatKebabVariables,
|
||||
(v, k) => !k.startsWith("breakpoints-")
|
||||
);
|
||||
|
||||
// These are sass style variables used in media queries.
|
||||
const mediaQueryVariables = mapValues(
|
||||
pickBy(flatKebabVariables, (v, k) => k.startsWith("breakpoints-")),
|
||||
@@ -49,7 +42,7 @@ module.exports = {
|
||||
// This allows us to define dynamic css variables.
|
||||
postcssPrependImports({
|
||||
path: "",
|
||||
files: [paths.appThemeVariablesCSS, paths.appThemeMixinsCSS],
|
||||
files: [paths.appThemeMixinsCSS],
|
||||
}),
|
||||
// Needed by above plugin.
|
||||
postcssImport(),
|
||||
@@ -59,10 +52,6 @@ module.exports = {
|
||||
postcssNested(),
|
||||
// Sass style variables to be used in media queries.
|
||||
postcssAdvancedVariables({ variables: mediaQueryVariables }),
|
||||
// CSS standard variables for everything else.
|
||||
postcssVariables({
|
||||
variables: cssVariables,
|
||||
}),
|
||||
// Provides a modern CSS environment.
|
||||
postcssPresetEnv(),
|
||||
// Does all the font handling logic.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
__webpack_public_path__ =
|
||||
JSON.parse(document.getElementById("config").innerText) || "/";
|
||||
JSON.parse(document.getElementById("config").innerText).staticURI || "/";
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
const lodashOptimizations = ["use-lodash-es", "lodash"];
|
||||
|
||||
module.exports = {
|
||||
presets: [
|
||||
["@babel/env", { targets: "last 2 versions, ie 11", modules: false }],
|
||||
"@babel/react",
|
||||
],
|
||||
presets: ["@babel/react"],
|
||||
plugins: ["@babel/syntax-dynamic-import"],
|
||||
env: {
|
||||
production: {
|
||||
presets: [["@babel/env", { targets: "last 2 versions", modules: false }]],
|
||||
plugins: [...lodashOptimizations],
|
||||
},
|
||||
development: {
|
||||
presets: [["@babel/env", { targets: "last 2 versions", modules: false }]],
|
||||
plugins: [...lodashOptimizations],
|
||||
},
|
||||
test: {
|
||||
presets: [["@babel/env", { targets: { node: "current" } }]],
|
||||
plugins: ["@babel/transform-modules-commonjs"],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
const path = require("path");
|
||||
module.exports = {
|
||||
extends: "../.babelrc.js",
|
||||
plugins: [
|
||||
[
|
||||
"babel-plugin-relay",
|
||||
{ artifactDirectory: path.resolve(__dirname, "./__generated__") },
|
||||
],
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { BrowserProtocol, queryMiddleware } from "farce";
|
||||
import { createFarceRouter, ElementsRenderer } from "found";
|
||||
import { Resolver } from "found-relay";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { CoralContextConsumer } from "coral-framework/lib/bootstrap/CoralContext";
|
||||
import TransitionControl from "coral-framework/testHelpers/TransitionControl";
|
||||
|
||||
import routeConfig from "../routeConfig";
|
||||
import NotFound from "../routes/NotFound";
|
||||
import MainLayout from "./MainLayout";
|
||||
|
||||
import "./App.css";
|
||||
|
||||
const Router = createFarceRouter({
|
||||
historyProtocol: new BrowserProtocol(),
|
||||
historyMiddlewares: [queryMiddleware],
|
||||
routeConfig,
|
||||
renderReady: function FarceRouterReady({ elements }) {
|
||||
return (
|
||||
<>
|
||||
<ElementsRenderer elements={elements} />
|
||||
{process.env.NODE_ENV === "test" && <TransitionControl />}
|
||||
</>
|
||||
);
|
||||
},
|
||||
renderError: function FarceRouterError({ error }) {
|
||||
return <div>{error.status === 404 ? <NotFound /> : "Error"}</div>;
|
||||
},
|
||||
});
|
||||
|
||||
const EntryContainer: FunctionComponent = () => (
|
||||
<CoralContextConsumer>
|
||||
{({ relayEnvironment }) => (
|
||||
<MainLayout>
|
||||
<Router resolver={new Resolver(relayEnvironment)} />
|
||||
</MainLayout>
|
||||
)}
|
||||
</CoralContextConsumer>
|
||||
);
|
||||
|
||||
export default EntryContainer;
|
||||
@@ -0,0 +1,10 @@
|
||||
.bar {
|
||||
height: calc(6 * var(--mini-unit));
|
||||
background-color: var(--palette-text-primary);
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin: 10% auto;
|
||||
padding: 0 calc(0.5 * var(--mini-unit));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
import styles from "./MainLayout.css";
|
||||
|
||||
const MainLayout: React.FunctionComponent = ({ children }) => (
|
||||
<div data-testid="main-layout">
|
||||
<div className={styles.bar} />
|
||||
<div className={styles.centered}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default MainLayout;
|
||||
@@ -0,0 +1 @@
|
||||
export { default, default as App } from "./App";
|
||||
@@ -0,0 +1,13 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Delay, Flex, Spinner } from "coral-ui/components";
|
||||
|
||||
const Loading: FunctionComponent = () => (
|
||||
<Flex justifyContent="center">
|
||||
<Delay>
|
||||
<Spinner />
|
||||
</Delay>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
export default Loading;
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Coral - Account</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"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import { createManaged } from "coral-framework/lib/bootstrap";
|
||||
|
||||
import App from "./App";
|
||||
import { initLocalState } from "./local";
|
||||
import localesData from "./locales";
|
||||
|
||||
// Import css variables.
|
||||
import "coral-ui/theme/variables.css";
|
||||
|
||||
async function main() {
|
||||
const ManagedCoralContextProvider = await createManaged({
|
||||
initLocalState,
|
||||
localesData,
|
||||
});
|
||||
|
||||
const Index: FunctionComponent = () => (
|
||||
<ManagedCoralContextProvider>
|
||||
<App />
|
||||
</ManagedCoralContextProvider>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Index />, document.getElementById("app"));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1 @@
|
||||
export { default as initLocalState } from "./initLocalState";
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import { initLocalBaseState } from "coral-framework/lib/relay";
|
||||
|
||||
/**
|
||||
* Initializes the local state, before we start the App.
|
||||
*/
|
||||
export default async function initLocalState(
|
||||
environment: Environment,
|
||||
context: CoralContext
|
||||
) {
|
||||
await initLocalBaseState(environment, context);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
type Local {
|
||||
accessToken: String
|
||||
accessTokenExp: Int
|
||||
accessTokenJTI: String
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
local: Local!
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 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 "coral-framework/lib/i18n";
|
||||
|
||||
export default {} as LocalesData;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { makeRouteConfig, Route } from "found";
|
||||
import React from "react";
|
||||
|
||||
import DownloadRoute from "./routes/download/Download";
|
||||
import ConfirmRoute from "./routes/email/Confirm";
|
||||
import UnsubscribeRoute from "./routes/notifications/Unsubscribe";
|
||||
import ResetRoute from "./routes/password/Reset";
|
||||
|
||||
export default makeRouteConfig(
|
||||
<Route path="account">
|
||||
<Route path="password">
|
||||
<Route path="reset" {...ResetRoute.routeConfig} />
|
||||
</Route>
|
||||
<Route path="email">
|
||||
<Route path="confirm" {...ConfirmRoute.routeConfig} />
|
||||
</Route>
|
||||
<Route path="download" {...DownloadRoute.routeConfig} />
|
||||
<Route path="notifications">
|
||||
<Route path="unsubscribe" {...UnsubscribeRoute.routeConfig} />
|
||||
</Route>
|
||||
</Route>
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { HorizontalGutter, Typography } from "coral-ui/components";
|
||||
|
||||
const NotFound: FunctionComponent = () => (
|
||||
<HorizontalGutter>
|
||||
<Typography variant="heading3">Not Found</Typography>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
|
||||
export default NotFound;
|
||||
@@ -0,0 +1,48 @@
|
||||
.title {
|
||||
font-family: var(--font-family-serif);
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-size: calc(48rem / var(--rem-base));
|
||||
line-height: calc(42em / 48);
|
||||
text-align: center;
|
||||
|
||||
color: var(--palette-common-black);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--spacing-2) var(--spacing-3) var(--spacing-3) var(--spacing-3);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.sectionText {
|
||||
font-family: var(--font-family-sans-serif);
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: calc(24rem / var(--rem-base));
|
||||
line-height: calc(34em / 24);
|
||||
letter-spacing: 0.3px;
|
||||
|
||||
color: var(--palette-text-primary);
|
||||
}
|
||||
|
||||
.list {
|
||||
list-style: none;
|
||||
margin-left: calc(-1 * var(--spacing-5));
|
||||
}
|
||||
|
||||
.list li {
|
||||
margin-bottom: calc(16em / 24);
|
||||
}
|
||||
|
||||
.listContent {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.bullet {
|
||||
display: inline-block;
|
||||
min-width: 24px;
|
||||
margin-right: var(--spacing-2);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import cn from "classnames";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Flex, HorizontalGutter, Icon, Typography } from "coral-ui/components";
|
||||
|
||||
import styles from "./DownloadDescription.css";
|
||||
|
||||
const DownloadDescription: FunctionComponent = () => {
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
<Localized id="download-landingPage-title">
|
||||
<div className={styles.title}>Download Your Comment History</div>
|
||||
</Localized>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.section}>
|
||||
<Localized id="download-landingPage-description">
|
||||
<Typography variant="bodyCopy" className={styles.sectionText}>
|
||||
Your comment history will be downloaded into a .zip file. After
|
||||
your comment history is unzipped you will have a comma separated
|
||||
value (or .csv) file that you can easily import into your favorite
|
||||
spreadsheet application.
|
||||
</Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<Localized id="download-landingPage-contentsDescription">
|
||||
<Typography variant="bodyCopy" className={styles.sectionText}>
|
||||
For each of your comments the following information is included:
|
||||
</Typography>
|
||||
</Localized>
|
||||
<ul className={styles.list}>
|
||||
<li>
|
||||
<Flex alignItems="center">
|
||||
<Icon size="lg" className={styles.bullet}>
|
||||
check
|
||||
</Icon>
|
||||
<Localized id="download-landingPage-contentsDate">
|
||||
<Typography
|
||||
variant="bodyCopy"
|
||||
className={cn(styles.sectionText, styles.listContent)}
|
||||
>
|
||||
When you wrote the comment
|
||||
</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</li>
|
||||
<li>
|
||||
<Flex alignItems="center">
|
||||
<Icon size="lg" className={styles.bullet}>
|
||||
check
|
||||
</Icon>
|
||||
<Localized id="download-landingPage-contentsUrl">
|
||||
<Typography
|
||||
variant="bodyCopy"
|
||||
className={cn(styles.sectionText, styles.listContent)}
|
||||
>
|
||||
The permalink URL for the comment
|
||||
</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</li>
|
||||
<li>
|
||||
<Flex alignItems="center">
|
||||
<Icon size="lg" className={styles.bullet}>
|
||||
check
|
||||
</Icon>
|
||||
<Localized id="download-landingPage-contentsText">
|
||||
<Typography
|
||||
variant="bodyCopy"
|
||||
className={cn(styles.sectionText, styles.listContent)}
|
||||
>
|
||||
The comment text
|
||||
</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</li>
|
||||
<li>
|
||||
<Flex alignItems="center">
|
||||
<Icon size="lg" className={styles.bullet}>
|
||||
check
|
||||
</Icon>
|
||||
<Localized id="download-landingPage-contentsStoryUrl">
|
||||
<Typography
|
||||
variant="bodyCopy"
|
||||
className={cn(styles.sectionText, styles.listContent)}
|
||||
>
|
||||
The URL on the article or story where the comment appears
|
||||
</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadDescription;
|
||||
@@ -0,0 +1,8 @@
|
||||
.form {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.downloadButton {
|
||||
font-size: calc(18rem / var(--rem-base));
|
||||
line-height: calc(23em / 18);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
|
||||
import { Button, HorizontalGutter } from "coral-ui/components";
|
||||
|
||||
import styles from "./DownloadForm.css";
|
||||
|
||||
interface Props {
|
||||
token: string;
|
||||
}
|
||||
|
||||
const DownloadForm: FunctionComponent<Props> = ({ token }) => {
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const onSubmit = useCallback(() => {
|
||||
setSubmitted(true);
|
||||
return true;
|
||||
}, [setSubmitted]);
|
||||
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
<form
|
||||
className={styles.form}
|
||||
method="post"
|
||||
action="/api/account/download"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<input name="token" type="hidden" value={token} />
|
||||
<Localized id="download-landingPage-downloadComments">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="filled"
|
||||
color="primary"
|
||||
disabled={submitted}
|
||||
className={styles.downloadButton}
|
||||
>
|
||||
Download My Comment History
|
||||
</Button>
|
||||
</Localized>
|
||||
</form>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadForm;
|
||||
@@ -0,0 +1,16 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.root {
|
||||
display: inline-block;
|
||||
|
||||
max-width: calc(70 * var(--mini-unit));
|
||||
|
||||
@media (min-width: $breakpoints-xs) {
|
||||
max-width: calc(90 * var(--mini-unit));
|
||||
}
|
||||
|
||||
text-align: left;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import Loading from "coral-account/components/Loading";
|
||||
import { useToken } from "coral-framework/hooks";
|
||||
import { createFetch } from "coral-framework/lib/relay";
|
||||
import { withRouteConfig } from "coral-framework/lib/router";
|
||||
import { parseHashQuery } from "coral-framework/utils";
|
||||
import { HorizontalGutter } from "coral-ui/components";
|
||||
|
||||
import DownloadDescription from "./DownloadDescription";
|
||||
import DownloadForm from "./DownloadForm";
|
||||
import Sorry from "./Sorry";
|
||||
|
||||
import styles from "./DownloadRoute.css";
|
||||
|
||||
const fetcher = createFetch(
|
||||
"downloadToken",
|
||||
async (environment: Environment, variables: { token: string }, { rest }) =>
|
||||
await rest.fetch<void>("/account/download", {
|
||||
method: "GET",
|
||||
token: variables.token,
|
||||
})
|
||||
);
|
||||
|
||||
interface Props {
|
||||
token: string | undefined;
|
||||
}
|
||||
|
||||
const DownloadRoute: FunctionComponent<Props> = ({ token }) => {
|
||||
const [state, error] = useToken(fetcher, token);
|
||||
|
||||
if (state === "UNCHECKED") {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<HorizontalGutter size="double">
|
||||
<Loading />
|
||||
</HorizontalGutter>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (state !== "VALID" || error) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<HorizontalGutter size="double">
|
||||
<DownloadDescription />
|
||||
<Sorry />
|
||||
</HorizontalGutter>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<DownloadDescription />
|
||||
<DownloadForm token={token!} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
render: function DownloadRouteRender({ match, Component }) {
|
||||
return (
|
||||
<Component token={parseHashQuery(match.location.hash).downloadToken} />
|
||||
);
|
||||
},
|
||||
})(DownloadRoute);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,7 @@
|
||||
.icon {
|
||||
margin-right: var(--spacing-2);
|
||||
}
|
||||
|
||||
.callout {
|
||||
justify-content: left;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { CallOut, Flex, Icon } from "coral-ui/components";
|
||||
|
||||
import styles from "./Sorry.css";
|
||||
|
||||
const Sorry: FunctionComponent = () => {
|
||||
return (
|
||||
<CallOut color="error" fullWidth className={styles.callout}>
|
||||
<Flex>
|
||||
<Icon size="md" className={styles.icon}>
|
||||
error
|
||||
</Icon>
|
||||
<Localized id="download-landingPage-sorry">
|
||||
Your download link is invalid.
|
||||
</Localized>
|
||||
</Flex>
|
||||
</CallOut>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sorry;
|
||||
@@ -0,0 +1 @@
|
||||
export { default, default as DownloadRoute } from "./DownloadRoute";
|
||||
@@ -0,0 +1,67 @@
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { useCallback } from "react";
|
||||
import { Form } from "react-final-form";
|
||||
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { useMutation } from "coral-framework/lib/relay";
|
||||
import { Button, HorizontalGutter, Typography } from "coral-ui/components";
|
||||
|
||||
import ConfirmMutation from "./ConfirmMutation";
|
||||
|
||||
interface Props {
|
||||
token: string;
|
||||
disabled?: boolean;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const ConfirmForm: React.FunctionComponent<Props> = ({ onSuccess, token }) => {
|
||||
const confirm = useMutation(ConfirmMutation);
|
||||
const onSubmit = useCallback(async () => {
|
||||
try {
|
||||
await confirm({ token });
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidRequestError) {
|
||||
return error.invalidArgs;
|
||||
}
|
||||
return { [FORM_ERROR]: error.message };
|
||||
}
|
||||
return;
|
||||
}, [token]);
|
||||
return (
|
||||
<Form onSubmit={onSubmit}>
|
||||
{({ handleSubmit, submitting }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="double">
|
||||
<HorizontalGutter>
|
||||
<Localized id="confirmEmail-emailConfirmation">
|
||||
<Typography variant="heading1">Email Confirmation</Typography>
|
||||
</Localized>
|
||||
<Localized id="confirmEmail-pleaseClickToConfirm">
|
||||
<Typography variant="bodyCopy">
|
||||
Click below to confirm your email address.
|
||||
</Typography>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
<HorizontalGutter>
|
||||
<Localized id="confirmEmail-confirmEmail">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="filled"
|
||||
color="primary"
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
>
|
||||
Confirm email
|
||||
</Button>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmForm;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { createMutation } from "coral-framework/lib/relay";
|
||||
|
||||
const ConfirmMutation = createMutation(
|
||||
"confirm",
|
||||
async (environment: Environment, variables: { token: string }, { rest }) =>
|
||||
await rest.fetch<void>("/account/confirm", {
|
||||
method: "PUT",
|
||||
token: variables.token,
|
||||
})
|
||||
);
|
||||
|
||||
export default ConfirmMutation;
|
||||
@@ -0,0 +1,16 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.root {
|
||||
display: inline-block;
|
||||
|
||||
max-width: calc(70 * var(--mini-unit));
|
||||
|
||||
@media (min-width: $breakpoints-xs) {
|
||||
max-width: calc(42 * var(--mini-unit));
|
||||
}
|
||||
|
||||
text-align: left;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import Loading from "coral-account/components/Loading";
|
||||
import { useToken } from "coral-framework/hooks";
|
||||
import { createFetch } from "coral-framework/lib/relay";
|
||||
import { withRouteConfig } from "coral-framework/lib/router";
|
||||
import { parseHashQuery } from "coral-framework/utils";
|
||||
|
||||
import ConfirmForm from "./ConfirmForm";
|
||||
import Sorry from "./Sorry";
|
||||
import Success from "./Success";
|
||||
|
||||
import styles from "./ConfirmRoute.css";
|
||||
|
||||
const fetcher = createFetch(
|
||||
"confirmToken",
|
||||
async (environment: Environment, variables: { token: string }, { rest }) =>
|
||||
await rest.fetch<void>("/account/confirm", {
|
||||
method: "GET",
|
||||
token: variables.token,
|
||||
})
|
||||
);
|
||||
|
||||
interface Props {
|
||||
token: string | undefined;
|
||||
}
|
||||
|
||||
const ConfirmRoute: React.FunctionComponent<Props> = ({ token }) => {
|
||||
const [finished, setFinished] = useState(false);
|
||||
const onSuccess = useCallback(() => {
|
||||
setFinished(true);
|
||||
}, []);
|
||||
const [state, error] = useToken(fetcher, token);
|
||||
|
||||
if (state === "UNCHECKED") {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<Loading />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state !== "VALID" || error) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<Sorry reason={error} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return !finished ? (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<ConfirmForm token={token!} onSuccess={onSuccess} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<Success />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
render: function ConfirmRouteRender({ match, Component }) {
|
||||
return (
|
||||
<Component token={parseHashQuery(match.location.hash).confirmToken} />
|
||||
);
|
||||
},
|
||||
})(ConfirmRoute);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
|
||||
import { CallOut, HorizontalGutter, Typography } from "coral-ui/components";
|
||||
|
||||
interface Props {
|
||||
reason: React.ReactNode;
|
||||
}
|
||||
|
||||
const Sorry: React.FunctionComponent<Props> = ({ reason }) => {
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
<Localized id="confirmEmail-oopsSorry">
|
||||
<Typography variant="heading1">Oops Sorry!</Typography>
|
||||
</Localized>
|
||||
<CallOut color="error" fullWidth>
|
||||
{reason ? (
|
||||
reason
|
||||
) : (
|
||||
<Localized id="account-tokenNotFound">
|
||||
<span data-testid="invalid-link">
|
||||
The specified link is invalid, check to see if it was copied
|
||||
correctly.
|
||||
</span>
|
||||
</Localized>
|
||||
)}
|
||||
</CallOut>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sorry;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
|
||||
import { HorizontalGutter, Typography } from "coral-ui/components";
|
||||
|
||||
const Success: React.FunctionComponent = () => {
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
<Localized id="confirmEmail-successfullyConfirmed">
|
||||
<Typography variant="heading1">Email successfully confirmed</Typography>
|
||||
</Localized>
|
||||
<Localized id="confirmEmail-youMayClose">
|
||||
<Typography variant="bodyCopy">
|
||||
You may now close this window.
|
||||
</Typography>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Success;
|
||||
@@ -0,0 +1 @@
|
||||
export { default, default as ConfirmRoute } from "./ConfirmRoute";
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
|
||||
import { CallOut, HorizontalGutter, Typography } from "coral-ui/components";
|
||||
|
||||
interface Props {
|
||||
reason: React.ReactNode;
|
||||
}
|
||||
|
||||
const Sorry: React.FunctionComponent<Props> = ({ reason }) => {
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
<Localized id="unsubscribe-oopsSorry">
|
||||
<Typography variant="heading1">Oops Sorry!</Typography>
|
||||
</Localized>
|
||||
<CallOut color="error" fullWidth>
|
||||
{reason ? (
|
||||
reason
|
||||
) : (
|
||||
<Localized id="account-tokenNotFound">
|
||||
<span data-testid="invalid-link">
|
||||
The specified link is invalid, check to see if it was copied
|
||||
correctly.
|
||||
</span>
|
||||
</Localized>
|
||||
)}
|
||||
</CallOut>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sorry;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
|
||||
import { HorizontalGutter, Typography } from "coral-ui/components";
|
||||
|
||||
const Success: React.FunctionComponent = () => {
|
||||
return (
|
||||
<HorizontalGutter data-testid="success" size="double">
|
||||
<Localized id="unsubscribe-successfullyUnsubscribed">
|
||||
<Typography variant="heading1">
|
||||
You are now unsubscribed from all notifications
|
||||
</Typography>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Success;
|
||||
@@ -0,0 +1,76 @@
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { useCallback } from "react";
|
||||
import { Form } from "react-final-form";
|
||||
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { useMutation } from "coral-framework/lib/relay";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
HorizontalGutter,
|
||||
Typography,
|
||||
} from "coral-ui/components";
|
||||
|
||||
import UnsubscribeNotificationsMutation from "./UnsubscribeNotificationsMutation";
|
||||
|
||||
interface Props {
|
||||
token: string;
|
||||
disabled?: boolean;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const UnsubscribeForm: React.FunctionComponent<Props> = ({
|
||||
onSuccess,
|
||||
token,
|
||||
}) => {
|
||||
const unsubscribe = useMutation(UnsubscribeNotificationsMutation);
|
||||
const onSubmit = useCallback(async () => {
|
||||
try {
|
||||
await unsubscribe({ token });
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidRequestError) {
|
||||
return error.invalidArgs;
|
||||
}
|
||||
return { [FORM_ERROR]: error.message };
|
||||
}
|
||||
return;
|
||||
}, [token]);
|
||||
return (
|
||||
<div data-testid="unsubscribe-form">
|
||||
<Form onSubmit={onSubmit}>
|
||||
{({ handleSubmit, submitting, submitError }) => (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<HorizontalGutter>
|
||||
<Localized id="unsubscribe-clickToConfirm">
|
||||
<Typography variant="heading1">
|
||||
Click below to confirm that you want to unsubscribe from all
|
||||
notifications.
|
||||
</Typography>
|
||||
</Localized>
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
<Localized id="unsubscribe-confirm">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="filled"
|
||||
color="primary"
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnsubscribeForm;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { createMutation } from "coral-framework/lib/relay";
|
||||
|
||||
const UnsubscribeNotificationsMutation = createMutation(
|
||||
"unsubscribeNotifications",
|
||||
async (environment: Environment, variables: { token: string }, { rest }) =>
|
||||
await rest.fetch<void>("/account/notifications/unsubscribe", {
|
||||
method: "DELETE",
|
||||
token: variables.token,
|
||||
})
|
||||
);
|
||||
|
||||
export default UnsubscribeNotificationsMutation;
|
||||
@@ -0,0 +1,16 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.root {
|
||||
display: inline-block;
|
||||
|
||||
max-width: calc(70 * var(--mini-unit));
|
||||
|
||||
@media (min-width: $breakpoints-xs) {
|
||||
max-width: calc(42 * var(--mini-unit));
|
||||
}
|
||||
|
||||
text-align: left;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import Loading from "coral-account/components/Loading";
|
||||
import { useToken } from "coral-framework/hooks";
|
||||
import { createFetch } from "coral-framework/lib/relay";
|
||||
import { withRouteConfig } from "coral-framework/lib/router";
|
||||
import { parseHashQuery } from "coral-framework/utils";
|
||||
|
||||
import Sorry from "./Sorry";
|
||||
import Success from "./Success";
|
||||
import UnsubscribeForm from "./UnsubscribeForm";
|
||||
|
||||
import styles from "./UnsubscribeRoute.css";
|
||||
|
||||
const fetcher = createFetch(
|
||||
"unsubscribeToken",
|
||||
async (environment: Environment, variables: { token: string }, { rest }) =>
|
||||
await rest.fetch<void>("/account/notifications/unsubscribe", {
|
||||
method: "GET",
|
||||
token: variables.token,
|
||||
})
|
||||
);
|
||||
|
||||
interface Props {
|
||||
token: string | undefined;
|
||||
}
|
||||
|
||||
const UnsubscribeRoute: React.FunctionComponent<Props> = ({ token }) => {
|
||||
const [finished, setFinished] = useState(false);
|
||||
const onSuccess = useCallback(() => {
|
||||
setFinished(true);
|
||||
}, []);
|
||||
const [state, error] = useToken(fetcher, token);
|
||||
|
||||
if (state === "UNCHECKED") {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<Loading />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state !== "VALID" || error) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<Sorry reason={error} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return !finished ? (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<UnsubscribeForm token={token!} onSuccess={onSuccess} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<Success />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
render: function UnsubscribeRouteRender({ match, Component }) {
|
||||
return (
|
||||
<Component token={parseHashQuery(match.location.hash).unsubscribeToken} />
|
||||
);
|
||||
},
|
||||
})(UnsubscribeRoute);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1 @@
|
||||
export { default, default as UnsubscribeRoute } from "./UnsubscribeRoute";
|
||||
@@ -0,0 +1,139 @@
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { useCallback } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { colorFromMeta, ValidationMessage } from "coral-framework/lib/form";
|
||||
import { useMutation } from "coral-framework/lib/relay";
|
||||
import {
|
||||
composeValidators,
|
||||
required,
|
||||
validatePassword,
|
||||
} from "coral-framework/lib/validation";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
FormField,
|
||||
HorizontalGutter,
|
||||
InputDescription,
|
||||
InputLabel,
|
||||
PasswordField,
|
||||
Typography,
|
||||
} from "coral-ui/components";
|
||||
|
||||
import ResetPasswordMutation from "./ResetPasswordMutation";
|
||||
|
||||
interface Props {
|
||||
token: string;
|
||||
disabled?: boolean;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
interface FormProps {
|
||||
password: string;
|
||||
}
|
||||
|
||||
const ResetPasswordForm: React.FunctionComponent<Props> = ({
|
||||
onSuccess,
|
||||
token,
|
||||
disabled,
|
||||
}) => {
|
||||
const resetPassword = useMutation(ResetPasswordMutation);
|
||||
const onSubmit = useCallback(
|
||||
async ({ password }: FormProps) => {
|
||||
try {
|
||||
await resetPassword({ token, password });
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidRequestError) {
|
||||
return error.invalidArgs;
|
||||
}
|
||||
return { [FORM_ERROR]: error.message };
|
||||
}
|
||||
return;
|
||||
},
|
||||
[token]
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<Form onSubmit={onSubmit}>
|
||||
{({ handleSubmit, submitting, submitError }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="double">
|
||||
<HorizontalGutter>
|
||||
<Localized id="resetPassword-resetYourPassword">
|
||||
<Typography variant="heading1">
|
||||
Reset your password
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Localized id="resetPassword-pleaseEnterNewPassword">
|
||||
<Typography variant="bodyCopy">
|
||||
Please enter a new password to use to sign in to your
|
||||
account. Make sure it is unique and be sure to keep it
|
||||
secure.
|
||||
</Typography>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
<HorizontalGutter>
|
||||
<Field
|
||||
name="password"
|
||||
validate={composeValidators(required, validatePassword)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="resetPassword-passwordLabel">
|
||||
<InputLabel htmlFor={input.name}>Password</InputLabel>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="resetPassword-passwordDescription"
|
||||
$minLength={8}
|
||||
>
|
||||
<InputDescription>
|
||||
{"Must be at least {$minLength} characters"}
|
||||
</InputDescription>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="resetPassword-passwordTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<PasswordField
|
||||
id={input.name}
|
||||
placeholder="Password"
|
||||
color={colorFromMeta(meta)}
|
||||
autoComplete="new-password"
|
||||
disabled={disabled || meta.submitting}
|
||||
fullWidth
|
||||
{...input}
|
||||
/>
|
||||
</Localized>
|
||||
<ValidationMessage meta={meta} fullWidth />
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
<Localized id="resetPassword-resetPassword">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="filled"
|
||||
color="primary"
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPasswordForm;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { createMutation } from "coral-framework/lib/relay";
|
||||
|
||||
const ResetPasswordMutation = createMutation(
|
||||
"resetPassword",
|
||||
async (
|
||||
environment: Environment,
|
||||
variables: { token: string; password: string },
|
||||
{ rest }
|
||||
) =>
|
||||
await rest.fetch<void>("/auth/local/forgot", {
|
||||
method: "PUT",
|
||||
token: variables.token,
|
||||
body: {
|
||||
password: variables.password,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default ResetPasswordMutation;
|
||||
@@ -0,0 +1,16 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.root {
|
||||
display: inline-block;
|
||||
|
||||
max-width: calc(70 * var(--mini-unit));
|
||||
|
||||
@media (min-width: $breakpoints-xs) {
|
||||
max-width: calc(42 * var(--mini-unit));
|
||||
}
|
||||
|
||||
text-align: left;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import Loading from "coral-account/components/Loading";
|
||||
import { useToken } from "coral-framework/hooks";
|
||||
import { createFetch } from "coral-framework/lib/relay";
|
||||
import { withRouteConfig } from "coral-framework/lib/router";
|
||||
import { parseHashQuery } from "coral-framework/utils";
|
||||
|
||||
import ResetPasswordForm from "./ResetPasswordForm";
|
||||
import Sorry from "./Sorry";
|
||||
import Success from "./Success";
|
||||
|
||||
import styles from "./ResetRoute.css";
|
||||
|
||||
const fetcher = createFetch(
|
||||
"resetToken",
|
||||
async (environment: Environment, variables: { token: string }, { rest }) =>
|
||||
await rest.fetch<void>("/auth/local/forgot", {
|
||||
method: "GET",
|
||||
token: variables.token,
|
||||
})
|
||||
);
|
||||
|
||||
interface Props {
|
||||
token: string | undefined;
|
||||
}
|
||||
|
||||
const ResetRoute: React.FunctionComponent<Props> = ({ token }) => {
|
||||
const [finished, setFinished] = useState(false);
|
||||
const onSuccess = useCallback(() => {
|
||||
setFinished(true);
|
||||
}, []);
|
||||
const [state, error] = useToken(fetcher, token);
|
||||
|
||||
if (state === "UNCHECKED") {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<Loading />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state !== "VALID" || error) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<Sorry reason={error} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return !finished ? (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<ResetPasswordForm token={token!} onSuccess={onSuccess} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<Success />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
render: function ResetRouteRender({ match, Component }) {
|
||||
return <Component token={parseHashQuery(match.location.hash).resetToken} />;
|
||||
},
|
||||
})(ResetRoute);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
|
||||
import { CallOut, HorizontalGutter, Typography } from "coral-ui/components";
|
||||
|
||||
interface Props {
|
||||
reason: React.ReactNode;
|
||||
}
|
||||
|
||||
const Sorry: React.FunctionComponent<Props> = ({ reason }) => {
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
<Localized id="resetPassword-oopsSorry">
|
||||
<Typography variant="heading1">Oops Sorry!</Typography>
|
||||
</Localized>
|
||||
<CallOut color="error" fullWidth>
|
||||
{reason ? (
|
||||
reason
|
||||
) : (
|
||||
<Localized id="account-tokenNotFound">
|
||||
<span data-testid="invalid-link">
|
||||
The specified link is invalid, check to see if it was copied
|
||||
correctly.
|
||||
</span>
|
||||
</Localized>
|
||||
)}
|
||||
</CallOut>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sorry;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
|
||||
import { HorizontalGutter, Typography } from "coral-ui/components";
|
||||
|
||||
const Success: React.FunctionComponent = () => {
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
<Localized id="resetPassword-successfullyReset">
|
||||
<Typography variant="heading1">Password successfully reset</Typography>
|
||||
</Localized>
|
||||
<Localized id="resetPassword-youMayClose">
|
||||
<Typography variant="bodyCopy">
|
||||
You may now close this window and sign in to your account with your
|
||||
new password.
|
||||
</Typography>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Success;
|
||||
@@ -0,0 +1 @@
|
||||
export { default, default as ResetRoute } from "./ResetRoute";
|
||||
@@ -0,0 +1,106 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders form 1`] = `
|
||||
<div
|
||||
data-testid="main-layout"
|
||||
>
|
||||
<div
|
||||
className="MainLayout-bar"
|
||||
/>
|
||||
<div
|
||||
className="MainLayout-centered"
|
||||
>
|
||||
<div
|
||||
className="ConfirmRoute-container"
|
||||
>
|
||||
<div
|
||||
className="ConfirmRoute-root"
|
||||
>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<h1
|
||||
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
|
||||
>
|
||||
Email Confirmation
|
||||
</h1>
|
||||
<p
|
||||
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Click below to confirm your email address.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
data-color="primary"
|
||||
data-variant="filled"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Confirm email
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders missing confirm token 1`] = `
|
||||
<div
|
||||
data-testid="main-layout"
|
||||
>
|
||||
<div
|
||||
className="MainLayout-bar"
|
||||
/>
|
||||
<div
|
||||
className="MainLayout-centered"
|
||||
>
|
||||
<div
|
||||
className="ConfirmRoute-container"
|
||||
>
|
||||
<div
|
||||
className="ConfirmRoute-root"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<h1
|
||||
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
|
||||
>
|
||||
Oops Sorry!
|
||||
</h1>
|
||||
<div
|
||||
className="CallOut-root CallOut-colorError CallOut-fullWidth"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
data-testid="invalid-link"
|
||||
>
|
||||
The specified link is invalid, check to see if it was copied correctly.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,163 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders form 1`] = `
|
||||
<div
|
||||
data-testid="main-layout"
|
||||
>
|
||||
<div
|
||||
className="MainLayout-bar"
|
||||
/>
|
||||
<div
|
||||
className="MainLayout-centered"
|
||||
>
|
||||
<div
|
||||
className="ResetRoute-container"
|
||||
>
|
||||
<div
|
||||
className="ResetRoute-root"
|
||||
>
|
||||
<div>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<h1
|
||||
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
|
||||
>
|
||||
Reset your password
|
||||
</h1>
|
||||
<p
|
||||
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Please enter a new password to use to sign in to your account.
|
||||
Make sure it is unique and be sure to keep it secure.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Box-root Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<p
|
||||
className="Box-root Typography-root Typography-fieldDescription Typography-colorTextSecondary"
|
||||
>
|
||||
Must be at least 8 characters
|
||||
</p>
|
||||
<div
|
||||
className="PasswordField-fullWidth PasswordField-root"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-wrapper"
|
||||
>
|
||||
<input
|
||||
autoCapitalize="off"
|
||||
autoComplete="new-password"
|
||||
autoCorrect="off"
|
||||
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
|
||||
disabled={false}
|
||||
id="password"
|
||||
name="password"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
placeholder="Password"
|
||||
spellCheck={false}
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Hide password"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
data-color="primary"
|
||||
data-variant="filled"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Reset Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders missing reset token 1`] = `
|
||||
<div
|
||||
data-testid="main-layout"
|
||||
>
|
||||
<div
|
||||
className="MainLayout-bar"
|
||||
/>
|
||||
<div
|
||||
className="MainLayout-centered"
|
||||
>
|
||||
<div
|
||||
className="ResetRoute-container"
|
||||
>
|
||||
<div
|
||||
className="ResetRoute-root"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<h1
|
||||
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
|
||||
>
|
||||
Oops Sorry!
|
||||
</h1>
|
||||
<div
|
||||
className="CallOut-root CallOut-colorError CallOut-fullWidth"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
data-testid="invalid-link"
|
||||
>
|
||||
The specified link is invalid, check to see if it was copied correctly.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,96 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders form 1`] = `
|
||||
<div
|
||||
data-testid="main-layout"
|
||||
>
|
||||
<div
|
||||
className="MainLayout-bar"
|
||||
/>
|
||||
<div
|
||||
className="MainLayout-centered"
|
||||
>
|
||||
<div
|
||||
className="UnsubscribeRoute-container"
|
||||
>
|
||||
<div
|
||||
className="UnsubscribeRoute-root"
|
||||
>
|
||||
<div
|
||||
data-testid="unsubscribe-form"
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<h1
|
||||
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
|
||||
>
|
||||
Click below to confirm that you want to unsubscribe from all notifications.
|
||||
</h1>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
data-color="primary"
|
||||
data-variant="filled"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders missing confirm token 1`] = `
|
||||
<div
|
||||
data-testid="main-layout"
|
||||
>
|
||||
<div
|
||||
className="MainLayout-bar"
|
||||
/>
|
||||
<div
|
||||
className="MainLayout-centered"
|
||||
>
|
||||
<div
|
||||
className="UnsubscribeRoute-container"
|
||||
>
|
||||
<div
|
||||
className="UnsubscribeRoute-root"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<h1
|
||||
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
|
||||
>
|
||||
Oops Sorry!
|
||||
</h1>
|
||||
<div
|
||||
className="CallOut-root CallOut-colorError CallOut-fullWidth"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
data-testid="invalid-link"
|
||||
>
|
||||
The specified link is invalid, check to see if it was copied correctly.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,147 @@
|
||||
import sinon from "sinon";
|
||||
|
||||
import { ERROR_CODES } from "coral-common/errors";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
act,
|
||||
createAccessToken,
|
||||
CreateTestRendererParams,
|
||||
replaceHistoryLocation,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import create from "./create";
|
||||
|
||||
const token = createAccessToken();
|
||||
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer, context } = create();
|
||||
return {
|
||||
context,
|
||||
testRenderer,
|
||||
root: testRenderer.root,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders missing confirm token", async () => {
|
||||
replaceHistoryLocation("http://localhost/account/email/confirm");
|
||||
const { root } = await createTestRenderer();
|
||||
await waitForElement(() => within(root).getByTestID("invalid-link"));
|
||||
expect(within(root).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders form", async () => {
|
||||
replaceHistoryLocation(
|
||||
`http://localhost/account/email/confirm#confirmToken=${token}`
|
||||
);
|
||||
const { root, context } = await createTestRenderer();
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/account/confirm", {
|
||||
method: "GET",
|
||||
token,
|
||||
})
|
||||
.once();
|
||||
|
||||
await act(async () => {
|
||||
await waitForElement(() =>
|
||||
within(root).getByText("Email Confirmation", {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
expect(within(root).toJSON()).toMatchSnapshot();
|
||||
restMock.verify();
|
||||
});
|
||||
|
||||
it("renders error from server", async () => {
|
||||
replaceHistoryLocation(
|
||||
`http://localhost/account/email/confirm#confirmToken=${token}`
|
||||
);
|
||||
|
||||
const codes = [
|
||||
ERROR_CODES.RATE_LIMIT_EXCEEDED,
|
||||
ERROR_CODES.EMAIL_CONFIRM_TOKEN_EXPIRED,
|
||||
ERROR_CODES.INTEGRATION_DISABLED,
|
||||
ERROR_CODES.USER_NOT_FOUND,
|
||||
ERROR_CODES.TOKEN_INVALID,
|
||||
];
|
||||
|
||||
for (const code of codes) {
|
||||
const { root, context } = await createTestRenderer();
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/account/confirm", {
|
||||
method: "GET",
|
||||
token,
|
||||
})
|
||||
.once()
|
||||
.throwsException(
|
||||
new InvalidRequestError({
|
||||
code,
|
||||
})
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await waitForElement(() =>
|
||||
within(root).getByText(code, {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
restMock.verify();
|
||||
}
|
||||
});
|
||||
|
||||
it("submits form", async () => {
|
||||
replaceHistoryLocation(
|
||||
`http://localhost/account/email/confirm#confirmToken=${token}`
|
||||
);
|
||||
const { root, context } = await createTestRenderer();
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/account/confirm", {
|
||||
method: "GET",
|
||||
token,
|
||||
})
|
||||
.once();
|
||||
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/account/confirm", {
|
||||
method: "PUT",
|
||||
token,
|
||||
})
|
||||
.once();
|
||||
|
||||
await act(async () => {
|
||||
await waitForElement(() =>
|
||||
within(root).getByText("Email Confirmation", {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
const form = within(root).getByType("form");
|
||||
|
||||
// Submit valid form.
|
||||
await act(async () => {
|
||||
form.props.onSubmit();
|
||||
await waitForElement(() =>
|
||||
within(root).getByText("successfully", {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
restMock.verify();
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
|
||||
import App from "coral-account/App";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
createTestRenderer,
|
||||
CreateTestRendererParams,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
export default function create(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
return createTestRenderer<GQLResolver>("account", <App />, params);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import sinon from "sinon";
|
||||
|
||||
import { ERROR_CODES } from "coral-common/errors";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
act,
|
||||
createAccessToken,
|
||||
CreateTestRendererParams,
|
||||
replaceHistoryLocation,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import create from "./create";
|
||||
|
||||
const token = createAccessToken();
|
||||
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer, context } = create();
|
||||
return {
|
||||
context,
|
||||
testRenderer,
|
||||
root: testRenderer.root,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders missing reset token", async () => {
|
||||
replaceHistoryLocation("http://localhost/account/password/reset");
|
||||
const { root } = await createTestRenderer();
|
||||
await waitForElement(() => within(root).getByTestID("invalid-link"));
|
||||
expect(within(root).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders form", async () => {
|
||||
replaceHistoryLocation(
|
||||
`http://localhost/account/password/reset#resetToken=${token}`
|
||||
);
|
||||
const { root, context } = await createTestRenderer();
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/auth/local/forgot", {
|
||||
method: "GET",
|
||||
token,
|
||||
})
|
||||
.once();
|
||||
|
||||
await act(async () => {
|
||||
await waitForElement(() =>
|
||||
within(root).getByText("Reset your password", {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
expect(within(root).toJSON()).toMatchSnapshot();
|
||||
restMock.verify();
|
||||
});
|
||||
|
||||
it("renders error from server", async () => {
|
||||
replaceHistoryLocation(
|
||||
`http://localhost/account/password/reset#resetToken=${token}`
|
||||
);
|
||||
|
||||
const codes = [
|
||||
ERROR_CODES.RATE_LIMIT_EXCEEDED,
|
||||
ERROR_CODES.PASSWORD_RESET_TOKEN_EXPIRED,
|
||||
ERROR_CODES.INTEGRATION_DISABLED,
|
||||
ERROR_CODES.USER_NOT_FOUND,
|
||||
ERROR_CODES.TOKEN_INVALID,
|
||||
];
|
||||
|
||||
for (const code of codes) {
|
||||
const { root, context } = await createTestRenderer();
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/auth/local/forgot", {
|
||||
method: "GET",
|
||||
token,
|
||||
})
|
||||
.once()
|
||||
.throwsException(
|
||||
new InvalidRequestError({
|
||||
code,
|
||||
})
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await waitForElement(() =>
|
||||
within(root).getByText(code, {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
restMock.verify();
|
||||
}
|
||||
});
|
||||
|
||||
it("submits form", async () => {
|
||||
replaceHistoryLocation(
|
||||
`http://localhost/account/password/reset#resetToken=${token}`
|
||||
);
|
||||
const { root, context } = await createTestRenderer();
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/auth/local/forgot", {
|
||||
method: "GET",
|
||||
token,
|
||||
})
|
||||
.once();
|
||||
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/auth/local/forgot", {
|
||||
method: "PUT",
|
||||
token,
|
||||
body: {
|
||||
password: "testtest",
|
||||
},
|
||||
})
|
||||
.once();
|
||||
|
||||
await act(async () => {
|
||||
await waitForElement(() =>
|
||||
within(root).getByText("Reset your password", {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
const form = within(root).getByType("form");
|
||||
const textField = within(root).getByLabelText("Password");
|
||||
|
||||
// Submit an empty form.
|
||||
act(() => {
|
||||
form.props.onSubmit();
|
||||
});
|
||||
within(root).getByText("field is required", {
|
||||
exact: false,
|
||||
});
|
||||
|
||||
// Password too short.
|
||||
act(() => {
|
||||
textField.props.onChange("test");
|
||||
});
|
||||
within(root).getByText("Password must contain at least 8 characters", {
|
||||
exact: false,
|
||||
});
|
||||
|
||||
// Submit valid form.
|
||||
await act(async () => {
|
||||
textField.props.onChange("testtest");
|
||||
form.props.onSubmit();
|
||||
await waitForElement(() =>
|
||||
within(root).getByText("successfully", {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
restMock.verify();
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
import sinon from "sinon";
|
||||
|
||||
import { ERROR_CODES } from "coral-common/errors";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
act,
|
||||
createAccessToken,
|
||||
CreateTestRendererParams,
|
||||
replaceHistoryLocation,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import create from "./create";
|
||||
|
||||
const token = createAccessToken();
|
||||
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer, context } = create();
|
||||
return {
|
||||
context,
|
||||
testRenderer,
|
||||
root: testRenderer.root,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders missing confirm token", async () => {
|
||||
replaceHistoryLocation("http://localhost/account/notifications/unsubscribe");
|
||||
const { root } = await createTestRenderer();
|
||||
await waitForElement(() => within(root).getByTestID("invalid-link"));
|
||||
expect(within(root).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders form", async () => {
|
||||
replaceHistoryLocation(
|
||||
`http://localhost/account/notifications/unsubscribe#unsubscribeToken=${token}`
|
||||
);
|
||||
const { root, context } = await createTestRenderer();
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/account/notifications/unsubscribe", {
|
||||
method: "GET",
|
||||
token,
|
||||
})
|
||||
.once();
|
||||
|
||||
await act(async () => {
|
||||
await waitForElement(() => within(root).getByTestID("unsubscribe-form"));
|
||||
});
|
||||
expect(within(root).toJSON()).toMatchSnapshot();
|
||||
restMock.verify();
|
||||
});
|
||||
|
||||
it("renders error from server", async () => {
|
||||
replaceHistoryLocation(
|
||||
`http://localhost/account/notifications/unsubscribe#unsubscribeToken=${token}`
|
||||
);
|
||||
|
||||
const codes = [
|
||||
ERROR_CODES.RATE_LIMIT_EXCEEDED,
|
||||
ERROR_CODES.INTEGRATION_DISABLED,
|
||||
ERROR_CODES.USER_NOT_FOUND,
|
||||
ERROR_CODES.TOKEN_INVALID,
|
||||
];
|
||||
|
||||
for (const code of codes) {
|
||||
const { root, context } = await createTestRenderer();
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/account/notifications/unsubscribe", {
|
||||
method: "GET",
|
||||
token,
|
||||
})
|
||||
.once()
|
||||
.throwsException(
|
||||
new InvalidRequestError({
|
||||
code,
|
||||
})
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await waitForElement(() =>
|
||||
within(root).getByText(code, {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
restMock.verify();
|
||||
}
|
||||
});
|
||||
|
||||
it("submits form", async () => {
|
||||
replaceHistoryLocation(
|
||||
`http://localhost/account/notifications/unsubscribe#unsubscribeToken=${token}`
|
||||
);
|
||||
const { root, context } = await createTestRenderer();
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/account/notifications/unsubscribe", {
|
||||
method: "GET",
|
||||
token,
|
||||
})
|
||||
.once();
|
||||
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/account/notifications/unsubscribe", {
|
||||
method: "DELETE",
|
||||
token,
|
||||
})
|
||||
.once();
|
||||
|
||||
await act(async () => {
|
||||
await waitForElement(() => within(root).getByTestID("unsubscribe-form"));
|
||||
});
|
||||
const form = within(root).getByType("form");
|
||||
|
||||
// Submit valid form.
|
||||
await act(async () => {
|
||||
form.props.onSubmit();
|
||||
await waitForElement(() => within(root).getByTestID("success"));
|
||||
});
|
||||
|
||||
restMock.verify();
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user