├── .arcconfig ├── .arclint ├── .dockerignore ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gcloudignore ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── DEBUGGING ├── Dockerfile ├── LICENSE ├── README.md ├── app-flex.yaml ├── app-standard.yaml ├── app.js ├── babel.config.js ├── deploy.sh ├── eslint ├── README.md ├── eslintrc ├── eslintrc.browser ├── eslintrc.flow ├── eslintrc.node ├── eslintrc.prettier ├── eslintrc.react └── eslintrc.shared ├── flow-typed ├── custom │ ├── @google-cloud │ │ ├── debug-agent.js │ │ ├── logging-winston.js │ │ ├── profiler.js │ │ └── trace-agent.js │ ├── apollo-cache-inmemory.js │ ├── argparse.js │ ├── express-winston.js │ └── jsdom.js └── npm │ ├── apollo-client_v2.x.x.js │ ├── apollo-link-http_v1.2.x.js │ ├── body-parser_v1.x.x.js │ ├── chai_v4.x.x.js │ ├── express_v4.16.x.js │ ├── mocha_v5.x.x.js │ ├── nock_v10.x.x.js │ ├── node-fetch_v1.x.x.js │ ├── sinon_v7.x.x.js │ └── winston_v3.x.x.js ├── generate_config_files.py ├── nginx.conf.template ├── nginx.list ├── package-lock.json ├── package.json ├── processes.json.template ├── set_default.sh ├── src ├── arguments.js ├── configure-apollo-network.js ├── create-render-context.js ├── custom-resource-loader.js ├── custom-resource-loader_test.js ├── fetch_package.js ├── fetch_package_test.js ├── get-request-id.js ├── get-request-id_test.js ├── lint_blacklist.txt ├── logging.js ├── main.js ├── patch-promise.js ├── profile.js ├── render.js ├── render_apollo_test.js ├── render_test.js ├── request-id-middleware.js ├── secret.js ├── secret_test.js ├── server.js ├── server_test.js ├── testdata │ ├── basic │ │ └── entry.js │ ├── globals │ │ └── entry.js │ └── webpacked │ │ ├── README.md │ │ ├── apollo │ │ ├── schema-error │ │ │ └── entry.js │ │ ├── simple │ │ │ └── entry.js │ │ ├── syntax-error │ │ │ └── entry.js │ │ └── variables │ │ │ └── entry.js │ │ ├── canvas │ │ └── entry.js │ │ ├── common │ │ ├── 1.js │ │ ├── 2.js │ │ └── 3.js │ │ ├── simple │ │ └── entry.js │ │ └── with-aphrodite │ │ └── entry.js ├── trace-agent.js └── types.js ├── test_setup.js └── yarn.lock /.arcconfig: -------------------------------------------------------------------------------- 1 | { 2 | "conduit_uri": "https://phabricator.khanacademy.org/", 3 | "lint.engine": "ArcanistConfigurationDrivenLintEngine" 4 | } 5 | -------------------------------------------------------------------------------- /.arclint: -------------------------------------------------------------------------------- 1 | { 2 | "linters": { 3 | "khan-linter": { 4 | "type": "script-and-regex", 5 | "script-and-regex.script": "ka-lint --always-exit-0 --blacklist=yes --propose-arc-fixes", 6 | "script-and-regex.regex": "\/^((?P[^:]*):(?P\\d+):((?P\\d+):)? (?P((?PE)|(?PW))\\S+) (?P[^\\x00\n]*)(\\x00(?P[^\\x00]*)\\x00(?P[^\\x00]*)\\x00)?)|(?PSKIPPING.*)$\/m" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .dockerignore 3 | Dockerfile 4 | npm-debug.log 5 | .git 6 | src/*_test.js 7 | src/testdata 8 | src/lint_blacklist.txt 9 | deploy.sh 10 | set_default.sh 11 | tools 12 | README.md 13 | DEBUGGING 14 | LICENSE 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | flow-typed/**/*.js 3 | coverage 4 | 5 | # eslint can't handle the type defintion for the addStyle HOC 6 | **/dist 7 | 8 | config 9 | 10 | src/testdata 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "./eslint/eslintrc", 4 | "plugin:eslint-comments/recommended" 5 | ], 6 | "plugins": [ 7 | "import", 8 | "promise", 9 | "eslint-comments", 10 | "disable" 11 | ], 12 | "settings": { 13 | "eslint-plugin-disable": { 14 | "paths": { 15 | "react": ["./*.js", "src/*.js"] 16 | } 17 | } 18 | }, 19 | "overrides": [ 20 | { 21 | "files": ["./*.js", "src/*.js"], 22 | "settings": { 23 | "flowtype": { 24 | "onlyFilesWithFlowAnnotation": false 25 | }, 26 | "react": { 27 | "version": "16.3" 28 | } 29 | } 30 | }, 31 | { 32 | "files": ["**/*_test.js"], 33 | "rules": { 34 | "max-lines": "off" 35 | } 36 | } 37 | ], 38 | "rules": { 39 | "flowtype/no-types-missing-file-annotation": "error", 40 | "import/no-unresolved": "error", 41 | "import/named": "error", 42 | "import/no-absolute-path": "error", 43 | "import/no-self-import": "error", 44 | "import/no-useless-path-segments": "error", 45 | "import/no-named-as-default": "error", 46 | "import/no-named-as-default-member": "error", 47 | "import/no-deprecated": "error", 48 | "import/first": "error", 49 | "import/no-duplicates": "error", 50 | "import/newline-after-import": "error", 51 | "import/no-unassigned-import": "error", 52 | "import/no-named-default": "error", 53 | "import/extensions": [ 54 | "error", 55 | "always", 56 | { 57 | "ignorePackages": true 58 | } 59 | ], 60 | "import/no-commonjs": "error", 61 | "promise/always-return": "error", 62 | "promise/no-return-wrap": "error", 63 | "promise/param-names": "error", 64 | "promise/catch-or-return": "error", 65 | "promise/no-new-statics": "error", 66 | "promise/no-return-in-finally": "error", 67 | "eslint-comments/no-unused-disable": "error" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /coverage 3 | /dist 4 | /.vscode 5 | /bin 6 | /src/testdata 7 | 8 | [include] 9 | /src 10 | 11 | [libs] 12 | /flow-typed/**/*.js 13 | 14 | [options] 15 | emoji=true 16 | exact_by_default=true 17 | module.use_strict=true 18 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe 19 | suppress_comment=\\(.\\|\n\\)*\\$FlowIgnore 20 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The secrets files used for /render requests, graphite logging, etc. 2 | secret 3 | hostedgraphite.api_key 4 | 5 | #### git defaults: 6 | 7 | # Logs 8 | logs 9 | *.log 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directory 32 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 33 | node_modules 34 | 35 | # Output Dir 36 | dist -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.md 3 | docs 4 | *.css 5 | *.yml 6 | *.txt 7 | dist 8 | node_modules 9 | flow-typed 10 | .vscode 11 | *.yaml -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "parser": "flow", 4 | "tabWidth": 4, 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "all", 8 | "bracketSpacing": false, 9 | "arrowParens": "always" 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | // Use to override node with NVM. Must be exact version (can't just be 8 or 10) 10 | // "runtimeVersion": "10.17.0", 11 | "request": "launch", 12 | "runtimeArgs": ["--require", "@babel/register"], 13 | "name": "Launch Program", 14 | "console": "integratedTerminal", 15 | "outputCapture": "std", 16 | "sourceMaps": true, 17 | "program": "${workspaceFolder}/src/main.js", 18 | "args": [ 19 | "--dev", 20 | "--port=8040", 21 | "--use-cache", 22 | "--log-level=silly" 23 | ] 24 | }, 25 | { 26 | "type": "node", 27 | "request": "launch", 28 | "name": "Mocha Tests", 29 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 30 | "args": [ 31 | "-u", 32 | "tdd", 33 | "--timeout", 34 | "999999", 35 | "--compilers", 36 | "js:@babel/register", 37 | "--colors", 38 | "${file}" 39 | ], 40 | "internalConsoleOptions": "openOnSessionStart" 41 | } 42 | ] 43 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.renderWhitespace": "boundary", 3 | "eslint.autoFixOnSave": true, 4 | "eslint.enable": true, 5 | "files.trimTrailingWhitespace": true, 6 | "flow.useNPMPackagedFlow": true, 7 | "typescript.format.enable": false, 8 | "typescript.validate.enable": false, 9 | "javascript.validate.enable": false, 10 | "coverage-gutters.lcovname": "./coverage/lcov.info", 11 | "jest.enableSnapshotUpdateMessages": false, 12 | "jest.pathToConfig": "./config/jest/test.config.js", 13 | "editor.codeActionsOnSave": { 14 | "source.fixAll.eslint": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DEBUGGING: -------------------------------------------------------------------------------- 1 | https://cloud.google.com/appengine/docs/managed-vms/host-env#google-managed_vms_and_user-managed_vms 2 | 3 | To log into an ssh container running an instance, do something like 4 | this: 5 | 6 | - ssh to the machine via a page like 7 | https://console.developers.google.com/appengine/instances?project=khan-academy&moduleId=react-render 8 | - run `sudo docker ps` to find the container running google-fluentd, the logging agent 9 | - run `container_exec /bin/bash` 10 | - cd'd to /var/log/saved_docker 11 | - cd'd to the only directory there (whose name was a 64-char string of hex characters) 12 | - you can look at -json.log (you could parse this file to look for all entries with the "stream" attribute set to "stdout", "stderr", etc.) 13 | - you can also look at /var/log/nginx/access.log 14 | 15 | ------ 16 | 17 | To copy fixtures files to an existing instance: 18 | 19 | - find javascript -name '*\.fixture\.*' | xargs tar cf /tmp/fixtures.tar 20 | - gcloud compute instances list 21 | * find an instance for an old vesion we're no longer using 22 | * Also note the zone it's in, we'll need it for later. 23 | - gcloud app modules set-managed-by react-render --version --self 24 | * you can get the version from the instance name, just turn '--' into '-' 25 | - cat /tmp/fixtures.tar | gcloud compute --project "khan-academy" ssh --zone --command 'cat > fixtures.tar' 26 | - gcloud compute --project "khan-academy" ssh --zone 27 | - CONTAINER_ID=`sudo docker ps | grep npm | awk '{print $1}'` 28 | - sudo docker cp fixtures.tar ${CONTAINER_ID}:/app/fixtures.tar 29 | - container_exec $CONTAINER_ID /bin/bash 30 | - cd app 31 | - tar xf fixtures.tar 32 | - export PATH=/nodejs/bin:$PATH 33 | - Bob's your uncle! 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Create conf files for nginx and pm2. This is done using a multi-stage build so 2 | # that we don't need to have any dependencies used to generate the conf files 3 | # (in this case python) in the container. 4 | FROM python:3 as config 5 | WORKDIR /usr/src/app 6 | COPY . . 7 | 8 | # Generate config files passing the number of node servers we want to run on 9 | # each instance. 10 | RUN python generate_config_files.py 4 11 | 12 | # Dockerfile extending the generic Node image with application files for a 13 | # single application. 14 | FROM gcr.io/google_appengine/nodejs 15 | # Check to see if the the version included in the base runtime satisfies 16 | # '^8.3.0', if not then do an npm install of the latest available 17 | # version that satisfies it. 18 | RUN /usr/local/bin/install_node '^8.3.0' 19 | COPY . /app/ 20 | # You have to specify "--unsafe-perm" with npm install 21 | # when running as root. Failing to do this can cause 22 | # install to appear to succeed even if a preinstall 23 | # script fails, and may have other adverse consequences 24 | # as well. 25 | # This command will also cat the npm-debug.log file after the 26 | # build, if it exists. 27 | RUN npm install --unsafe-perm || \ 28 | ((if [ -f npm-debug.log ]; then \ 29 | cat npm-debug.log; \ 30 | fi) && false) 31 | 32 | # Install pm2 33 | RUN npm install --unsafe-perm pm2@latest -g 34 | 35 | # Set up the nginx reverse proxy. We need a more recent version of nginx than is 36 | # available from the regular sources. 37 | COPY nginx.list /etc/apt/sources.list.d/nginx.list 38 | RUN curl http://nginx.org/keys/nginx_signing.key | apt-key add - 39 | RUN apt-get update && \ 40 | apt-get install -y -q --no-install-recommends nginx && \ 41 | apt-get clean && \ 42 | rm -r /var/lib/apt/lists/* 43 | 44 | # Copy conf files from build stage 45 | COPY --from=config /usr/src/app/nginx.conf /etc/nginx/nginx.conf 46 | COPY --from=config /usr/src/app/processes.json /app/ 47 | 48 | # Start ngnix, node 49 | CMD ["pm2-docker", "processes.json"] 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Khan Academy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-render-server 2 | 3 | react-render-server was a node.js server used to support server-side rendering at Khan Academy. It's no longer used at this time. 4 | -------------------------------------------------------------------------------- /app-flex.yaml: -------------------------------------------------------------------------------- 1 | service: react-render 2 | runtime: custom 3 | env: flex 4 | 5 | # TODO(jlfwong): Figure out what the right scaling policy is 6 | automatic_scaling: 7 | min_num_instances: 1 8 | max_num_instances: 200 9 | cool_down_period_sec: 60 10 | cpu_utilization: 11 | target_utilization: 0.4 12 | 13 | resources: 14 | cpu: 1 15 | # TODO(benkraft): This feels like more memory than we should need! 16 | memory_gb: 3 17 | disk_size_gb: 10 18 | -------------------------------------------------------------------------------- /app-standard.yaml: -------------------------------------------------------------------------------- 1 | service: react-render 2 | runtime: nodejs10 3 | default_expiration: "30m" # per ADR-52 4 | 5 | instance_class: F4 6 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // @noflow 2 | // This is only needed when deploying to Appengine Standard 3 | // (app.js is the hard-coded entrypoint for appengine-standard apps). 4 | // eslint-disable-next-line import/no-unassigned-import, import/no-commonjs 5 | require("./dist/main.js"); 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // @noflow 2 | 3 | /** 4 | * The configuration for babel transpilation. 5 | * 6 | * Here we ensure that babel retranspiles files if the node version or the 7 | * node environment are different. 8 | */ 9 | 10 | /** 11 | * This file will not get transpiled, so we have to be cautious in the syntax 12 | * we use. 13 | */ 14 | // eslint-disable-next-line import/no-commonjs 15 | module.exports = function(api) { 16 | /** 17 | * Determine the major version of NodeJS that is executing. 18 | * The version is of the form v8.16.2, for example. 19 | */ 20 | const nodeMajorVersion = process.version.split(".")[0].replace("v", ""); 21 | // eslint-disable-next-line no-console 22 | console.log(`Transpiling for NodeJS ${nodeMajorVersion}`); 23 | 24 | /** 25 | * Cache based on the major node version and the environment. 26 | */ 27 | api.cache.using(() => `${nodeMajorVersion}:${process.env.NODE_ENV}`); 28 | 29 | /** 30 | * Some common options. 31 | */ 32 | const options = { 33 | comments: false, 34 | sourceMaps: true, 35 | retainLines: true, 36 | }; 37 | 38 | /** 39 | * We're going to compile based on the node version executing. 40 | */ 41 | const presets = [ 42 | [ 43 | "@babel/preset-env", 44 | { 45 | targets: { 46 | node: nodeMajorVersion, 47 | }, 48 | }, 49 | ], 50 | "@babel/preset-flow", 51 | ]; 52 | 53 | /** 54 | * Plugins to use. 55 | */ 56 | const plugins = ["@babel/plugin-proposal-class-properties"]; 57 | 58 | const config = { 59 | presets, 60 | plugins, 61 | ...options, 62 | }; 63 | return config; 64 | }; 65 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # Deploy the latest commit to AppEngine 4 | 5 | : ${PROJECT:=khan-academy} 6 | : ${VERBOSITY:=info} 7 | : ${DOCKER:=} 8 | 9 | die() { 10 | echo "FATAL ERROR: $@" 11 | exit 1 12 | } 13 | 14 | # Calculate the version name for the latest commit 15 | # Format is: YYMMDD-HHMM-RRRRRRRRRRRR 16 | # 17 | # Keep this in sync with VERSION in set_default.sh 18 | VERSION=`git log -n1 --format="format:%H %ct" | perl -ne '$ENV{TZ} = "US/Pacific"; ($rev, $t) = split; @lt = localtime($t); printf "%02d%02d%02d-%02d%02d-%.12s\n", $lt[5] % 100, $lt[4] + 1, $lt[3], $lt[2], $lt[1], $rev'` 19 | 20 | # Ensure the 'secret' file exists (so we can verify /render requests) 21 | [ -s "secret" ] \ 22 | || die "You must create a file called 'secret' with the secret from\n https://phabricator.khanacademy.org/K121" 23 | 24 | # Ensure the repository isn't dirty 25 | [ `git status -u -s | wc -c` -eq 0 ] \ 26 | || die "You must commit your changes before deploying." 27 | 28 | # Don't deploy if tests fail 29 | npm test 30 | 31 | # Use the default value for use_appengine_api. This is configuration set by 32 | # deployment of webapp. 33 | gcloud config set "app/use_appengine_api" "True" 34 | 35 | # Yay we're good to go! 36 | if [ -n "$DOCKER" ]; then 37 | echo "Building docker image..." 38 | docker build -f Dockerfile -t react-render-server . 39 | docker tag react-render-server "us.gcr.io/khan-academy/react-render-server-$VERSION" 40 | 41 | echo "Pushing docker image..." 42 | gcloud docker -- push "us.gcr.io/khan-academy/react-render-server-$VERSION" 43 | 44 | echo "Deploying ${VERSION} via docker..." 45 | 46 | gcloud -q --verbosity "${VERBOSITY}" app deploy app-flex.yaml \ 47 | --project "$PROJECT" --version "$VERSION" --no-promote \ 48 | --image-url=us.gcr.io/khan-academy/react-render-server-$VERSION 49 | else 50 | gcloud -q --verbosity "${VERBOSITY}" app deploy app-standard.yaml \ 51 | --project "$PROJECT" --version "$VERSION" --no-promote 52 | fi 53 | 54 | echo "DONE" 55 | -------------------------------------------------------------------------------- /eslint/README.md: -------------------------------------------------------------------------------- 1 | The files in this directory were copied from https://github.com/Khan/khan-linter/tree/60889f7a1c86e9eb7f3154184a89504a3d2163f8. 2 | 3 | To update, copy over the files and update github link in this file. 4 | -------------------------------------------------------------------------------- /eslint/eslintrc: -------------------------------------------------------------------------------- 1 | // These are rules that apply to javascript files, particularly for 2 | // KA webapp. These files use ES6 constructs, which get transpiled 3 | // down to ES5 before deploying. They are also often jsx files. 4 | // They also allow a lot of globals because they use third-party 5 | // packages that expose globals, or for backwards-compatibility 6 | // reasons. 7 | // 8 | // If you want to adjust/add a rule that is not ES6-, flow-, or jsx-specific, 9 | // modify eslintrc.shared instead. In particular, when adjusting the 10 | // list of built-in globals (useful for both javascript files and 11 | //