From 0c421df0f359323650807bca3e5fb9599405df4d Mon Sep 17 00:00:00 2001 From: Kevin Grandon Date: Tue, 1 May 2018 16:37:38 -0700 Subject: [PATCH] New docs (#11) --- .gitignore | 7 +- .prettierignore | 1 - Gemfile | 3 - Gemfile.lock | 206 - README.md | 43 +- _config.yml | 29 - _layouts/default.html | 85 - assets/css/style.scss | 79 - bootstrap.sh | 17 + content/README.md | 26 + .../creating-a-plugin.md | 0 .../creating-endpoints.md | 0 .../creating-providers.md | 0 dependencies.md => content/dependencies.md | 0 .../framework-comparison.md | 0 .../getting-started.md | 0 .../modifying-html-template.md | 0 tokens.md => content/tokens.md | 0 .../universal-code.md | 0 .../virtual-modules.md | 0 .../working-with-secrets.md | 0 docs/_static/metadata.json | 6 + documentation/api/plugins.md | 26 + .../docs/getting-started/create-a-project.md | 14 + .../docs/getting-started/environment-setup.md | 85 + documentation/docs/getting-started/index.md | 27 + .../docs/getting-started/project-structure.md | 69 + .../getting-started/required-knowledge.md | 28 + .../docs/getting-started/run-your-project.md | 130 + .../docs/getting-started/why-fusion.md | 66 + documentation/docs/guides/configuration.md | 24 + documentation/docs/guides/debugging.md | 32 + documentation/docs/guides/fetching-data.md | 12 + documentation/docs/guides/forms.md | 226 + documentation/docs/guides/index.md | 24 + .../docs/guides/internationalization.md | 54 + documentation/docs/guides/performance.md | 156 + documentation/docs/guides/routing.md | 335 + documentation/docs/guides/security.md | 66 + documentation/docs/guides/server-code.md | 14 + documentation/docs/guides/state-management.md | 78 + documentation/docs/guides/static-assets.md | 85 + documentation/docs/guides/styling.md | 228 + .../docs/guides/testing/component.md | 201 + documentation/docs/guides/testing/index.md | 26 + .../docs/guides/testing/integration.md | 98 + .../docs/guides/testing/simulation.md | 78 + documentation/docs/guides/testing/snapshot.md | 32 + documentation/docs/guides/testing/unit.md | 102 + documentation/docs/guides/typing.md | 118 + .../docs/guides/universal-rendering.md | 154 + gatsby-browser.js | 14 + gatsby-config.js | 81 + gatsby-node.js | 60 + gatsby-ssr.js | 34 + package.json | 64 +- packages-oss.txt | 26 + .../gatsby-remark-transform-links/index.js | 37 + .../package.json | 4 + src/components/footer.js | 62 + src/components/main-nav.js | 68 + src/components/side-nav.js | 118 + src/components/style-settings.js | 13 + src/components/styled-elements.js | 91 + src/components/uber-logo.js | 57 + src/css/normalize.css | 332 + src/css/prism.css | 167 + src/html.js | 73 + src/images/alarm-clock.svg | 1 + src/images/amsmith.jpg | Bin 0 -> 6671 bytes src/images/angus.png | Bin 0 -> 12451 bytes src/images/bug.svg | 1 + src/images/chat-message.svg | 1 + src/images/dennis.lin.jpg | Bin 0 -> 22315 bytes src/images/ganemone.png | Bin 0 -> 162096 bytes src/images/github.svg | 1 + src/images/keving.jpeg | Bin 0 -> 28200 bytes src/images/lhorie.jpg | Bin 0 -> 2518 bytes src/images/mlmorg.jpeg | Bin 0 -> 18975 bytes src/images/nadiia.jpg | Bin 0 -> 32741 bytes src/images/people.svg | 1 + src/images/rtsao.jpg | Bin 0 -> 1826 bytes src/images/segu.jpg | Bin 0 -> 2736 bytes src/images/stack.svg | 11 + src/images/support.svg | 227 + src/images/uchat.png | Bin 0 -> 3841 bytes src/images/uchat.svg | 1 + src/images/web-dev-group.svg | 207 + src/layouts/index.js | 147 + src/layouts/styled-elements.js | 150 + src/nav-api.yml | 92 + src/nav-docs.yml | 65 + src/pages/404.js | 10 + src/pages/index.js | 154 + src/pages/support/index.js | 123 + src/pages/team/index.js | 92 + src/server/index.js | 19 + src/team.js | 63 + src/templates/doc.js | 40 + src/utils/index.js | 90 + static/favicon.ico | Bin 0 -> 1150 bytes yarn.lock | 9659 +++++++++++++++++ 102 files changed, 15118 insertions(+), 428 deletions(-) delete mode 100644 .prettierignore delete mode 100644 Gemfile delete mode 100644 Gemfile.lock delete mode 100644 _config.yml delete mode 100644 _layouts/default.html delete mode 100644 assets/css/style.scss create mode 100755 bootstrap.sh create mode 100644 content/README.md rename creating-a-plugin.md => content/creating-a-plugin.md (100%) rename creating-endpoints.md => content/creating-endpoints.md (100%) rename creating-providers.md => content/creating-providers.md (100%) rename dependencies.md => content/dependencies.md (100%) rename framework-comparison.md => content/framework-comparison.md (100%) rename getting-started.md => content/getting-started.md (100%) rename modifying-html-template.md => content/modifying-html-template.md (100%) rename tokens.md => content/tokens.md (100%) rename universal-code.md => content/universal-code.md (100%) rename virtual-modules.md => content/virtual-modules.md (100%) rename working-with-secrets.md => content/working-with-secrets.md (100%) create mode 100644 docs/_static/metadata.json create mode 100644 documentation/api/plugins.md create mode 100644 documentation/docs/getting-started/create-a-project.md create mode 100644 documentation/docs/getting-started/environment-setup.md create mode 100644 documentation/docs/getting-started/index.md create mode 100644 documentation/docs/getting-started/project-structure.md create mode 100644 documentation/docs/getting-started/required-knowledge.md create mode 100644 documentation/docs/getting-started/run-your-project.md create mode 100644 documentation/docs/getting-started/why-fusion.md create mode 100644 documentation/docs/guides/configuration.md create mode 100644 documentation/docs/guides/debugging.md create mode 100644 documentation/docs/guides/fetching-data.md create mode 100644 documentation/docs/guides/forms.md create mode 100644 documentation/docs/guides/index.md create mode 100644 documentation/docs/guides/internationalization.md create mode 100644 documentation/docs/guides/performance.md create mode 100644 documentation/docs/guides/routing.md create mode 100644 documentation/docs/guides/security.md create mode 100644 documentation/docs/guides/server-code.md create mode 100644 documentation/docs/guides/state-management.md create mode 100644 documentation/docs/guides/static-assets.md create mode 100644 documentation/docs/guides/styling.md create mode 100644 documentation/docs/guides/testing/component.md create mode 100644 documentation/docs/guides/testing/index.md create mode 100644 documentation/docs/guides/testing/integration.md create mode 100644 documentation/docs/guides/testing/simulation.md create mode 100644 documentation/docs/guides/testing/snapshot.md create mode 100644 documentation/docs/guides/testing/unit.md create mode 100644 documentation/docs/guides/typing.md create mode 100644 documentation/docs/guides/universal-rendering.md create mode 100644 gatsby-browser.js create mode 100644 gatsby-config.js create mode 100644 gatsby-node.js create mode 100644 gatsby-ssr.js create mode 100644 packages-oss.txt create mode 100644 plugins/gatsby-remark-transform-links/index.js create mode 100644 plugins/gatsby-remark-transform-links/package.json create mode 100644 src/components/footer.js create mode 100644 src/components/main-nav.js create mode 100644 src/components/side-nav.js create mode 100644 src/components/style-settings.js create mode 100644 src/components/styled-elements.js create mode 100644 src/components/uber-logo.js create mode 100644 src/css/normalize.css create mode 100644 src/css/prism.css create mode 100644 src/html.js create mode 100644 src/images/alarm-clock.svg create mode 100644 src/images/amsmith.jpg create mode 100644 src/images/angus.png create mode 100644 src/images/bug.svg create mode 100644 src/images/chat-message.svg create mode 100644 src/images/dennis.lin.jpg create mode 100644 src/images/ganemone.png create mode 100644 src/images/github.svg create mode 100644 src/images/keving.jpeg create mode 100644 src/images/lhorie.jpg create mode 100644 src/images/mlmorg.jpeg create mode 100644 src/images/nadiia.jpg create mode 100644 src/images/people.svg create mode 100644 src/images/rtsao.jpg create mode 100644 src/images/segu.jpg create mode 100644 src/images/stack.svg create mode 100644 src/images/support.svg create mode 100644 src/images/uchat.png create mode 100644 src/images/uchat.svg create mode 100644 src/images/web-dev-group.svg create mode 100644 src/layouts/index.js create mode 100644 src/layouts/styled-elements.js create mode 100644 src/nav-api.yml create mode 100644 src/nav-docs.yml create mode 100644 src/pages/404.js create mode 100644 src/pages/index.js create mode 100644 src/pages/support/index.js create mode 100644 src/pages/team/index.js create mode 100644 src/server/index.js create mode 100644 src/team.js create mode 100644 src/templates/doc.js create mode 100644 src/utils/index.js create mode 100644 static/favicon.ico create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore index c08f9ad..96dbd01 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ -_site \ No newline at end of file +.cache +fusion/ +node_modules +public +docs/_build + diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index f58c80e..0000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -assets/css/ \ No newline at end of file diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 9ee354e..0000000 --- a/Gemfile +++ /dev/null @@ -1,3 +0,0 @@ -source 'https://rubygems.org' -gem 'github-pages', group: :jekyll_plugins -gem 'jekyll-seo-tag', group: :jekyll_plugins diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 5a9cb78..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,206 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - activesupport (4.2.8) - i18n (~> 0.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - addressable (2.5.2) - public_suffix (>= 2.0.2, < 4.0) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.12.2) - colorator (1.1.0) - concurrent-ruby (1.0.5) - ethon (0.11.0) - ffi (>= 1.3.0) - execjs (2.7.0) - faraday (0.15.0) - multipart-post (>= 1.2, < 3) - ffi (1.9.23) - forwardable-extended (2.6.0) - gemoji (3.0.0) - github-pages (146) - activesupport (= 4.2.8) - github-pages-health-check (= 1.3.5) - jekyll (= 3.4.5) - jekyll-avatar (= 0.4.2) - jekyll-coffeescript (= 1.0.1) - jekyll-default-layout (= 0.1.4) - jekyll-feed (= 0.9.2) - jekyll-gist (= 1.4.0) - jekyll-github-metadata (= 2.5.1) - jekyll-mentions (= 1.2.0) - jekyll-optional-front-matter (= 0.2.0) - jekyll-paginate (= 1.1.0) - jekyll-readme-index (= 0.1.0) - jekyll-redirect-from (= 0.12.1) - jekyll-relative-links (= 0.4.1) - jekyll-sass-converter (= 1.5.0) - jekyll-seo-tag (= 2.2.3) - jekyll-sitemap (= 1.0.0) - jekyll-swiss (= 0.4.0) - jekyll-theme-architect (= 0.0.4) - jekyll-theme-cayman (= 0.0.4) - jekyll-theme-dinky (= 0.0.4) - jekyll-theme-hacker (= 0.0.4) - jekyll-theme-leap-day (= 0.0.4) - jekyll-theme-merlot (= 0.0.4) - jekyll-theme-midnight (= 0.0.4) - jekyll-theme-minimal (= 0.0.4) - jekyll-theme-modernist (= 0.0.4) - jekyll-theme-primer (= 0.3.1) - jekyll-theme-slate (= 0.0.4) - jekyll-theme-tactile (= 0.0.4) - jekyll-theme-time-machine (= 0.0.4) - jekyll-titles-from-headings (= 0.2.0) - jemoji (= 0.8.0) - kramdown (= 1.13.2) - liquid (= 3.0.6) - listen (= 3.0.6) - mercenary (~> 0.3) - minima (= 2.1.1) - rouge (= 1.11.1) - terminal-table (~> 1.4) - github-pages-health-check (1.3.5) - addressable (~> 2.3) - net-dns (~> 0.8) - octokit (~> 4.0) - public_suffix (~> 2.0) - typhoeus (~> 0.7) - html-pipeline (2.7.1) - activesupport (>= 2) - nokogiri (>= 1.4) - i18n (0.9.5) - concurrent-ruby (~> 1.0) - jekyll (3.4.5) - addressable (~> 2.4) - colorator (~> 1.0) - jekyll-sass-converter (~> 1.0) - jekyll-watch (~> 1.1) - kramdown (~> 1.3) - liquid (~> 3.0) - mercenary (~> 0.3.3) - pathutil (~> 0.9) - rouge (~> 1.7) - safe_yaml (~> 1.0) - jekyll-avatar (0.4.2) - jekyll (~> 3.0) - jekyll-coffeescript (1.0.1) - coffee-script (~> 2.2) - jekyll-default-layout (0.1.4) - jekyll (~> 3.0) - jekyll-feed (0.9.2) - jekyll (~> 3.3) - jekyll-gist (1.4.0) - octokit (~> 4.2) - jekyll-github-metadata (2.5.1) - jekyll (~> 3.1) - octokit (~> 4.0, != 4.4.0) - jekyll-mentions (1.2.0) - activesupport (~> 4.0) - html-pipeline (~> 2.3) - jekyll (~> 3.0) - jekyll-optional-front-matter (0.2.0) - jekyll (~> 3.0) - jekyll-paginate (1.1.0) - jekyll-readme-index (0.1.0) - jekyll (~> 3.0) - jekyll-redirect-from (0.12.1) - jekyll (~> 3.3) - jekyll-relative-links (0.4.1) - jekyll (~> 3.3) - jekyll-sass-converter (1.5.0) - sass (~> 3.4) - jekyll-seo-tag (2.2.3) - jekyll (~> 3.3) - jekyll-sitemap (1.0.0) - jekyll (~> 3.3) - jekyll-swiss (0.4.0) - jekyll-theme-architect (0.0.4) - jekyll (~> 3.3) - jekyll-theme-cayman (0.0.4) - jekyll (~> 3.3) - jekyll-theme-dinky (0.0.4) - jekyll (~> 3.3) - jekyll-theme-hacker (0.0.4) - jekyll (~> 3.3) - jekyll-theme-leap-day (0.0.4) - jekyll (~> 3.3) - jekyll-theme-merlot (0.0.4) - jekyll (~> 3.3) - jekyll-theme-midnight (0.0.4) - jekyll (~> 3.3) - jekyll-theme-minimal (0.0.4) - jekyll (~> 3.3) - jekyll-theme-modernist (0.0.4) - jekyll (~> 3.3) - jekyll-theme-primer (0.3.1) - jekyll (~> 3.3) - jekyll-theme-slate (0.0.4) - jekyll (~> 3.3) - jekyll-theme-tactile (0.0.4) - jekyll (~> 3.3) - jekyll-theme-time-machine (0.0.4) - jekyll (~> 3.3) - jekyll-titles-from-headings (0.2.0) - jekyll (~> 3.3) - jekyll-watch (1.5.0) - listen (~> 3.0, < 3.1) - jemoji (0.8.0) - activesupport (~> 4.0) - gemoji (~> 3.0) - html-pipeline (~> 2.2) - jekyll (>= 3.0) - kramdown (1.13.2) - liquid (3.0.6) - listen (3.0.6) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9.7) - mercenary (0.3.6) - mini_portile2 (2.1.0) - minima (2.1.1) - jekyll (~> 3.3) - minitest (5.11.3) - multipart-post (2.0.0) - net-dns (0.8.0) - nokogiri (1.6.8.1) - mini_portile2 (~> 2.1.0) - octokit (4.8.0) - sawyer (~> 0.8.0, >= 0.5.3) - pathutil (0.16.1) - forwardable-extended (~> 2.6) - public_suffix (2.0.5) - rb-fsevent (0.10.3) - rb-inotify (0.9.10) - ffi (>= 0.5.0, < 2) - rouge (1.11.1) - safe_yaml (1.0.4) - sass (3.5.6) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sawyer (0.8.1) - addressable (>= 2.3.5, < 2.6) - faraday (~> 0.8, < 1.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) - thread_safe (0.3.6) - typhoeus (0.8.0) - ethon (>= 0.8.0) - tzinfo (1.2.5) - thread_safe (~> 0.1) - unicode-display_width (1.3.2) - -PLATFORMS - ruby - -DEPENDENCIES - github-pages - jekyll-seo-tag - -BUNDLED WITH - 1.15.4 diff --git a/README.md b/README.md index bec6846..69e29e4 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,33 @@ -# What is Fusion.js +# Fusion.js Documentation -Fusion.js is a web application framework developed by Uber. Because Uber operates on a large scale in countries with slow mobile networks, performance is a big driving factor for Fusion.js. +## Contribute -Fusion.js has a modular architecture to promote small bundle sizes, and is designed in such a way that performance milestones in its development pipeline can be rolled out to consumers via version bumps, as opposed to requiring big migrations or entirely offloading that work to app developers. +Install the dependencies: -Here are the features you'll find in Fusion.js: +`yarn install` -* server side rendering and async rendering -* ES2017 and JSX support out of the box -* hot module reloading in development mode -* bundle splitting -* universal rendering (run the same code in the server and the browser) -* server-side development via Koa.js -* plugin-based architecture (so you only include what you need in your browser bundles) -* a curated set of plugins for data fetching, styling, etc maintained by the Fusion.js team -* plugins for error logging, security, etc. -* bundle analysis tooling +Bootstrap: -If you want to know how Fusion.js compares to similar projects, see the [framework comparison page](framework-comparison.md). +`yarn boostrap` ---- +It clones all the documentation. -### Next steps +Run in dev mode: -* [Getting started](getting-started.md) +`yarn dev` + +The `replaceRenderer` from `gatsby-ssr.js` is not called during a development build (https://github.com/gatsbyjs/gatsby/issues/3166), that results in a Styletron error `"Uncaught TypeError: Cannot read property 'sheet' of undefined"`. +As a temporary solution while in dev mode, remove the `styleElements` from passing to the client's Styletron instance in the `gatsby-browser.js`: +`const styletron = new Styletron(styleElements);` => `const styletron = new Styletron();`. + +Build docs website locally: + +`yarn build-docs` + +To add a new package to render its documentation: +- for public packages from github add the repo name to `packages-oss.txt`; + +Then add a newly added package's docs to the side navigation menu in `/src/nav-api.yml`. +A doc page of README of a package will be created with the path `/api/[package_name]`. Additional documentation for the package can live under its `/docs` folder, and pages for that documentation will be created with a full path like `/api/[package_name]/docs/[file_name]`. + +While adding or removing any documentation files in this repo under `/documentation` folder, don't forget to add/remove a side menu items to the docs in `/src/nav-docs.yml`. diff --git a/_config.yml b/_config.yml deleted file mode 100644 index 396ebf7..0000000 --- a/_config.yml +++ /dev/null @@ -1,29 +0,0 @@ -theme: jekyll-theme-slate -title: FusionJS Documentation -main_nav: - - page: What is Fusion.js - url: / - children: - - page: 'Framework comparison' - url: '/framework-comparison.html' - - page: 'Getting started' - url: '/getting-started.html' - - page: 'Universal code' - url: '/universal-code.html' - - page: 'Creating a plugin' - url: '/creating-a-plugin.html' - children: - - page: 'Tokens' - url: '/tokens.html' - - page: 'Dependencies' - url: '/dependencies.html' - - page: 'Creating endpoints' - url: '/creating-endpoints.html' - - page: 'Creating providers' - url: '/creating-providers.html' - - page: 'Modifying the HTML template' - url: '/modifying-html-template.html' - - page: 'Working with secrets' - url: '/working-with-secrets.html' - - page: 'Virtual modules' - url: '/virtual-modules.html' diff --git a/_layouts/default.html b/_layouts/default.html deleted file mode 100644 index cbf2878..0000000 --- a/_layouts/default.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - {% seo %} - - - - - -
-
- View on GitHub - -

- {{ site.title | default: site.github.repository_name }} -

- - - {% if site.show_downloads %} -
- Download this project as a .zip file - Download this project as a tar.gz file -
- {% endif %} -
-
- - -
-
- -
- {{ content }} -
-
-
- - - - - {% if site.google_analytics %} - {% endif %} - - - \ No newline at end of file diff --git a/assets/css/style.scss b/assets/css/style.scss deleted file mode 100644 index 5c65d10..0000000 --- a/assets/css/style.scss +++ /dev/null @@ -1,79 +0,0 @@ ---- ---- - -@import "{{ site.theme }}"; - -html * { - font-family: ff-clan-web-pro, '-apple-system', BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, - Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'sans-serif'; -} - -#header_wrap .inner { - padding: 15px 10px 15px 10px; - - h1 a { - color: #FFF; - text-decoration: none; - } -} - -.inner { - width: auto; - max-width: 100%; -} - -#main_content_wrap { - background: #fff; - padding: 0 4%; - box-sizing: border-box; -} - -#main_content_inner { - max-width: 100%; - margin: 0 auto; - display: flex; - justify-content: space-between; - -webkit-box-pack: justify; - - nav { - background: #F0F0EF; - padding: 50px 36px 0 999px; - margin: 0 36px 0 -999px; - max-width: 310px; - - ul { - list-style-type: none; - margin: 0; - padding: 0 0 0 16px; - - li { - a { - display: inline-block; - padding: 11px 11px; - color: #999; - font-size: 16px; - - &:hover { - color: #4db5d9; - text-decoration: none; - font-weight: bold; - } - } - - &.active > a { - color: #000; - font-weight: bold; - } - - &.active > a:before { - border-left: 4px solid #4db5d9; - } - } - } - } - - #main_content { - flex: 1; - padding: 40px 0; - } -} diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100755 index 0000000..d592f57 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -ex + +oss=packages-oss.txt +dest=fusion + +rm -rf $dest +mkdir -p $dest + +while read -r repo +do + git clone https://github.com/fusionjs/$repo.git $dest/$repo +done < $oss + +# Custom handling for fusionjs.github.io +mkdir -p fusion/fusion-docs +cp -R content/* fusion/fusion-docs/. diff --git a/content/README.md b/content/README.md new file mode 100644 index 0000000..bec6846 --- /dev/null +++ b/content/README.md @@ -0,0 +1,26 @@ +# What is Fusion.js + +Fusion.js is a web application framework developed by Uber. Because Uber operates on a large scale in countries with slow mobile networks, performance is a big driving factor for Fusion.js. + +Fusion.js has a modular architecture to promote small bundle sizes, and is designed in such a way that performance milestones in its development pipeline can be rolled out to consumers via version bumps, as opposed to requiring big migrations or entirely offloading that work to app developers. + +Here are the features you'll find in Fusion.js: + +* server side rendering and async rendering +* ES2017 and JSX support out of the box +* hot module reloading in development mode +* bundle splitting +* universal rendering (run the same code in the server and the browser) +* server-side development via Koa.js +* plugin-based architecture (so you only include what you need in your browser bundles) +* a curated set of plugins for data fetching, styling, etc maintained by the Fusion.js team +* plugins for error logging, security, etc. +* bundle analysis tooling + +If you want to know how Fusion.js compares to similar projects, see the [framework comparison page](framework-comparison.md). + +--- + +### Next steps + +* [Getting started](getting-started.md) diff --git a/creating-a-plugin.md b/content/creating-a-plugin.md similarity index 100% rename from creating-a-plugin.md rename to content/creating-a-plugin.md diff --git a/creating-endpoints.md b/content/creating-endpoints.md similarity index 100% rename from creating-endpoints.md rename to content/creating-endpoints.md diff --git a/creating-providers.md b/content/creating-providers.md similarity index 100% rename from creating-providers.md rename to content/creating-providers.md diff --git a/dependencies.md b/content/dependencies.md similarity index 100% rename from dependencies.md rename to content/dependencies.md diff --git a/framework-comparison.md b/content/framework-comparison.md similarity index 100% rename from framework-comparison.md rename to content/framework-comparison.md diff --git a/getting-started.md b/content/getting-started.md similarity index 100% rename from getting-started.md rename to content/getting-started.md diff --git a/modifying-html-template.md b/content/modifying-html-template.md similarity index 100% rename from modifying-html-template.md rename to content/modifying-html-template.md diff --git a/tokens.md b/content/tokens.md similarity index 100% rename from tokens.md rename to content/tokens.md diff --git a/universal-code.md b/content/universal-code.md similarity index 100% rename from universal-code.md rename to content/universal-code.md diff --git a/virtual-modules.md b/content/virtual-modules.md similarity index 100% rename from virtual-modules.md rename to content/virtual-modules.md diff --git a/working-with-secrets.md b/content/working-with-secrets.md similarity index 100% rename from working-with-secrets.md rename to content/working-with-secrets.md diff --git a/docs/_static/metadata.json b/docs/_static/metadata.json new file mode 100644 index 0000000..2056100 --- /dev/null +++ b/docs/_static/metadata.json @@ -0,0 +1,6 @@ +{ + "category": "web", + "tags": ["web", "web app", "web architecture", "fusion", "tutorial"], + "title": "Fusion.js documentation", + "description": "Learn about Fusion.js" +} diff --git a/documentation/api/plugins.md b/documentation/api/plugins.md new file mode 100644 index 0000000..9a5db29 --- /dev/null +++ b/documentation/api/plugins.md @@ -0,0 +1,26 @@ +--- +title: Plugins +path: /plugins/ +--- + +# Plugins + +* [Browser performance emitter](/api/fusion-plugin-browser-performance-emitter) +* [CSRF protection](/api/fusion-plugin-csrf-protection) +* [CSRF protection for React](/api/fusion-plugin-csrf-protection-react) +* [Error handling](/api/fusion-plugin-error-handling) +* [Font loading](/api/fusion-plugin-font-loader-react) +* [i18n](/api/fusion-plugin-i18n) +* [i18n for React](/api/fusion-plugin-i18n-react) +* [JWT](/api/fusion-plugin-jwt) +* [Node performance emitter](/api/fusion-plugin-node-performance-emitter) +* [React router](/api/fusion-plugin-react-router) +* [React/Redux](/api/fusion-plugin-react-redux) +* [Redux action emitter enhancer](/api/fusion-plugin-redux-action-emitter-enhancer) +* [RPC](/api/fusion-plugin-rpc) +* [RPC/Redux](/api/fusion-rpc-redux) +* [RPC/Redux/React](/api/fusion-plugin-rpc-redux-react) +* [Styletron for React](/api/fusion-plugin-styletron-react) +* [Universal events](/api/fusion-plugin-universal-events) +* [Universal events for React](/api/fusion-plugin-universal-events-react) +* [Universal logger](/api/fusion-plugin-universal-logger) diff --git a/documentation/docs/getting-started/create-a-project.md b/documentation/docs/getting-started/create-a-project.md new file mode 100644 index 0000000..336a811 --- /dev/null +++ b/documentation/docs/getting-started/create-a-project.md @@ -0,0 +1,14 @@ +--- +title: Create a project +path: /create-a-project/ +--- + +# Create a project + +The easiest way to create a project is to clone one of our boilerplate applications here: https://github.com/kevingrandon/fusion-boilerplate + +We are working on creating tools which will automatically scaffold a new application in the future. + +### Next steps + +* [Project structure](/docs/getting-started/project-structure) diff --git a/documentation/docs/getting-started/environment-setup.md b/documentation/docs/getting-started/environment-setup.md new file mode 100644 index 0000000..b8e13fb --- /dev/null +++ b/documentation/docs/getting-started/environment-setup.md @@ -0,0 +1,85 @@ +--- +title: Environment setup +path: /environment-setup/ +--- + +# Environment setup + +All initial prototyping and development should be performed on your local +machine. Web projects use Node.js for transpilation, bundling, and HTTP request +handling, and they use the NPM ecosystem for various pieces of functionality, +from client-side libraries to developer tooling. + +### Global dependency versions + +We have standardized around package.json's `engines` fields as a way to specify +the intended versions for building and running a Web App/Service. We recommend that you keep them up-to-date. +An example might look like: + +```json +{ + "engines": { + "node": "8.11.1", + "npm": "5.6.0", + "yarn": "1.6.0" + } +} +``` + +#### Node.js + +Make sure you're on the latest LTS release of Node.js. To check your Node.js +version, run: + +```sh +node --version +``` + +You can use [`nvm`](https://github.com/creationix/nvm) to manage Node.js +versions locally. `nvm` is a common tool used to switch Node versions and manage +environments between different versions. + +#### Yarn + +We recommend that you use [yarn](https://yarnpkg.com/en/) for package +management. The Yarn team recommends installing the executable via +[brew](https://yarnpkg.com/en/docs/install#mac-tab) on MacOS. + +To install it, run + +```sh +brew install yarn +``` + +To verify your yarn version, run: + +```sh +yarn --version +``` + +When you need to upgrade your yarn version, run + +```sh +brew upgrade yarn +``` +--- + +### Troubleshooting + +**What versions of Node.js and npm can I use for my web frontend service?** + +We recommend using the latest LTS release. + +**What is SyntaxError: Unexpected token and how do I fix it?** + +Some JS syntax in your project or one of its dependencies is not compatible with +the current Node version. This usually happens when you attempt to run ES2015 +code on node 0.10. Upgrade the affected dependency, modify the code to support +the desired node version, or change the required Node version to support the +syntax. + +--- + +### Next steps + +- [Create a project](/docs/getting-started/create-a-project) diff --git a/documentation/docs/getting-started/index.md b/documentation/docs/getting-started/index.md new file mode 100644 index 0000000..a0fdc7a --- /dev/null +++ b/documentation/docs/getting-started/index.md @@ -0,0 +1,27 @@ +--- +title: Getting started +path: /getting-started/ +--- + +# Getting started + +At the core of modern web applications at Uber is Fusion.js, a web framework for building high quality universal React/Redux/Node applications. If you've never used React/Redux before, [start here](/docs/getting-started/required-knowledge). + +Fusion.js is a collection of packages which form the core runtime, CLI, and build system. In addition, there are a set of [plugins](/api/plugins) that integrate with Fusion.js's Plugin API. Most of Fusion.js's functionality is added through these plugins. + +Check out the [Fusion API documentation](/api/fusion-docs) for a deeper look into Fusion's functionality. + +### Example app + +We have a list of Fusion.js boilerplate applications at: https://github.com/kevingrandon/fusion-boilerplate. We're looking to expand upon these boilerplate applications. + +### Create a new web application + +When you're ready to start on a new web project, run through these steps: + +* [Why Fusion.js](/docs/getting-started/why-fusion) +* [Required knowledge](/docs/getting-started/required-knowledge) +* [Environment setup](/docs/getting-started/environment-setup) +* [Create a project](/docs/getting-started/create-a-project) +* [Project structure](/docs/getting-started/project-structure) +* [Run your project](/docs/getting-started/run-your-project) diff --git a/documentation/docs/getting-started/project-structure.md b/documentation/docs/getting-started/project-structure.md new file mode 100644 index 0000000..eaeb4cf --- /dev/null +++ b/documentation/docs/getting-started/project-structure.md @@ -0,0 +1,69 @@ +--- +title: Project structure +path: /project-structure/ +--- + +# Project structure + +Here is an example of the file structure for a Fusion.js application. + +```yaml +- src + - components + - root.js + - static + - __tests__ + - plugins + - redux.js + - main.js +- translations +``` + +#### src/main.js + +The `src/main.js` is the entry point of a Fusion.js application. This file creates an `app` instance and registers plugins for various features such as css-in-js, data fetching, translations, logging and monitoring, and more. + +#### src/components + +The root component of your React application lives in `src/components/root.js`. + +#### src/redux.js + +We recommend using Redux and provide a `src/redux.js` file where you can define a root reducer and import other reducers as you write them. + +#### src/static + +We recommend placing all of your static assets in the `src/static` folder. They can be referenced in code via the `assetUrl` virtual module: + +```js +// src/components/example.js +import {assetUrl} from 'fusion-core'; + +const image = ; +``` + +#### src/**tests** + +Tests can be located anywhere in a project directory tree inside **tests** directories. Test files that end in .node.js will be run in Node.js and files ending in .browser.js will be run in a headless browser environment (via Jsdom), and universal tests will be run in both. + +#### src/plugins + +Custom plugins go in the `src/plugins` folder. + +#### translations + +Put translations in the `translations` folder as JSON files whose names are their respective locales (e.g. `en-US.json`). + +Here's an example translation file: + +```json +{ + "greeting": "Hello ${name}" +} +``` + +--- + +### Next steps + +* [Run your project](/docs/getting-started/run-your-project) diff --git a/documentation/docs/getting-started/required-knowledge.md b/documentation/docs/getting-started/required-knowledge.md new file mode 100644 index 0000000..8db5c59 --- /dev/null +++ b/documentation/docs/getting-started/required-knowledge.md @@ -0,0 +1,28 @@ +--- +title: Required knowledge +path: /required-knowledge/ +--- + +# Required knowledge + +Many Fusion.js applications will use React and Redux as well as abstractions built on top of these technologies. If you are not well-versed in React and/or Redux, below we have compiled a few tutorials and documentation pages that will help give you a good base for building a web application at Uber. + +### React + +Read Facebook's [Thinking In React](https://facebook.github.io/react/docs/thinking-in-react.html) page and their [React Tutorial](https://facebook.github.io/react/docs/tutorial.html). + +### Redux + +Watch [Getting Started with Redux](https://egghead.io/courses/getting-started-with-redux) by Dan Abramov (creator of Redux). + +If you prefer reading, the [Redux documentation](http://redux.js.org/docs/introduction/) is very thorough and covers similar content. + +### React + Redux + +Watch [Building React Applications with Idiomatic Redux](https://egghead.io/courses/building-react-applications-with-idiomatic-redux). This is the follow-up course to Dan Abramov's Redux course. + +--- + +### Next steps + +* [Environment setup](/docs/getting-started/environment-setup) diff --git a/documentation/docs/getting-started/run-your-project.md b/documentation/docs/getting-started/run-your-project.md new file mode 100644 index 0000000..63fc1f1 --- /dev/null +++ b/documentation/docs/getting-started/run-your-project.md @@ -0,0 +1,130 @@ +--- +title: Run your project +path: /run-project/ +--- + +# Run your project + +### Development mode + +To run your project in development mode, run: + +```sh +yarn dev +``` + +This enables hot module reloading, integration with various dev tools, etc. + +The command above also starts the Fusion.js HTTP server. The development site +will be available at `http://localhost:3000`. + +### Production mode + +It's sometimes useful to run a project in production mode, for example, to check +bundle size or to debug a production-only issue. To run your project in +production mode locally, run: + +```sh +yarn build-production && NODE_ENV=production yarn start +``` +Refer to the [debugging](/docs/guides/debugging#debugging-your-production-build-locally) section if you get any error while running a production build locally. + +**Note**: Development tooling such as hot reloading is not available in +production builds. + +### NPM scripts + +Fusion.js provides various CLI commands that you can run via `yarn run`. For +example, to run the application in development mode, run `yarn run fusion dev`. + +```json +{ + "scripts": { + "dev": "fusion dev", + "test": "fusion test", + "cover": "fusion test --cover", + "build": "fusion build", + "build-production": "npm run build -- --production && upload-assets-to-s3", + "start": "PORT_HTTP=\"$UBER_PORT_HTTP\" fusion start", + "lint": "eslint ." + } +} +``` + +#### `yarn run fusion --help` + +Shows Fusion.js CLI help. Get contextual help for each individual +command by passing `--help` as an argument to a command. For example, to see +what options are available to the `dev` command, run `yarn run fusion dev --help` + +#### `yarn dev` + +Starts the development mode server (with hot module reloading, etc.) + +* `yarn dev --debug` - Enables debugger socket. See + [Node.js inspector docs for more information](https://nodejs.org/en/docs/inspector). +* `yarn dev --port=1234` - Specifies what port the app should run on +* `yarn dev --no-open` - Don't open browser window automatically +* `yarn dev --no-hmr` - Disables hot module reloading +* `yarn dev --log-level=level` - Filters logs by level. Valid levels are: + `error`, `warn`, `info`, `verbose`, `debug`, `silly` +* `yarn dev --dir=dir` - Specify the root directory for the application, + relative to cwd. Defaults to `.` + +#### `yarn test` + +Runs tests + +* `yarn test --cover` - Also runs test coverage +* `yarn test --skip-build` - Tests existing assets without recompiling +* `yarn test --watch` - Re-runs tests when files change +* `yarn test --dir=dir` - Specify the root directory for the application, + relative to cwd. Defaults to `.` + +#### `yarn cover` + +Runs test coverage + +#### `yarn build` + +Builds a development bundle without starting the server + +* `yarn build --test` - Builds test assets in addition to development assets +* `yarn build --cover` - Builds test assets with coverage instrumentation in + addition to development assets +* `yarn build --production` - Builds production assets +* `yarn build --dir=dir` - Specify the root directory for the application, + relative to cwd. Defaults to `.` + +#### `yarn build-production` + +Builds a production bundle + +#### `yarn start` + +Runs a bundle that was built via `yarn build` + +* `yarn start --environment=env` - Runs the app as if running in a specific + environment. Valid environments are: `development`, `production` +* `yarn start --dir=dir` - Specify the root directory for the application, + relative to cwd. Defaults to `.` + +#### `yarn lint` + +Runs eslint + + + +--- diff --git a/documentation/docs/getting-started/why-fusion.md b/documentation/docs/getting-started/why-fusion.md new file mode 100644 index 0000000..e57c9dd --- /dev/null +++ b/documentation/docs/getting-started/why-fusion.md @@ -0,0 +1,66 @@ +--- +title: Why Fusion.js +path: /why-fusion/ +--- + +# Why Fusion.js + +Fusion.js is a collection of modern abstractions built from the ground up. It reflects today's best practices in web development and improves in many of the areas where other frameworks come up short. + +### Benefits of Fusion.js + +#### Developer productivity + +Fusion.js was built from the ground up for modern, React and React-like apps; it includes a lot of core developer experience benefits: + +* Features [React Router 4 style routing](/docs/guides/routing#component-based-routing), [composable data fetching](/docs/guides/fetching-data#use-rpc-method-in-a-component), and [internationalization](/docs/guides/internationalization#translations) using higher order components +* Unlocks the ability to write [universal code](/docs/guides/universal-rendering) that can be shared across browser and server render via a [lifecycle abstraction](/api/fusion-docs/creating-a-plugin) inspired by Koa middlewares and build-time environment fencing +* Out-of-the-box [build tooling](/api/fusion-cli) that supports modern stage 3+ EcmaScript features (like async/await), hot module reloading, and bundle splitting, with no configuration +* Faster dependency installer, build system, and test runner + +#### Maintainability + +One of Fusion's goals was to have a more maintainable and easily-upgradeable architecture that could last for years to come: + +* Fusion's [universal plugin architecture](/api/fusion-docs/creating-a-plugin) is its core feature; every single feature is added through a set of [open source](/api/plugins) plugins and your own plugins as well. +* Plugins are self-contained entities (though some may have dependencies); this means that we can easily upgrade plugins as-needed and we don't need to upgrade the entire system. +* For each breaking change release, we intend to introduce codemods for even simpler migrations. + +#### Performance + +Fusion was created with development and performance in mind, and was built with a focus on a lightweight, flexible system: + +* Initial DOMContentLoaded is fast. We've removed asset URLs, translations, and experiments from the application state sent down in the page body; now they are split per page-bundle, resulting in minimal HTML page size +* Our vendor JavaScript bundle small through usage of our modern build system, and we still have more improvements to come +* Brotli compression is turned on by default, resulting in another 15-20% improvement on modern browsers (Edge, Chrome, Safari, Firefox, etc.) +* [Bundle splitting](/docs/guides/routing#async-loading-routes) is extremely easy, allowing you to break down large applications into smaller parts and loading as necessary; in addition, we split translations, asset URLs and experiments along with each new bundle +* Performance gains can easily be added into the system over time because of the way the core libraries and build tooling work; we already plan to improve our vendor bundle size even more, create ES2015-specific code bundles (no polyfill or babel-transpiled code) and Preact-fusion plugins (Preact is 87% smaller than React). + +#### Quality + +We've improved a lot of tooling and APIs to increase the overall quality of our web applications, including: + +* Fusion's core plugin system was built to be easily testable through what we call "[simulation testing](/docs/guides/testing/simulation)"; this allows you to test your application easily and in isolation +* Fusion also ships with improved test APIs for doing both [snapshot](/docs/guides/testing/snapshot) and [integration](/docs/guides/testing/integration) tests, allowing you to increase the quality of your app through various different test types +* [Flow typing](/docs/guides/typing) is included out-of-the-box; we still have a bit more work left in order to fully type the Fusion architecture, but even without that adding Flow types to applications will lessen errors +* We've added [Yarn](https://yarnpkg.com/en/) as our dependency installer, which means we will now get consistent builds across local, CI and prod using [lockfiles](https://yarnpkg.com/lang/en/docs/yarn-lock/); it's also faster than npm! + +#### Same core technologies + +All of these changes can be daunting, but much of them are in the core architecture and build system, meaning: + +* The core technologies of all of Uber's web applications — React and Redux — remain the same in Fusion; this means the vast majority of application code: components, state management, data fetching will not need to change much in order to get many of Fusion's benefits +* For major performance improvements, though, there may need to be changes in areas around [font loading](/docs/guides/performance#font-preloading), using heavy 3rd-party dependencies and using the old Superfine CSS file (vs [Styletron](https://github.com/rtsao/styletron)) + +#### Unified documentation & open source development + +Documentation and open-source-first development has been a core tenet of Fusion’s development. + +* We’ve created a large set of guides, tutorials and Fusion core/plugin API docs that all sit on a [website](/docs/getting-started); you can make pull requests for documentation improvements as well as create tasks/issues directly from the documentation site +* Fusion.js is open source, so major changes will be proposed and discussed through the [transparent RFC process](https://github.com/fusionjs/rfcs) + +--- + +### Next steps + +* [Required knowledge](/docs/getting-started/required-knowledge) diff --git a/documentation/docs/guides/configuration.md b/documentation/docs/guides/configuration.md new file mode 100644 index 0000000..8a9edc7 --- /dev/null +++ b/documentation/docs/guides/configuration.md @@ -0,0 +1,24 @@ +--- +title: Configuration +path: /configuration/ +--- + +# Configuration + +### Environment variables + +The `NODE_ENV` environment variable is inferred by the compiler from the initiating Fusion CLI command and flags. It cannot be manually configured. + +Fusion.js can also receive some start-up configuration through a few environment variables: + +* `ROUTE_PREFIX` - A path under which the application responds. For example, if `ROUTE_PREFIX=/foo`, the app will live in `http://the-site.com/foo`. Defaults to empty string. +* `FRAMEWORK_STATIC_ASSET_PATH` - A path under which requests are treated as static asset requests. Defaults to `_static`. +* `ROOT_DIR` - The root directory of the app, relative to CWD. Can be configured via the `--dir` flag in Fusion CLI. Defaults to `.` + +--- + +### Static configuration + +The standard way to configure plugins in Fusion.js is to register the configuration values into the DI system via `app.register`. To support configuration tree shaking, we recommended keeping configuration in .js files rather than .json files. + +If you have to add a new plugin to `app.js`, you should still separate configuration into an appropriate file in `src/config` in order to keep configuration code discoverable for future project maintainers. diff --git a/documentation/docs/guides/debugging.md b/documentation/docs/guides/debugging.md new file mode 100644 index 0000000..43fa3ee --- /dev/null +++ b/documentation/docs/guides/debugging.md @@ -0,0 +1,32 @@ +--- +title: Debugging +path: /debugging/ +--- + +# Debugging + +### Debugging in development + +When running the application in dev mode, connect a debugger to the +Node.js server via the `yarn dev --debug` command. + +Running that command wil provide you with a debugger socket URL that can be +consumed by tools like the Chrome debugger, which then allows you to step +through server code. See +[Node.js inspector docs for more information](https://nodejs.org/en/docs/inspector). + +You can debug tests in a similar way by running `yarn test --debug`. + +#### Devtools + +##### React + +React provides [dev tools](https://github.com/facebook/react-devtools) that make +it easy to inspect the React component tree directly from the browser dev tools +panel. This tool works in Chrome and Firefox. + +##### Redux + +We recommend using +[the Redux dev tool extension](https://github.com/zalmoxisus/redux-devtools-extension) +for inspecting Redux actions and state tree. diff --git a/documentation/docs/guides/fetching-data.md b/documentation/docs/guides/fetching-data.md new file mode 100644 index 0000000..d2b0c68 --- /dev/null +++ b/documentation/docs/guides/fetching-data.md @@ -0,0 +1,12 @@ +--- +title: Fetching data +path: /fetching-data/ +--- + +# Fetching data + +Out of the box, we support data fetching in web applications with following components: + +1. [fusion-plugin-rpc-redux-react](/api/fusion-plugin-rpc-redux-react) is used to abstract away universal communication between components, redux store, and the server. +2. [fusion-react-async](/api/fusion-react-async) is used to trigger RPC calls during the render lifecycle. +3. [fusion-apollo](/api/fusion-apollo) can be used for leveraging GraphQL and Fusion.js. diff --git a/documentation/docs/guides/forms.md b/documentation/docs/guides/forms.md new file mode 100644 index 0000000..1deaf90 --- /dev/null +++ b/documentation/docs/guides/forms.md @@ -0,0 +1,226 @@ +--- +title: Forms +path: /forms/ +--- + +# Adding and submitting a form + +In this tutorial we look at how to create a simple form that prints out the value from an input when a button is pressed. For a bit more realism, we'll make the output value be set via an RPC call. + +Before we start, let's sketch out what we want to display to the user: + +```html +
+ + +

You wrote: ...

+
+``` + +Let's create a React component that looks like that: + +```js +// src/components/form.js +import React from 'react'; + +const Form = () => ( +
+ + +

You wrote: ...

+
+); + +export default Form; +``` + +In order to see this component, reference it in `src/components/root.js`. + +```js +// src/components/root.js +import Form from './form'; + +export default
; +``` + +You can run the app via `yarn dev`, which should open a browser window pointing at `http://localhost:3000`. + +Next, let's model our Redux store. We need a property to store the value of the input and a value to store the value that comes back from the server after an RPC call. + +```js +{ + input: '', + output: '', +} +``` + +We also need to dispatch an action when the input value changes, and another when the button is pressed. + +Here's the reducer for setting the form input value: + +```js +// src/reducers/form.js +export const input = (state, {type, value}) => { + return type === 'SET_INPUT' ? value : state; +}; +``` + +For submitting, we'll create an RPC reducer: + +```js +// src/reducers/form.js (continued) +import {withRPCRedux} from 'fusion-plugin-rpc-redux-react'; + +const reducers = { + start: state => state, + success: (state, {value}) => value, + failure: state => state, +}; +export const output = withRPCRedux('submit', reducers, ''); +``` + +Finally, let's compose the reducers into a root reducer: + +```js +// src/redux.js +import {input, output} from './reducers/form.js'; + +export default { + reducer: (state = {}, action) => ({ + input: input(state.input, action), + output: output(state.output, action), + }), +}; +``` + +Next, let's expose the state and actions to the React component. To do that, we need to compose an HOC: + +```js +import {compose} from 'redux'; +import {connect} from 'react-redux'; +import {withRPCRedux} from 'fusion-plugin-rpc-redux-react'; + +const hoc = compose( + withRPCRedux('submit'), + connect( + ({input, output}) => ({input, output}), + dispatch => ({ + setInput({value}) { + dispatch({type: 'SET_INPUT', value}); + }, + }) + ) +); +``` + +Now we can decorate the `Form` component: + +```js +export default hoc(Form); +``` + +`Form` now receives the following props: `input`, `output` (via the mapStateToProps argument in `connect`), `setInput` (via `mapDispatchToProps`), and `submit` (via `withRPCRedux('submit', ...)`. + +We can then wire up the React elements: + +```js +const Form = ({input, output, setInput, submit}) => ( + + setInput({value: e.target.value})} value={input} /> + + {output &&

You wrote: {output}

} +
+); +``` + +As a final step, let's implement the server handler for the `submit` RPC call: + +```js +// src/rpc/handlers.js +import {createPlugin} from 'fusion-core'; + +export default createPlugin({ + provides() { + return {submit: value => value}; + }, +}); +``` + +Here's how the code looks when everything is put together: + +```js +// src/redux.js +import {input, output} from './reducers/form.js'; + +export default { + reducer: (state = {}, action) => ({ + input: input(state.input, action), + output: output(state.output, action), + }), +}; + +// src/reducers/form.js +import {createRPCReducer} from 'fusion-rpc-redux'; + +export const input = (state = '', {type, value}) => { + return type === 'SET_INPUT' ? value : state; +}; + +const reducers = { + start: state => state, + success: (state, {payload}) => payload.value, + failure: state => state, +}; +export const output = createRPCReducer('submit', reducers, ''); + +// src/rpc/handlers.js +import {createPlugin} from 'fusion-core'; + +export default createPlugin({ + provides() { + return {submit: value => value}; + }, +}); + +// src/components/form.js +import React from 'react'; + +import {compose} from 'redux'; +import {connect} from 'react-redux'; +import {withRPCRedux} from 'fusion-plugin-rpc-redux-react'; + +const hoc = compose( + withRPCRedux('submit'), + connect( + ({input, output}) => ({input, output}), + dispatch => ({ + setInput({value}) { + dispatch({type: 'SET_INPUT', value}); + }, + }) + ) +); + +const Form = ({input, output, setInput, submit}) => ( +
+ setInput({value: e.target.value})} value={input} /> + + {output &&

You wrote: {output}

} +
+); + +export default hoc(Form); + +// src/components/root.js +import Form from './form'; + +export default
; +``` + +### Running the app + +Don't forget to run `cerberus` from the command line. Then start the app via `yarn dev`. diff --git a/documentation/docs/guides/index.md b/documentation/docs/guides/index.md new file mode 100644 index 0000000..b0a5a7a --- /dev/null +++ b/documentation/docs/guides/index.md @@ -0,0 +1,24 @@ +--- +title: Guides +path: /guides/ +--- + +# Guides + +* [State management](/docs/guides/state-management) +* [Styling components](/docs/guides/styling-components) +* [Routing](/docs/guides/routing) +* [Fetching data](/docs/guides/fetching-data) +* [Forms](/docs/guides/forms) +* [Working with URL parameters](/docs/guides/working-with-url-parameters) +* [Internationalization](/docs/guides/internationalization) +* [Testing](/docs/guides/testing) +* [Typing](docs/guides/typing) +* [Security](/docs/guides/security) +* [Universal rendering](/docs/guides/universal-rendering) +* [Server code](/docs/guides/server-code) +* [Performance](/docs/guides/performance) +* [Debugging](/docs/guides/debugging) +* [Static assets](/docs/guides/static-assets) +* [Configuration](/docs/guides/configuration) +* [Authentication and authorization](/docs/guides/authentication-and-authorization) diff --git a/documentation/docs/guides/internationalization.md b/documentation/docs/guides/internationalization.md new file mode 100644 index 0000000..af598e6 --- /dev/null +++ b/documentation/docs/guides/internationalization.md @@ -0,0 +1,54 @@ +--- +title: Internationalization +path: /Internationalization/ +--- + +# Internationalization + +### Translations + +The Fusion.js applications can use the [fusion-plugin-i18n-react](https://github.com/fusionjs/fusion-plugin-i18n-react) plugin for translations. + +The easiest way to add translations to an app is to use the `Translate` React component: + +```js +import React from 'react'; +import {Translate} from 'fusion-plugin-i18n-react'; + +export default () => { + // translates the key "test" + return ; +}); +``` + +If a translation needs to be parameterized, pass an object to the `data` prop: + +```js +import React from 'react'; +import {Translate} from 'fusion-plugin-i18n-react'; + +export default () => { + // translates a key "test" with a value like "hello ${name}" + return ; +}); +``` + +**Note**: If translations for a key are not available, the key will be displayed. + +--- + +### Dates and time + +There are many libraries that offer i18n support for dates/time: [moment.js](https://momentjs.com/), [date-fns](https://date-fns.org/) and [globalize.js](https://github.com/globalizejs/globalize) are popular ones. + +Supporting a large number of locales can have a significant impact on bundle size if formatting for a large number of locales happens client-side. We recommend formatting dates server-side when fetching data and avoid formatting data when rendering date/times in the React layer. + +Avoid parsing formatted dates. Date parsing is extremely bug-prone in native Javascript and libraries alike because they all fall back to undocumented browser-specific semantics when strings don't conform to the [ISO 8601 standard](https://en.wikipedia.org/wiki/ISO_8601). It's also often unreliable due to lack of timezone information. Instead of parsing dates that have been formatted, use timestamps to get fast and reliable serialization/deserialization and robust `Date` object reconstruction. + +--- + +### Numbers and currency + +The [globalize.js](https://github.com/globalizejs/globalize) library is commonly used to format numbers and currency. + +Supporting a large number of locales can have a significant impact on bundle size if formatting for a large number of locales happens client-side. We recommend formatting numbers and currency when fetching data and avoid formatting data when rendering date/times in the React layer. diff --git a/documentation/docs/guides/performance.md b/documentation/docs/guides/performance.md new file mode 100644 index 0000000..7dbecd0 --- /dev/null +++ b/documentation/docs/guides/performance.md @@ -0,0 +1,156 @@ +--- +title: Performance +date: 2017-10-31 +path: /performance/ +category: Documentation +--- + +# Performance + +This section will describe various low-effort techniques you can use use to make your web app faster: + +* [bundle splitting](#bundle-splitting) +* [font preloading](#font-preloading) +* [image compression](#image-compression) + +### Bundle splitting + +Bundle splitting means separating JavaScript code into multiple files such that a smaller amount of code is downloaded on page load, and other sections of the web app are loaded later on demand. + +While Fusion.js allows splitting bundles to render shell and fill in the spaces separately, the easiest strategy to reduce download times is to make the top-level component of routes split. + +Below is an example showing what an app would look like before applying bundle splitting based on routes: + +```js +// src/main.js +import App from 'fusion-react'; +import root from './components/root'; + +export default () => { + return new App(root); +} + +// src/components/root.js +import React from 'react'; +import Hello from './components/hello'; + +const root = ( +
+
    +
  • Home
  • +
  • Hello
  • +
+
+ + + + +) + +export default root; + +// src/components/hello.js +export default () => ( +
Hello
+) +``` + +And here's what it would look like if we added bundle splitting to the component for the `/hello` route: + +```js +// src/main.js +import App from 'fusion-react'; +import root from './components/root'; + +export default () => { + return new App(root); +} + +// src/components/root.js +import React from 'react'; +import {split} from 'fusion-react-async'; + +const LoadingComponent = () =>
Loading...
; +const ErrorComponent = () =>
Error loading component
; +const Hello = split({ + load: () => import('./components/hello'); + LoadingComponent, + ErrorComponent +}); + +const root = ( +
+
    +
  • Home
  • +
  • Hello
  • +
+
+ + + + +) + +export default root; + +// src/components/hello.js +export default () => ( +
Hello
+) +``` + +--- + +### Font preloading + +Font downloads can be a significant source of latency. Configuring how fonts are loaded can improve download times. Here's an example configuration: + +```js +export default { + preloadDepth: 1, + fonts: { + MyFont: { + urls: { + woff2: assetUrl('../static/fonts/MyFont-Book.woff2'), + woff: assetUrl('../static/fonts/MyFont-Book.woff'), + }, + fallback: { + name: 'Helvetica', + }, + }, + Medium: { + urls: { + woff2: assetUrl('../static/fonts/MyFont-Medium.woff2'), + woff: assetUrl('../static/fonts/MyFont-Medium.woff'), + }, + fallback: { + name: 'MyFont', + }, + styles: { + fontWeight: 'bold', + }, + }, + }, + // ... +}; +``` + +The `fonts` property defines a graph of dependencies: `MyFont` falls back to `Helvetica` and `Medium` falls back to `MyFont`. Additionally, `Medium` is defined as a `bold` font weight. + +The `preloadDepth` property indicates how many levels of the dependency graph are preloaded via inline CSS and by extension how many are loaded asynchronously via JavaScript. + +Preloading a font makes text using that font render with the correct font faster, because the browser can efficiently prioritize and parallelize font and CSS downloads if fonts are declared with inline CSS. By contrast, downloading a font asynchronously requires running JavaScript, which blocks the browser's parsing pipeline and can take an arbitrarily much longer time to fire a font request than an inline CSS declaration. With that being said, while preloading one or two fonts typically has little or no impact on DOMContentLoaded timing, the trade-off of preloading several fonts - even if they aren't that noticeably used - is that you would have to wait a long time for anything to render at all because you've saturated your download pipeline with font-related requests, which has a significant negative impact on DOMContentLoaded timing. + +Instead of doing that, the configuration above only preloads one level of the dependency graph, i.e. it only preloads `MyFont`. It can be acceptable to defer loading of `Medium` because the browser can synthesize faux styles for bold and italics for any font by using a generic algorithm. However, the faux font synthesis algorithm isn't perfect. Font design is an art, which means that there are artistic and ergonomic differences between a synthesized bold style and a hand-crafted font such as `MyFont-Medium`. Ideally we want to use `Medium` instead of a synthesized bold, but it's acceptable to temporarily show a synthesized bold while waiting for the true `Medium` font to be downloaded and switching over when the download is done. This technique is known as FOFT (flash of faux text), and is far less jarring than FOIT (flash of invisible text) and FOUT (flash of unstyled text). + +The configuration above is usually good for most applications, but there may be reasons to tune it differently. If, for example, we were working on an landing page that heavily used MyFont-Medium above the fold, the FOFT might be more jarring than preloading `Medium`. In that case, we could change `preloadDepth` to `0` to force `Medium` to be preloaded via inline CSS. + +On the other hand, if there's a major concern about overall performance of a web application, it might be an acceptable trade-off to preload no fonts. In that case, changing `preloadDepth` to `2` would force both `MyFont` and `Medium` to be loaded asynchronously. This would mean that the page would render immediately using `Helvetica` and later would switch to `MyFont` and `Medium` as these fonts finished downloading (causing a FOUT). + +--- + +### Image compression + +Image compression is perhaps the most cost-effective way to improve performance of public facing web apps. + +There are many tools that can automatically compress images, such as [`imagemin-cli`](https://www.npmjs.com/package/imagemin-cli) diff --git a/documentation/docs/guides/routing.md b/documentation/docs/guides/routing.md new file mode 100644 index 0000000..9909c91 --- /dev/null +++ b/documentation/docs/guides/routing.md @@ -0,0 +1,335 @@ +--- +title: Routing +path: /routing/ +--- + +# Component-based routing + +Fusion apps can use the [fusion-plugin-react-router](/api/fusion-plugin-react-router) to integrate routing features into the component tree. The plugin uses react-router under the hood, and exposes a similar API which allows you to add routing behavior anywhere in your component tree. + +### Example + +```js +import React from 'react'; +import { + Router, + Route, + Link, + Switch, + NotFound, +} from 'fusion-plugin-react-router'; + +const Home = () =>
Hello
; +const Test = () =>
Test
; +const PageNotFound = () => ( + +
404
+
+); + +const root = ( +
+
    +
  • + Home +
  • +
  • + Test +
  • +
  • + 404 +
  • +
+ + + + + +
+); +``` + +## Async loading routes + +The [fusion-plugin-react-router](/api/fusion-plugin-react-router) integrates nicely with the [fusion-react-async](/api/fusion-react-async) library to support async loading routes. This means that you will only load the code for the route that the user is currently visiting, and can make significant performance improvements to your application. + +### Example + +```js +// src/components/root.js +import React from 'react'; +import {split} from 'fusion-react-async'; +import {Route} from 'fusion-plugin-react-router'; + +const LoadingComponent = () =>
Loading...
; +const ErrorComponent = () =>
Error loading component
; +const BundleSplitHello = split({ + load: () => import('./components/hello'), + LoadingComponent, + ErrorComponent +}); + +const root = ( +
+
This is part of the initial bundle
+ +
+) +export default root; + +// ... +// src/components/hello.js +export default () => ( +
+ This is part of a separate bundle that gets loaded asynchronously + when the BundleSplit component gets mounted +
+) +``` + +## Working with URL parameters + +Let's look at how to use URL parameters as arguments to data fetching. We'll build an app that displays links to vehicles and when a user clicks on one, it will display some information about that vehicle. + +Let's define the states of our application: + +* viewing the links to vehicles +* viewing a page for a valid vehicle +* viewing a page for a non-existent vehicle (i.e. a bogus vehicle id) + +For simplicity, let's put the links on the top of all pages: + +```js +// src/components/root.js +import React from 'react'; +import {Link} from 'fusion-plugin-react-router'; +import Vehicle from './vehicle'; + +export default ( +
+
    +
  • + Home +
  • +
  • + Valid vehicle +
  • +
  • + Invalid vehicle +
  • +
+
+); +``` + +Next, let's add a parameterized route below our list of links: + +```js +// src/components/root.js +import React from 'react'; +import {Link, Route} from 'fusion-plugin-react-router'; +import Vehicle from './vehicle'; + +export default ( +
+
    +
  • + Home +
  • +
  • + Valid vehicle +
  • +
  • + Invalid vehicle +
  • +
+ +
+); +``` + +Notice that `path="/:id"` matches if the URL is a `/vehicle/` followed by some text. We'll create the `Vehicle` component shortly. + +Now, let's create a fake RPC method that returns dummy vehicle data for `id=1234` and an error for `id=0`: + +```js +// src/rpc/handlers.js +export default { + getVehicle: async ({id}) => { + if (id === '1234') return {make: 'Ford', model: 'Focus'}; + else throw new Error('Invalid vehicle id'); + }, +}; +``` + +Note that the parameter is a string. + +Let's create a Redux reducer to handle RPC call actions: + +```js +// src/redux.js +import {createRPCReducer} from 'fusion-plugin-rpc-redux-react'; + +export default { + reducer: createRPCReducer('getVehicle', { + start: state => ({ + ...state, + make: '', + model: '', + error: '', + }), + success: (state, {payload}) => ({ + ...state, + make: payload.make, + model: payload.model, + error: '', + }), + failure: (state, {payload}) => ({ + ...state, + make: '', + model: '', + error: payload.message, + }), + }), +}; +``` + +Next, let's create a higher order React component that can consume our Redux state and our RPC method. First we compose the `reactor` and a `connect` HOCs + +```js +// src/components/vehicle.js +import {compose} from 'redux'; +import {connect} from 'react-redux'; +import {withRPCRedux} from 'fusion-plugin-rpc-redux-react'; + +const hoc = compose( + withRPCRedux('getVehicle'), + connect(({make, model, error}) => ({make, model, error})) +); +``` + +Now we can create the `Vehicle` component and decorate it with the HOC: + +```js +// src/components/vehicle.js +import React from 'react'; +import {compose} from 'redux'; +import {connect} from 'react-redux'; +import {withRPCRedux} from 'fusion-plugin-rpc-redux-react'; + +class Vehicle extends React.Component { + renderLoading() { + return
loading...
; + } + renderError() { + return
{this.props.error}
; + } + render() { + const {make, model, error} = this.props; + if (error) return this.renderError(); + else if (!make) return this.renderLoading(); + return ( + + + + + + + + + + + +
Make{make}
Model{model}
+ ); + } +} + +const hoc = compose( + withRPCRedux('getVehicle'), + connect(({make, model, loading, error}) => ({make, model, loading, error})) +); + +export default hoc(Vehicle); +``` + +You may have noticed that we're only reading from the Redux state at this point, but we never called `getVehicle` anywhere. + +In order to call that method, we first need to access the `:id` parameter that is provided by the URL. This can be found in `props.match.params.id`. + +In addition, there are actually two separate scenarios where we need to make a new `getVehicle` call: when a component is created and when it's updated with a different id. These are handled by the `componentDidMount` and `componentWillReceiveProps` lifecycle methods, respectively. Here's what those two methods end up looking like: + +```js +class Vehicle extends React.Component { + componentDidMount() { + this.props.getVehicle({id: this.props.match.params.id}); + } + componentWillReceiveProps(newProps) { + const oldProps = this.props; + const oldId = oldProps.match && oldProps.match.params.id; + const newId = newProps.match && newProps.match.params.id; + if (oldId !== newId) { + this.props.getVehicle({id: newId}); + } + } + // ... rest of the render methods +} +``` + +And here's the complete component file: + +```js +// src/components/vehicle.js +import React from 'react'; +import {compose} from 'redux'; +import {connect} from 'react-redux'; +import {withRPCRedux} from 'fusion-plugin-rpc-redux-react'; + +class Vehicle extends React.Component { + componentWillReceiveProps(newProps) { + const oldProps = this.props; + const oldId = oldProps.match && oldProps.match.params.id; + const newId = newProps.match && newProps.match.params.id; + if (oldId !== newId) { + this.props.getVehicle({id: newId}); + } + } + componentDidMount() { + this.props.getVehicle({id: this.props.match.params.id}); + } + renderLoading() { + return
loading...
; + } + renderError() { + return
{this.props.error}
; + } + render() { + const {make, model, error} = this.props; + if (error) return this.renderError(); + else if (!make) return this.renderLoading(); + return ( + + + + + + + + + + + +
Make{make}
Model{model}
+ ); + } +} + +const hoc = compose( + withRPCRedux('getVehicle'), + connect(({make, model, loading, error}) => ({make, model, loading, error})) +); + +export default hoc(Vehicle); +``` + +Run `yarn dev` to see the application running at `http://localhost:3000` diff --git a/documentation/docs/guides/security.md b/documentation/docs/guides/security.md new file mode 100644 index 0000000..c69aa17 --- /dev/null +++ b/documentation/docs/guides/security.md @@ -0,0 +1,66 @@ +--- +title: Security +path: /security/ +--- + +# Security + +Fusion.js plugins can allow for secure application development, plugins are provided to: + +* CSRF protection +* Frameguard +* Content security policy + +Most of these require no configuration from you. + +--- + +### Configuring CSRF protection rules + +By default, any [non-idepotent HTTP method](http://restcookbook.com/HTTP%20Methods/idempotency/) is protected by this plugin. + +A CSRF (cross-site request forgery) attack happens if a victim visits a malicious website, and that website triggers a spoofed request via Javascript to execute an unwanted actions on a web application in which the victim is currently authenticated. CSRF protection ensures that a malicious site cannot trigger such requests by requiring a token to be associated with state-changing requests. This token cannot be spoofed thanks to security restrictions built into how browsers deal with cross-site javascript-based requests. + +Some examples of CSRF attacks include a maliciously crafted facebook link that triggers expensive operations on a user's behalf, or one that forges a request to your backend. You should never disable CSRF protection. + +It's possible to create a whitelist of URLs where CSRF protection is disabled if rare exceptions need to be made. For example, it is critical for error logging requests to be accepted by the server, rather than being blocked if they're missing a CSRF token, and error logging requests from Fusion.js are done with POST requests, which are subject to CSRF protection. + +To add this exception to the whitelist, use the `CsrfIgnoreRoutesToken` configuration value: + +```js +// src/app.js +import {CsrfIgnoreRoutesToken} from 'fusion-plugin-csrf-protection-react'; + +app.register(CsrfIgnoreRoutesToken, ['/_errors']); +``` + +--- + +### dangerouslySetHTML + +When authoring plugins that affect the server-rendered template, you will often need to add values to the `ctx.template.head` and `ctx.template.body` arrays. + +These values must be sanitized HTML values, which are produced by the `html` template tag. Sanitized HTML values are actually not strings at all. This restriction exists to prevent potential XSS attacks. You should always use `html` when hard-coding HTML into the template: + +```js +import {html} from 'fusion-core'; + +export default __NODE__ && + createPlugin({ + middleware() { + return (ctx, next) => { + ctx.template.head.push( + html`` + ); + }; + }, + }); +``` + +The `html` template tag automatically escapes interpolated values via the `escape` utility function. + +```js +import {escape} from 'fusion-core'; // note: this is not the same as the global.escape function! +``` + +There's another function called `dangerouslySetHTML` which disables protection against XSS attack. Needless to say, you should never use this function, unless you have taken care to manually call `escape` on ALL user data, _and_ added tests to ensure your custom sanitization code works correctly. diff --git a/documentation/docs/guides/server-code.md b/documentation/docs/guides/server-code.md new file mode 100644 index 0000000..7f93173 --- /dev/null +++ b/documentation/docs/guides/server-code.md @@ -0,0 +1,14 @@ +--- +title: Server code +path: /server-code/ +--- + +# Server code + +If you need to implement server-side code to fetch data from a service, [consider using RPC, as described in the fetching data section](/docs/guides/fetching-data) + +### Advanced usage + +If you need to modify the HTML template (e.g. the `` or `` tags), see [this section](/api/fusion-docs/modifying-html-template) + +If you need to augment a Fusion.js application via a custom plugin, see [this section](/api/fusion-docs/creating-a-plugin) diff --git a/documentation/docs/guides/state-management.md b/documentation/docs/guides/state-management.md new file mode 100644 index 0000000..9828d54 --- /dev/null +++ b/documentation/docs/guides/state-management.md @@ -0,0 +1,78 @@ +--- +title: State management +date: 2017-10-31 +path: /state-management/ +category: Guides +--- + +# State management + +### Redux + +Redux is the recommended state management library for web applications at Uber. Fusion.js provides Redux integrations for its RPC plugins, and there are also [open source developer tools available](https://github.com/zalmoxisus/redux-devtools-extension). + +If you've never used Redux, refer to the [required knowledge](/docs/getting-started/required-knowledge#redux) section. + +--- + +### Where to put reducers + +We recommend exporting a root reducer from `src/redux.js`: + +```js +export default { + reducer: myReducer, +}; +``` + +For example, given a state tree `{count: 0}` that responds to an action `{type: 'INCREMENT'}`, you would export this: + +```js +export default { + reducer: (state, action) => ({ + count: state.count + (action.type === 'INCREMENT' ? 1 : 0), + }), +}; +``` + +**Note**: Unlike the example above, you should typically refactor a root reducer so that each key in the state object is handled by its own reducer. See [`combineReducers`](https://redux.js.org/docs/api/combineReducers.html). + +The Fusion.js [fusion-plugin-react-redux plugin](/api/fusion-plugin-react-redux) will setup a provider for you, so you don't need to manually wrap your React tree with a provider. + +Use `connect` from `react-redux` to expose redux state to React props: + +```js +// src/components/root.js +import {connect} from 'react-redux'; + +const hoc = connect( + ({count}) => ({count}), // copies state.count into props.count + dispatch => ({ + // defines that props.increment dispatches a `INCREMENT` action + increment() { + dispatch({type: 'INCREMENT'}); + }, + }) +); + +const Component = ({count, increment}) => ( + <button onClick={increment}>{count}</button> +); + +export default hoc(Component); +``` + +To see how to integrate Redux with RPC calls, refer to the [fetching data](/docs/guides/fetching-data) section. + +--- + +### Where to put enhancers + +If you need to add [enhancers](https://github.com/reactjs/redux/blob/master/docs/Glossary.md#store-enhancer) or [middlewares](https://github.com/reactjs/redux/blob/master/docs/Glossary.md#middleware), export them like this: + +```js +export default { + reducer, + enhancer: myEnhancer, +}; +``` diff --git a/documentation/docs/guides/static-assets.md b/documentation/docs/guides/static-assets.md new file mode 100644 index 0000000..8224e8c --- /dev/null +++ b/documentation/docs/guides/static-assets.md @@ -0,0 +1,85 @@ +--- +title: Static assets +path: /static-assets/ +--- + +# Static assets + +During the compilation process, Fusion.js places files in the `.fusion` directory. Any file there is a static asset. + +Fusion.js uses two environment variables to determine the URL prefix at which assets are served: + +* `ROUTE_PREFIX` (defaults to empty string) +* `FRAMEWORK_STATIC_ASSET_PATH` (defaults to `/_static`) + +If, for example, `ROUTE_PREFIX` is `/foo` and `FRAMEWORK_STATIC_ASSET_PATH` is `/_static`, then a request to `/foo/_static/bar.gif` in a production environment would attempt to respond with an asset at `.fusion/dist/production/client/bar.gif` + +### What is assetUrl? + +Fusion.js provides an `assetUrl` virtual function to both client and server side contexts. The helper simply converts asset relative paths (e.g. `./src/asset.js`) to the fully qualified URL (e.g. `/_static/asset.js`). + +To use it, first import it: + +```js +import {assetUrl} from 'fusion-core'; +``` + +Then call it with a string literal as an argument: + +```js +assetUrl('./src/static/foo.gif'); // becomes '/_static/foo.gif' +``` + +Note that `assetUrl` is resolved at compile time via a Babel plugin. Therefore, using a variable expression instead of a string literal will not work. + +```js +// works +assetUrl('./src/static/foo.gif'); + +// throws compilation error +const path = './src/static/foo.gif'; +assetUrl(path); +``` + +### Adding 3rd party assets + +> **NOTE:** We are working on a more elegant and streamlined solution for serving up third-party assets. In the mean time, the following is a work-around. + +Resolve assets stored in `node_modules` at compile time with `assetUrl` (e.g. `assetUrl(../../node_modules/some-module/example-asset.css')`). + +To include asset in the `<head>` container, create a plugin: + +```js +import {assetUrl, dangerouslySetHTML, createPlugin} from 'fusion-core'; + +export default createPlugin({ + middleware: () => { + const url = assetUrl('../../node_modules/some-path-to-asset'); + const escaped = dangerouslySetHTML(`<link rel="stylesheet" href="${url}">`); + return (ctx, next) => { + if(ctx.element) { + ctx.template.head.push(escaped); + } + return next(); + } + } +}); +``` + +And register the plugin: + +```js +// src/app.js +import MyStaticAssetPlugin from './plugins/my-static-asset-plugin'; +// ... +export default app => { + // ... + app.register(MyStaticAssetPlugin); + // ... +} +``` + +#### What about externally hosted assets? + +We do not recommend using externally hosted static assets in your application due to security and reliability concerns. If necessary, download and serve the asset as outlined above. + diff --git a/documentation/docs/guides/styling.md b/documentation/docs/guides/styling.md new file mode 100644 index 0000000..eaaa2f3 --- /dev/null +++ b/documentation/docs/guides/styling.md @@ -0,0 +1,228 @@ +--- +title: Styling +path: /styling/ +--- + +# Styling components + +For custom styling, we recommend using Styletron (via [`fusion-plugin-styletron-react`](https://github.com/fusionjs/fusion-plugin-styletron-react)). + +This plugin automatically sets up SSR, hydration, and context provider boilerplate and re-exports the styling functions from `styletron-react` so you can just focus on styling. + +```js +// Be sure to import HOCs from "fusion-plugin-styletron-react" +import { + styled, + withStyle, + withStyleDeep, + withTransform +} from "fusion-plugin-styletron-react"; +``` + +**See the [main README for a thorough guide on using Styletron](https://github.com/rtsao/styletron/tree/v4-beta/packages/styletron-react).** + +**Note**: Extra features such as LTR-to-RTL might be included in `fusion-plugin-styletron-react`, so it's best to use its exports instead of `styletron-react` directly. + +## `styletron-react` guide + +Here's a simple example that creates a styled component: + +```js +// src/components/root.js +import react from 'react'; +import {styled} from 'fusion-plugin-styletron-react'; + +const Panel = styled('div', { + backgroundColor: 'silver', +}); + +export default <Panel>Hello</Panel>; +``` + +### Parameterized styled components + +You can also have customizable styles by passing a function as the second argument: + +```js +// src/components/root.js +import React from 'react'; +import {styled} from 'fusion-plugin-styletron-react'; + +const Panel = styled('div', props => ({ + backgroundColor: props.$bg || 'silver', +})); + +export default <Panel $bg="gold">Hello</Panel>; +``` + +### Styled prop filtering + +Styled components automatically pass through all props to their underlying base except those prefixed by `$`, which will be filtered out. Use this namespace for props only used for styling. React will [no longer automatically filter out non-HTML props](https://reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html) so this convention avoids the need for burdensome manual prop filtering. + +```jsx +const StyledInput = styled("input", props => ({ + color: props.disabled ? "gray" : "black", + background: props.$variant === "error" ? "red" : "blue" +})); + +<StyledInput disabled={true} $variant="error" />; +``` + +### Build a style guide + +We recommend that you organize your styled components into a style guide to promote code reusability and consistency: + +```js +// src/style-guide/panel.js +import React from 'react'; +import {styled} from 'fusion-plugin-styletron-react'; + +export default styled('div', { + backgroundColor: 'silver', +}); + +// src/style-guide/button.js +import React from 'react'; +import {styled} from 'fusion-plugin-styletron-react'; + +export default styled('button', { + backgroundColor: 'silver', +}); + +// in your app +import Panel from './style-guide/panel'; +import Button from './style-guide/button'; + +export default ( + <Panel> + <Button>Click here!</Button> + </Panel> +); +``` + +### Variants + +A variant is pattern for implementing slightly different variations of a base style. Variants are a useful pattern to help codify the purposes of different style variations, and reduce the amount of inconsistencies in a style guide. + +Here's an example `Button` with a `danger` and `warning` variants: + +```js +// src/style-guide/button.js +import React from 'react'; +import {styled} from 'fusion-plugin-styletron-react'; + +const variants = { + default: {backgroundColor: 'silver'}, + danger: {backgroundColor: 'red'}, + warning: {backgroundColor: 'yellow'}, +} + +export default styled('button', ({$variant}) => variants[$variant] || variants.default); + +// in your app +import Panel from './style-guide/panel'; +import Button from './style-guide/button'; + +export default ( + <Panel> + <Button>Cancel</Button> + <Button $variant="danger">Delete</Button> + </Panel> +); +``` + +### Hover styles + +Here's how to add styles to the `:hover` pseudo-class: + +```js +import React from 'react'; +import {styled} from 'fusion-plugin-styletron-react'; + +export default styled('button', { + backgroundColor: 'silver', + ':hover': { + backgroundColor: 'gold', + }, +}); +``` + +### Media queries + +Styletron compiles CSS into atomic classes (i.e. one class per style declaration), which doesn't support media query declaration order in the same way that unoptimized CSS does. One way to get around that is to define mutually exclusive media queries: + +```js +import React from 'react'; +import {styled} from 'fusion-plugin-styletron-react'; + +export default styled('button', { + '@media (max-width: 1199px)': { + width: '100%', + }, + '@media (min-width: 1200px)': { + margin: '10px', + }, + background: 'silver', +}); +``` + +## Declarative `@keyframes` and `@font-face` rules + +Both `@font-face` and `@keyframes` rules can be used declaratively within style objects. + +### `@font-face` + +If a font face object is used in place of a string for `fontFamily`, a corresponding `@font-face` rule will be automatically generated. + +```jsx +const font = { + src: "..." +}; + +const Foo = styled("div", {fontFamily: font}); + +<Foo />; +``` + +### `@keyframes` + +If a keyframes object is used in place of a string for `animationName`, a corresponding `@keyframes` rule will be automatically generated. + +```jsx +const animation = { + from: {color: "red"}, + to: {color: "blue"} +}; + +const Foo = styled("div", {animationName: animation}); + +<Foo />; +``` + +#### Built-in props + +#### `$as` for rendering a different element + +```jsx +const Foo = styled("div", /* ... */); + +<Foo />; +<Foo $as="span" />; +<Foo $as={Link} />; +``` + +#### `$ref` for setting refs + +The the `$ref` prop to set a React `ref` on the underlying element. + +```jsx +const Foo = styled("div", /* ... */); + +class Component extends React.Component { + <Foo + $ref={c => { + this.foo = c; + }} + /> +} +``` diff --git a/documentation/docs/guides/testing/component.md b/documentation/docs/guides/testing/component.md new file mode 100644 index 0000000..7df6615 --- /dev/null +++ b/documentation/docs/guides/testing/component.md @@ -0,0 +1,201 @@ +# Component testing + +Component testing allows you to make assertions and validate your React component logic. We recommend using [enzyme](https://github.com/airbnb/enzyme/blob/master/README.md) to test React components. + +### Testing a functional React component + +Test a functional React component with enzyme's `shallow` +function. The subsections below show examples of different types of tests for React +components. + +#### Testing props + +This example defines a functional component that takes some props, renders them +via enzyme's `shallow`, and makes assertions on the output props. + +```js +// src/components/tag-title.js +import React from 'react'; + +export default ({name, isLoading}) => ( + <div style={{borderRightWidth: isLoading ? '1px' : '0px'}}> + {isLoading ? 'Deleting...' : name} + </div> +); + +// src/components/__tests__/tag-title.node.js +import React from 'react'; +import {shallow} from 'enzyme'; +import {test} from 'fusion-test-utils'; +import TagTitle from '../tag-title'; + +test('Normal title', assert => { + const mockData = { + name: 'myTag', + isLoading: false, + }; + + const output = shallow(<TagTitle {...mockData} />); + const outputProps = output.props(); + + assert.equal(outputProps.style.borderRightWidth, '0px', 'no border'); + assert.equal(outputProps.children, 'myTag', 'correct title'); +}); + +test('Loading title', assert => { + const loadingMockData = { + name: 'myTag', + isLoading: true, + }; + import React from 'react'; + + const output = shallow(<TagTitle {...loadingMockData} />); + const outputProps = output.props(); + + assert.equal(outputProps.style.borderRightWidth, '1px', 'no border'); + assert.equal(outputProps.children, 'Deleting...', 'correct title'); +}); +``` + +#### Simulate click + +This example defines a component with an `onClick` handler and uses fusion-test-utils' +`mockFunction` to check that the handler is called a click event is triggered. + +```js +// src/components/delete-icon.js +import React from 'react'; + +export default ({id, deleteFn}) => <a onClick={() => deleteFn(id)}>Delete</a>; + +// src/components/__tests__/delete-icon.node.js +import React from 'react'; +import {shallow} from 'enzyme'; +import {test, mockFunction} from 'fusion-test-utils'; +import DeleteIcon from '../delete-icon'; + +test('Delete button', assert => { + const mockData = { + id: 'myTag', + }; + + const deleteFn = mockFunction(); + const wrapper = shallow(<DeleteIcon {...mockData} deleteFn={deleteFn} />); + + wrapper.find('a').simulate('click'); + assert.equal( + deleteFn.mock.calls.length, + 1, + 'delete function only called once' + ); + assert.equal( + deleteFn.mock.calls[0][0], + mockData.id, + 'delete function called with correct id' + ); +}); +``` + +#### Test for presence + +This example tests success and error states for a component by asserting on the +expected shape of the output. + +```js +// src/components/example-error.js +import React from 'react'; + +export default class ExampleError extends Component { + render() { + const {error} = this.props; + + if (error) { + return <span>{error.stack}</span>; + } + return <div>No Errors Here</div>; + } +} + +// src/components/__tests__/example-error.node.js +import React from 'react'; +import {shallow} from 'enzyme'; +import {test} from 'fusion-test-utils'; +import ExampleError from '../example-error'; + +test('Without an error', assert => { + const wrapper = shallow(<ExampleError />); + + assert.equal(wrapper.find('div').length, 1, 'renders a div'); +}); + +test('With an error', assert => { + const wrapper = shallow(<ExampleError error={{stack: 'some stack'}} />); + + assert.equal(wrapper.find('span').length, 1, 'renders error'); +}); +``` + +### Testing a React component with lifecycle methods + +To test a React component's lifecycle methods, use enzyme's +`mount` function. The `mount` function requires tests to be run in a browser +environment. + +In the example below, we're interested in testing that the `getUser` function +gets called only if `user.name` and `user.error` are both falsy. + +```js +// src/components/tags-editor.js +import React from 'react'; + +export default class TagsEditor extends Component { + componentDidMount() { + const {name, error} = this.props.user; + if (!error && !name) { + this.props.getUser(); + } + } + + render() { + const {user} = this.props; + return ( + <div> + <h1>{user.name}</h1> + </div> + ); + } +} + +// src/components/__tests__/tags-editor.browser.js +import React from 'react'; +import {mount} from 'enzyme'; +import {test, mockFunction} from 'fusion-test-utils'; +import TagsEditor from '../tags-editor'; + +test('Hydrated render of tags editor', assert => { + const mockHydratedData = {user: {name: 'Test Name'}}; + + const getUser = mockFunction(); + const wrapper = mount(<TagsEditor {...mockHydratedData} getUser={getUser} />); + + assert.equal(getUser.mock.calls.length, 0, 'getUser not called'); +}); + +test('Requests data if not in props', assert => { + const mockUnhydratedData = {user: {}}; + + const getUser = mockFunction(); + mount(<TagsEditor {...mockUnhydratedData} getUser={getUser} />); + + assert.equal(getUser.mock.calls.length, 1, 'getUser called'); +}); + +test('Dont request data if errored', assert => { + const mockErredData = {user: {error: new Error()}}; + + const getUser = mockFunction(); + mount(<TagsEditor {...mockErredData} getUser={getUser} />); + + assert.equal(getUser.mock.calls.length, 0, 'getUser not called'); +}); +``` diff --git a/documentation/docs/guides/testing/index.md b/documentation/docs/guides/testing/index.md new file mode 100644 index 0000000..0e6dfaa --- /dev/null +++ b/documentation/docs/guides/testing/index.md @@ -0,0 +1,26 @@ +--- +title: Testing +path: /testing/ +--- + +# Testing + +Tests can be located anywhere in a project directory tree: simply create +`__tests__` directories and put your tests inside. You can have as many +`__tests__` directories as you want. This means you can colocate tests to +features just as easily as you can put all your tests in a single location. + +Fusion.js uses a file naming convention to determine which files correspond to +what kind of tests: files in a `__tests__` directory that end in `.node.js` will +be run in Node.js and files ending in `.browser.js` will be run in a browser environment (currently jsdom). + +Fusion.js uses a small wrapper around [jest](https://github.com/facebook/jest) to run +tests in Node.js and browser environments. We are currently exploring leveraging [unitest](https://github.com/rtsao/unitest) as a future test runner for Fusion.js applications. + +Find out about the various testing strategies that work with FusionJS applications: + +* [Component testing](/docs/guides/testing/component) +* [Unit testing](/docs/guides/testing/unit) +* [Snapshot testing](/docs/guides/testing/snapshot) +* [Simulation testing](/docs/guides/testing/simulation) +* [Integration testing](/docs/guides/testing/integration) diff --git a/documentation/docs/guides/testing/integration.md b/documentation/docs/guides/testing/integration.md new file mode 100644 index 0000000..0e41bb4 --- /dev/null +++ b/documentation/docs/guides/testing/integration.md @@ -0,0 +1,98 @@ +# Integration testing + +Integration tests verify that your modules work together as expected. They open your application within a browser, and perform actions in the same way that an end user would. + +* [Overview](#overview) +* [Example Puppeteer test](#example-puppeteer-test) +* [Debugging tests](#debugging-tests) + * [Headless mode](#headless-mode) + * [Screenshots](#screenshots) +* [Common pitfalls](#common-pitfalls) + * [Appropriate waiting](#appropriate-waiting) + * [Resiliant selectors](#resiliant-selectors) + +## Overview + +We currently recommend using [puppeteer](https://github.com/GoogleChrome/puppeteer) for integration testing. + +## Example Puppeteer test + +```js +/* eslint-env node */ +import puppeteer from 'puppeteer'; +import {test} from 'fusion-test-utils'; + +test('Page has content', async assert => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('http://localhost:3000/'); + const content = await page.content(); + assert.ok(content.length > 0); + await page.close(); + await browser.close(); +}); +``` + +## Debugging tests + +Integration tests can be incredibly useful for catching errors before they hit production, but also tricky to debug without the right tooling. Here's a few tools which you can use to debug your tests, and ensure that they don't fail intermittently. + +### Headless mode + +Puppeteer launches Chromium in headless mode by default. To launch a full version of Chromium, set the 'headless' option when launching a browser. This can be useful when debugging as you can see the actual browser window. + +```js +const browser = await puppeteer.launch({headless: false}); // default is true +``` + +### Screenshots + +Screenshots are a very useful tool for recording the state of a page at a given point within your test. You can have your test always record screenshots, or only when there's an unexpected error. + +```js +await page.goto('https://www.uber.com'); +await page.screenshot({ path: 'screenshots/homepage.png' }); +``` + +## Common pitfalls + +Care must be used when writing an integration test to ensure that intermittently failing tests are avoided. Here are some strategies you can use to ensure that your tests are more resilient to failure. + +### Appropriate waiting + +Waiting for a set amount of milliseconds can be prone to failure due to undeterministic load times, or running on different classes of hardware. You should always avoid waiting for a set amount of time, and instead opt to wait for some condition to be true. + +```js +// Never do this as it's prone to failure. +await page.waitFor(5000); + +// This is good, we wait for the element to appear. +// This could be used at any time, for example, after starting the test or clicking on a link. +await page.waitForSelector('.some-thing-present'); + +// A more complex wait, where we wait for a DOM condition to be true. +await page.waitFor(async () => { + const content = await page.content(); + return content.includes('some-delayed-content'); +}); +``` + +### Resiliant selectors + +Building integration tests with unsuitable selectors can cause unexpected failures and lost productivity. It is often not a good idea to query for selectors which are only used for presentational cases, as these change and cause tests to break. We are currently investing adding deterministic querying capabilities using enzyme selectors alongside puppeteer. Here's a few examples of selectors to use, and some to avoid. + +```js +// Bad selector, not specific enough and only based on presentation. +'.col-2 span'; + +// Bad selector, this is more specific, but could easily break if another list was added to the page. +'ul li:first-child span'; + +// A good selector, based on semantic tag names and specific attributes. +'ul#past-trips li:first-child img.avatar'; + +// A good selector as it's clear this is used for testing. +// Occasionally it may be necessary to add custom hooks for testing. +// Data attributes are one way of doing this, and we recommend prefixing with `data-test-` +'[data-test-custom-hook]'; +``` diff --git a/documentation/docs/guides/testing/simulation.md b/documentation/docs/guides/testing/simulation.md new file mode 100644 index 0000000..df09ac9 --- /dev/null +++ b/documentation/docs/guides/testing/simulation.md @@ -0,0 +1,78 @@ +# Simulation testing + +It is often useful to simulate a request/render of a Fusion application. The `fusion-test-utils` library exports some utilities to make this easy. +You can create a `test-app` file that will load your Fusion app from `src/main.js` and register some commonly used mocks. In the following example, we will use that test utility to test a server side render and browser side render of an application including the data fetching it does. + +The test in `src/components/__tests__/hello.node.js` tests server-side rendering +and the test in `src/components/__tests__/hello.browser.js` tests browser +rendering. + +```js +// src/redux.js +export default { + reducer: (state, {payload}) => ({...state, ...payload}), // contrived reducer; normally would use proper reducer composition + preloadedState: {}, +}; + +// src/components/hello.js +import React from 'react'; +import {withRPCRedux} from 'fusion-plugin-rpc-redux-react'; +import {prepared} from 'fusion-react-async'; +import {compose} from 'redux'; +import {connect} from 'react-redux'; + +const Hello = ({name}) => <div>Hello {name}</div>; + +export default compose( + connect(({name}) => ({name})), + withRPCRedux('getUser'), + prepared(props => { + return ( + props.user || + props.getUser() + ); + }) +)(Hello); + +// src/components/__tests__/hello.node.js +import loadApp from '../../test-utils/test-app'; +import React from 'react'; +import App from 'fusion-react'; +import Redux from 'fusion-plugin-react-redux'; +import {mock as MockRPC, RPCHandlersToken, RPCToken} from 'fusion-plugin-rpc-redux-react'; +import {test, getSimulator} from 'fusion-test-utils'; + +test('getUser works', async assert => { + const app = await loadApp(); + app.register(RPCToken, MockRPC); + app.register(RPCHandlersToken, { + async getUser() { + return {name: 'Bob'} + } + }); + const simulator = getSimulator(app); + const ctx = await render(app, '/'); + assert.ok(ctx.rendered.includes('Bob'), 'returns data'); +}); + +// src/components/__tests__/hello.browser.js +import loadApp from '../../test-utils/test-app'; +import React from 'react'; +import App from 'fusion-react'; +import Redux from 'fusion-plugin-react-redux'; +import {mock as MockRPC, RPCHandlersToken, RPCToken} from 'fusion-plugin-rpc-redux-react'; +import {test, getSimulator} from 'fusion-test-utils'; + +test('getUser works', async assert => { + const app = await loadApp(); + app.register(RPCToken, MockRPC); + app.register(RPCHandlersToken, { + async getUser() { + return {name: 'Bob'} + } + }); + const simulator = getSimulator(app); + const ctx = await render(app, '/'); + assert.equal(ctx.rendered.find(Hello).text(), 'Hello Bob', 'renders'); +}); +``` diff --git a/documentation/docs/guides/testing/snapshot.md b/documentation/docs/guides/testing/snapshot.md new file mode 100644 index 0000000..935c177 --- /dev/null +++ b/documentation/docs/guides/testing/snapshot.md @@ -0,0 +1,32 @@ +# Snapshot testing + +Snapshot testing is one possible way of preventing regressions by surfacing differences in components and objects. These tests may be run in either Node or browser environments, and against components or data structures. When snapshots need to be updated, you can update them with: `yarn test --updateSnapshot`. For more information see the [Jest snapshot testing documentation](https://facebook.github.io/jest/docs/en/snapshot-testing.html). + +Example of snapshot testing a component: + +```js +import React from 'react'; +import {test} from 'fusion-test-utils'; +import {shallow} from 'enzyme'; + +import MyComponent from '../MyComponent'; + +test('MyComponent', async assert => { + const wrapper = shallow(<MyComponent />); + assert.matchSnapshot(wrapper); +}); +``` + +Snapshot testing a serializable object: + +```js +import React from 'react'; +import {test} from 'fusion-test-utils'; + +import someLogic from '../logic'; + +test('test that should match snapshot', () => { + const fixture = ['some', 'data', 'here']; + assert.matchSnapshot(someLogic(fixture)); +}); +``` diff --git a/documentation/docs/guides/testing/unit.md b/documentation/docs/guides/testing/unit.md new file mode 100644 index 0000000..34e831e --- /dev/null +++ b/documentation/docs/guides/testing/unit.md @@ -0,0 +1,102 @@ +# Unit testing + +Use unit testing to ensure that your methods behave as expected within the browser and Node environments. + +### Testing a Redux reducer + +#### Simple reducer + +Testing Redux reducers is straightforward. Just call the reducer function with +the initial state and action you want, and assert on the shape of the returned +state. + +```js +// src/redux.js +export default { + reducer: (state, action) => state || {myDefaultState: 'foo'}, +}; + +// src/__tests__/redux.node.js +import reduxOptions from '../redux'; + +test('Empty action', assert => { + const state = reduxOptions.reducer(undefined, {}); + const expectedState = {myDefaultState: 'foo'}; + + assert.deepEqual(state, expectedState, 'returns default state'); +}); +``` + +#### RPC reducer + +RPC calls can trigger 3 different actions (start, success, failure). The names +for these action types are the RPC method name in all caps snake case followed +by either `_START`, `_SUCCESS` or `_FAILURE`). To test each case, pass an action +with the appropriate type. + +```js +// src/reducers/user.js +export default createRPCReducer('getUser', { + start: () => ({ + loading: true, + user: null, + error: null, + }), + success: (s, action) => ({ + user: action.payload.user, + error: null, + loading: false, + }), + failure: (s, action) => ({ + loading: false, + error: action.payload, + user: null, + }), +}); + +// src/reducers/__tests__/user.node.js +import userReducer from '../user'; + +test('Empty action', assert => { + const state = userReducer(undefined, {}); + const expectedState = {data: {}}; + + assert.deepEqual(state, expectedState, 'returns default state'); +}); + +test('getUser start', assert => { + const action = {type: 'GET_USER_START', payload: {}}; + const state = userReducer(undefined, action); + const expectedState = {user: null, error: null, loading: true}; + + assert.deepEqual(state, expectedState, 'enables isLoading'); +}); + +test('getUser success', assert => { + const initialState = {user: null, error: null, loading: true}; + const action = {type: 'GET_USER_SUCCESS', payload: {user: {name: 'My Name'}}}; + const state = userReducer(initialState, action); + const expectedState = {user: {name: 'My Name'}, error: null, loading: false}; + + assert.deepEqual(state, expectedState, 'adds user and disables isLoading'); +}); + +test('getUser failure', assert => { + const initialState = {user: null, error: null, loading: true}; + const action = { + type: 'GET_USER_FAILURE', + payload: {error: 'Something went wrong'}, + }; + const state = userReducer(initialState, action); + const expectedState = { + user: null, + error: 'Something went wrong', + loading: false, + }; + + assert.deepEqual(state, expectedState, 'adds error and disables isLoading'); +}); +``` + +Normally, rather than testing RPC reducers in isolation, it's more convenient +and robust to write integration tests. diff --git a/documentation/docs/guides/typing.md b/documentation/docs/guides/typing.md new file mode 100644 index 0000000..085a452 --- /dev/null +++ b/documentation/docs/guides/typing.md @@ -0,0 +1,118 @@ +--- +title: Typing +path: /typing/ +--- + +# Typing + +Fusion.js supports [Flow](https://flow.org/) out of the box for static type checking. + +### Why use static types in JavaScript? + +JavaScript lacks language level support for static types, making it difficult to ensure that code you write has the correct types at compile time. Although you can write JavaScript code without any type annotations and have it work, there is the risk that bad code leads to malformed inputs being provided to methods or functions that have to then either gracefully fail (e.g. runtime type checking with error handling) or fail hard (e.g. unhandled exceptions). In the worst case, a mismatched type goes unchecked and cascades to a failure down the road leading to lengthy debugging and unintended side effects. + +At its core, static type checking aims to verify and enforce that values being used by your code meet the invariant conditions specified by the types at **compile time**. This offers a number of advantages over dynamic typing: + +* Early bug and error detection due to type mismatches +* Self-documenting code through type annotations +* Cleaner error handling to check variable or argument types at runtime +* Increases portability of code +* Improves the testing experience - no more tediously testing for mistyped inputs + +### Why use Flow? + +Flow is not the only solution for static type checking in JavaScript. It is, however, an elegant solution that provides benefits **without having to change any of your existing code**. Flow has the ability to infer types within your code, ensuring that it can help catch type errors from the get go. And adding type annotations allows for incrementally improving type coverage. We recommended that you use Flow since Fusion.js provides types for core components of the framework that can be used in your application. + +### Getting Started + +If you are new to Flow, we recommended that you check out the [Getting Started](https://flow.org/en/docs/getting-started/) and [Type Annotations](https://flow.org/en/docs/types/) documentation. + +If you use Visual Studio Code as your primary IDE, check out the [Flow for Visual Studio Code](https://github.com/flowtype/flow-for-vscode) extension. + +### Annotating a simple React component + +Annotating React components with type annotations can be very powerful for ensuring your components are used as expected. To illustrate this, let's write a very simple component that displays a user's name. + +```js +// src/components/display-name +// @flow +import React from 'react'; + +type Props = {| + firstName: string, + lastName: ?string // may be undefined, if last name not provided +|} +class DisplayNameComponent extends React.Component<Props> { + render() { + const fullName = `${this.props.firstName} ${this.props.lastName || ''}`; + return <div>{fullName}</div>; + } +} +``` + +If we run `flow run check`, we'd expect to see find no errors. Let's see what happens if we try to also display a user's age. + +```js +class DisplayNameComponent extends React.Component<Props> { + render() { + const fullName = `${this.props.firstName} ${this.props.lastName || ''}`; + const age = this.props.age; // type error! + return <div>{fullName}</div>; + } +} +``` + +In this case, we get the an error that the property `age` was not found in `Props`, as illustrated [here](https://flow.org/try/#0PTAEGcCcGNmh7AtgB3gOwKZoC7mAEwEtxkAbAQwE8BaNcxDAKBFAAEAzU+Ad0cJXiRsoAEoZy0Ye0hJQAckjjJcgNyNG2SsgygACjOThQAXlABvAD6NQodoUjhsAOXoYAXBGyRCaAOYAaa1AKRxcGDwB+R28-UBZEKlAAIx0AVzR8DDtMfH9QQnZg8kdQOgZS+GFkGQA3Qkz8RgsAX0ZoEKMAEWIyKjCMAGEkVEwcUAwAD2wsfCMxCWwAOiGBUewAHn14QwA+cyDFDIxIAAoASn2bGwQ0EvZU0lJ+k1AAAwASM2wAC2JF6u24EWdgczlczVAnx+fwBhkWITB5QsFnkcmarzUV1ANxK5F8OlM0KBsKBeIwmKuimwqUgaFA6yINR2Znuj36zXWBEITIprWaQA). + +We recommended that you check out the [Flow + React documentation](https://flow.org/en/docs/react/). + +### Annotating a simple Fusion.js plugin + +To see how we can leverage built in Fusion.js types, let's write a very simple Fusion.js plugin that logs a random cat fact on every request to `/log-cat-fact`. + +```js +// src/plugins/cat-facts.js +// @flow + +import {createPlugin} from 'fusion-core'; +import {LoggerToken} from 'fusion-tokens'; +import type {Context} from 'fusion-core'; + +/* Helpers */ +const FACTS: Array<string> = [ // provided by: https://catfact.ninja/fact + "A 2007 Gallup poll revealed that both men and women were equally likely to own a cat.", + "Most cats adore sardines.", + "Cats have been domesticated for half as long as dogs have been.", + "The average litter of kittens is between 2 - 6 kittens.", +]; +function getCatFact(): string { + return FACTS[Math.floor(Math.random() * FACTS.length)]; +} + +export default createPlugin({ + deps: {logger: LoggerToken}, + middleware: deps => (ctx: Context, next: () => Promise<*>) => { + if(ctx.url === '/log-cat-fact') { + deps.logger.log(getCatFact()); + } + return next(); + } +}); +``` + +Let's break this down. We import a `type {Context}` from `fusion-core` which is used in our middleware signature. This let's us ensure safe accessibility of the `ctx` object. We are also using the dependency injected logger which is registered with `LoggerToken`. If we look at the type of `logger`, we would expect to see: + +```js +// defined in https://github.com/fusionjs/fusion-tokens/blob/master/src/index.js#L24 +export type Logger = { + log(level: string, arg: any): void, + error(arg: any): void, + warn(arg: any): void, + info(arg: any): void, + verbose(arg: any): void, + debug(arg: any): void, + silly(arg: any): void, +}; +``` + +If we tried to do `deps.logger.notALogMethod(...)` we would expect a type error. Fusion.js will automatically hook up these injected types when they are available, ensuring more seamless type safety across the Fusion.js ecosystem. diff --git a/documentation/docs/guides/universal-rendering.md b/documentation/docs/guides/universal-rendering.md new file mode 100644 index 0000000..1a50d06 --- /dev/null +++ b/documentation/docs/guides/universal-rendering.md @@ -0,0 +1,154 @@ +--- +title: Universal rendering +path: /universal-rendering/ +--- + +# Universal rendering + +Fusion.js supports universal rendering. Universal rendering means that large parts of the codebase can run on the server (for performing server-side rendering) and on the browser. + +In some frameworks, this is only limited to React code. In Fusion.js, the entire application runs in an universal context by default, from React components to middlewares in Fusion.js plugins. This means that plugin registration code only needs to be written once even if it requires server-only or browser-only code (e.g. custom hydration code), and that plugins can activate behavior across the entire lifecycle of an application without the need to configure things in many different parts of the app. + +Naturally, you can also write React code once and have that code be automatically server-side rendered as you would expect. + +## Server-only / browser-only code + +It is sometimes desirable to write server-only code, browser-only code, and at times, development-only code. To enable that, Fusion.js provides the `__NODE__`, `__BROWSER__` and `__DEV__` global flags. These special flags are statically replaced by the compiler with the appropriate boolean value, depending on which bundle the code is compiled for. Then, unused code gets removed via tree shaking and dead code elimination. + +To write code that only runs in the server, wrap your code in the appropriate code fence: + +```js +if (__NODE__) { + // server-side code goes here +} +``` + +To write code that only runs in the browser: + +```js +if (__BROWSER__) { + // client-side code goes here +} +``` + +We recommend that you only use `__DEV__` to enhance developer experience with regards to error conditions: + +```js +// this conditional gets removed from the browser bundle in production, saving a few bytes +if (__DEV__) { + throw new Error( + 'The `{options}` argument is required. See the documentation at https://the-docs-website/api-docs/the-package' + ); +} +``` + +You should avoid writing significantly different code branches for different environments since doing so introduces more potential points of failure, and makes it more difficult to reproduce issues with confidence. + +We also recommend that you use `__DEV__` and avoid using `process.env.NODE_ENV === 'production'`, since Fusion.js provides better static compilation and eslint support for the former. + +## Imports + +The ES6 standard specifies that `import`/`export` syntax cannot appear inside of an `if` statement of conditional expressions, but Fusion.js is still able to intelligently eliminate server-side imports from client-side bundles, thanks to tree-shaking. + +Consider the example below: + +```js +import fs from 'fs'; + +if (__NODE__) fs.readFileSync('package.json'); +``` + +The compiler removes the `fs.readFileSync()` call from the browser bundle because the `if (__NODE__)` code fence evaluates to `false`, making the code branch unreacheable. + +The `import` statement is outside of the code fence, but it is also removed because the compiler infers that it's also dead code, because no code paths ever use `fs` in this file for the browser bundle! + +### Server-side side effects in dependencies + +On some rare occasions, poorly written server-side packages might incur top-level side-effects. In those cases, the compiler becomes unable to treeshake the misbehaving dependency in the browser bundle, and compilation typically fails due to unpolyfilled server dependencies. + +A simple way to avoid this issue is to simply load the module dynamically via a good old CommonJS `require` call. + +```js +// before +import foo from 'misbehaving-dependency'; + +// after +const foo = __NODE__ && require('misbehaving-dependency'); +``` + +Now the code follows the basic dead code elimination rules and the browser bundle will be compiled as expected. + +--- + +## Linting + +Fusion.js provides an `eslint-config-fusion` configuration that issues contextual warnings depending on whether code is server-side or client-side. + +```sh +yarn add eslint-config-fusion +``` + +To enable it, add it to your `.eslintrc.js`: + +```js +module.exports = { + extends: [require.resolve('eslint-config-fusion')], +}; +``` + +Now ESLint will complain if you inadvertedly forget to code-fence: + +```js +// we didn't code fence this browser-specific code, so it would also try to run in the server. Thus, eslint complains +window.addEventListener('load', () => {}); + +// after we code-fence, the eslint warning goes away +if (__BROWSER__) { + window.addEventListener('load', () => {}); +} +``` + +You can also mark an entire file as server-only or browser-only: + +```js +/* eslint-env node */ +``` + +```js +/* eslint-env browser */ +``` + +This pattern is useful if a plugin has vastly different implementations on the server and the browser: + +```js +// plugin-entry-point.js +import server from './server'; +import client from './client'; + +export default __NODE__ ? server : client; + +// server.js +/* eslint-env node */ +export default serverCodeGoesHere; + +// server.js +/* eslint-env browser */ +export default browserCodeGoesHere; +``` + +--- + +## Disabling server-side rendering + +Sometimes it is desirable to avoid server-side rendering. To do that, override the `render` argument when instantiating `App`: + +```js +// src/main.js +import App from 'fusion-react'; +import ReactDOM from 'react-dom'; + +const render = __NODE__ + ? () => '<div id="root"></div>' + : el => ReactDOM.render(el, document.getElementById('root')); +const app = new App(root, render); +``` diff --git a/gatsby-browser.js b/gatsby-browser.js new file mode 100644 index 0000000..5fd955e --- /dev/null +++ b/gatsby-browser.js @@ -0,0 +1,14 @@ +const React = require('react'); +const Styletron = require('styletron-client'); +const {StyletronProvider} = require('styletron-react'); + +exports.wrapRootComponent = ({Root}) => () => { + const styleElements = document.getElementsByClassName('_styletron_hydrate_'); + const styletron = new Styletron(styleElements); + + return ( + <StyletronProvider styletron={styletron}> + <Root /> + </StyletronProvider> + ); +}; diff --git a/gatsby-config.js b/gatsby-config.js new file mode 100644 index 0000000..b30da63 --- /dev/null +++ b/gatsby-config.js @@ -0,0 +1,81 @@ +/* eslint-env node */ +const {lstatSync, readdirSync} = require('fs'); +const path = require('path'); + +function getPackages(source) { + const isDirectory = dir => + lstatSync(source).isDirectory(path.join(source, dir)); + return readdirSync(source).filter(isDirectory); +} + +function createSourcePlugins() { + return getPackages(path.join(__dirname, './fusion')).map(pkg => { + const pkgOriginal = pkg === 'fusion-docs' ? 'fusionjs.github.io' : ''; + return { + resolve: 'gatsby-source-filesystem', + options: { + name: `package|${pkg}${pkgOriginal ? `|${pkgOriginal}` : ''}`, + path: path.join(__dirname, './fusion', pkg), + }, + }; + }); +} + +function isDevEnv() { + return process.env['NODE_ENV'] === 'development'; +} + +module.exports = { + siteMetadata: { + title: 'Fusion.js Development', + }, + pathPrefix: '/', + plugins: [ + 'gatsby-plugin-react-next', + { + resolve: 'gatsby-source-filesystem', + options: { + name: 'docs', + path: path.join(__dirname, './documentation/docs'), + }, + }, + { + resolve: 'gatsby-source-filesystem', + options: { + name: 'api', + path: path.join(__dirname, './documentation/api'), + }, + }, + ...createSourcePlugins(), + 'gatsby-plugin-react-helmet', + { + resolve: 'gatsby-transformer-remark', + options: { + plugins: [ + 'gatsby-remark-transform-links', + 'gatsby-remark-autolink-headers', + { + resolve: 'gatsby-remark-prismjs', + options: { + // Class prefix for <pre> tags containing syntax highlighting; + // defaults to 'language-' (eg <pre class="language-js">). + // If your site loads Prism into the browser at runtime, + // (eg for use with libraries like react-live), + // you may use this to prevent Prism from re-processing syntax. + // This is an uncommon use-case though; + // If you're unsure, it's best to use the default value. + classPrefix: 'gatsby-remark-', + }, + }, + ], + }, + }, + 'gatsby-plugin-catch-links', + { + resolve: 'gatsby-plugin-google-analytics', + options: { + trackingId: 'UA-7157694-88', + }, + }, + ], +}; diff --git a/gatsby-node.js b/gatsby-node.js new file mode 100644 index 0000000..c23531b --- /dev/null +++ b/gatsby-node.js @@ -0,0 +1,60 @@ +/* eslint-env node */ +const path = require('path'); +const {buildPagePath} = require('./src/utils'); + +exports.createPages = ({graphql, boundActionCreators}) => { + const {createPage} = boundActionCreators; + return new Promise((resolve, reject) => { + const docTemplate = path.resolve('./src/templates/doc.js'); + const templates = { + docs: docTemplate, + }; + + resolve( + graphql(` + { + allFile(filter: {extension: {eq: "md"}}) { + edges { + node { + sourceInstanceName + childMarkdownRemark { + html + frontmatter { + title + path + date(formatString: "MMMM DD, YYYY") + category + } + } + relativePath + absolutePath + dir + name + } + } + } + } + `).then(result => { + if (result.errors) { + reject(result.errors); + } + + // Create docs pages + result.data.allFile.edges.forEach(({node}) => { + const {localPath, remoteUrl} = buildPagePath(node); + createPage({ + path: localPath, + component: templates[node.sourceInstanceName] || templates.docs, + context: { + path: localPath, + metadata: node.childMarkdownRemark.frontmatter, + html: node.childMarkdownRemark.html, + remoteUrl, + }, + }); + }); + return null; + }) + ); + }); +}; diff --git a/gatsby-ssr.js b/gatsby-ssr.js new file mode 100644 index 0000000..6278e78 --- /dev/null +++ b/gatsby-ssr.js @@ -0,0 +1,34 @@ +const React = require('react'); +const Styletron = require('styletron-server'); +const {StyletronProvider} = require('styletron-react'); +const {renderToString} = require('react-dom/server'); + +exports.replaceRenderer = ({ + bodyComponent, + setHeadComponents, + replaceBodyHTMLString, +}) => { + const styletron = new Styletron(); + + const app = ( + <StyletronProvider styletron={styletron}>{bodyComponent}</StyletronProvider> + ); + + replaceBodyHTMLString(renderToString(app)); + + const stylesheets = styletron.getStylesheets(); + const headComponents = stylesheets.map((sheet, index) => { + return ( + <style + media={sheet.media} + className="_styletron_hydrate_" + key={index} + dangerouslySetInnerHTML={{ + __html: sheet.css, + }} + /> + ); + }); + + setHeadComponents(headComponents); +}; diff --git a/package.json b/package.json index ec54b9c..dbaa754 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,63 @@ { - "name": "fusion-docs", - "version": "1.0.0", - "main": "index.js", - "repository": "git@github.com:fusionjs/fusionjs.github.io.git", - "author": "Kevin Grandon <kevingrandon@yahoo.com>", + "name": "fusionjs-docs", + "description": "Fusion.js documentation", + "private": true, + "version": "0.0.0", + "author": "Web Platform Team <eng-web-platform@uber.com>", "license": "MIT", + "repository": { + "type": "git", + "url": "git@github.com:fusionjs/fusionjs.github.io.git" + }, + "keywords": [], + "dependencies": { + "express": "^4.16.2", + "gatsby": "^1.9.17", + "gatsby-link": "^1.6.16", + "gatsby-plugin-catch-links": "^1.0.17", + "gatsby-plugin-google-analytics": "^1.0.14", + "gatsby-plugin-react-helmet": "^1.0.5", + "gatsby-plugin-react-next": "^1.0.4", + "gatsby-remark-autolink-headers": "^1.4.8", + "gatsby-remark-copy-linked-files": "^1.5.7", + "gatsby-remark-prismjs": "^1.2.8", + "gatsby-source-filesystem": "^1.4.12", + "gatsby-transformer-remark": "^1.7.7", + "react": "^16.0.0", + "react-dom": "^16.0.0", + "react-helmet": "^5.2.0", + "styletron-client": "^3.0.0-rc.1", + "styletron-react": "^3.0.0-rc.2", + "styletron-server": "^3.0.0-rc.1", + "unist-util-visit": "^1.2.0" + }, + "devDependencies": { + "babel-eslint": "^8.0.1", + "eslint": "^4.6.1", + "eslint-config-prettier": "^2.6.0", + "eslint-plugin-prettier": "^2.3.1", + "eslint-plugin-react": "^7.4.0", + "prettier": "^1.6.1" + }, "scripts": { - "dev": "bundle install && bundle exec jekyll serve" + "bootstrap": "./bootstrap.sh", + "lint": "eslint .", + "prettier": "prettier *", + "build-only": "gatsby build --prefix-paths", + "build": "yarn run bootstrap && yarn run build-only", + "clean-docs": "rm -rf docs", + "cp-docs": + "yarn run clean-docs && mkdir -p docs/_build/html && cp -a public/. docs/_build/html/", + "build-docs": "yarn run build && yarn run cp-docs", + "start": "node src/server/index.js", + "dev": "NODE_ENV=development gatsby develop", + "format": + "prettier --trailing-comma es5 --no-semi --single-quote --write 'src/**/*.js'", + "test": "yarn run lint" + }, + "engines": { + "node": "^8.11.1", + "npm": "5.6.0", + "yarn": "^1.6.0" } } diff --git a/packages-oss.txt b/packages-oss.txt new file mode 100644 index 0000000..a1380e6 --- /dev/null +++ b/packages-oss.txt @@ -0,0 +1,26 @@ +fusion-apollo +fusion-apollo-universal-client +fusion-core +fusion-cli +fusion-plugin-browser-performance-emitter +fusion-plugin-csrf-protection +fusion-plugin-csrf-protection-react +fusion-plugin-error-handling +fusion-plugin-font-loader-react +fusion-plugin-i18n +fusion-plugin-i18n-react +fusion-plugin-jwt +fusion-plugin-node-performance-emitter +fusion-plugin-react-redux +fusion-plugin-react-router +fusion-plugin-redux-action-emitter-enhancer +fusion-plugin-rpc +fusion-plugin-rpc-redux-react +fusion-plugin-styletron-react +fusion-plugin-universal-events +fusion-plugin-universal-events-react +fusion-plugin-universal-logger +fusion-react +fusion-react-async +fusion-rpc-redux +fusion-test-utils diff --git a/plugins/gatsby-remark-transform-links/index.js b/plugins/gatsby-remark-transform-links/index.js new file mode 100644 index 0000000..51934c5 --- /dev/null +++ b/plugins/gatsby-remark-transform-links/index.js @@ -0,0 +1,37 @@ +const visit = require('unist-util-visit'); +const {URL} = require('url'); +const path = require('path'); + +// the pathPrefix in args comes empty and the __PREFIX_PATHS__ not set +// so getting the prefix here from the config +const {pathPrefix} = require('../../gatsby-config.js'); + +module.exports = ({markdownAST}) => { + visit(markdownAST, 'link', node => { + const fusionjsLink = 'https://github.com/fusionjs/'; + const reAbsoluteLink = new RegExp('^(?:[a-z]+:)?//', 'i'); + // strip .md extention in any relative link + if (!reAbsoluteLink.test(node.url)) { + node.url = node.url.replace(/\.md$/i, ''); + } + // update github's fusionjs links to docs to links to docs website + if (node.url.indexOf(fusionjsLink) === 0) { + const linkUrl = new URL(node.url); + // change a link to a fusionjs's packages docs to a relative docs website link + linkUrl.pathname = path.join( + '/api', + linkUrl.pathname.replace(/^\/fusionjs\//i, '') + ); + // remove branch path + linkUrl.pathname = linkUrl.pathname.replace('/blob/master/', '/'); + // strip .md extention in any fusionjs link + node.url = linkUrl + .toString() + .replace('https://github.com/', '/') + .replace(/\.md$/i, ''); + + // prepend the pathPrefix to all relative links + node.url = `${pathPrefix === '/' ? '' : pathPrefix}${node.url}`; + } + }); +}; diff --git a/plugins/gatsby-remark-transform-links/package.json b/plugins/gatsby-remark-transform-links/package.json new file mode 100644 index 0000000..9e18a7d --- /dev/null +++ b/plugins/gatsby-remark-transform-links/package.json @@ -0,0 +1,4 @@ +{ + "name": "gatsby-remark-transform-links", + "version": "" +} diff --git a/src/components/footer.js b/src/components/footer.js new file mode 100644 index 0000000..0b5245c --- /dev/null +++ b/src/components/footer.js @@ -0,0 +1,62 @@ +import React from 'react'; +import {styled} from 'styletron-react'; +import Link from 'gatsby-link'; +import {PageWidth, FlexContainer, FlexItem} from '../layouts/styled-elements'; + +const Footer = styled('footer', ({styleProps = {}}) => ({ + width: '100%', + height: '90px', + backgroundColor: styleProps.hasSideNav ? '#f8f8f9' : '#041725', + color: '#999', + fontSize: '12px', + ...styleProps.overrides, +})); + +const ExternalLink = styled('a', { + backgroundColor: 'transparent', + color: 'inherit', + textDecoration: 'none', +}); + +const InternalLink = props => { + return <Link style={{backgroundColor: 'transparent'}} {...props} />; +}; + +const LinkText = styled('span', { + color: '#999', + textTransform: 'uppercase', + ':hover': { + textDecoration: 'underline', + }, +}); + +export default ({styleProps = {}}) => { + return ( + <Footer {...{styleProps}}> + <PageWidth> + <FlexContainer + styleProps={{ + overrides: { + ...(styleProps.hasSideNav && {paddingLeft: '336px'}), + }, + }} + > + <FlexItem> + <p>© 2018 Uber Technologies Inc.</p> + </FlexItem> + <FlexItem> + <p> + <ExternalLink href="https://github.com/fusionjs"> + <LinkText>Github</LinkText> + </ExternalLink> +    |    + <InternalLink to="/"> + <LinkText>Home</LinkText> + </InternalLink> + </p> + </FlexItem> + </FlexContainer> + </PageWidth> + </Footer> + ); +}; diff --git a/src/components/main-nav.js b/src/components/main-nav.js new file mode 100644 index 0000000..eeb3706 --- /dev/null +++ b/src/components/main-nav.js @@ -0,0 +1,68 @@ +import React from 'react'; +import {styled} from 'styletron-react'; +import Helmet from 'react-helmet'; +import Link from 'gatsby-link'; +import {MainNavContainer, MainNavItem} from './styled-elements'; +import {getPath} from '../utils'; + +const InternalLink = props => { + return <Link style={{backgroundColor: 'transparent'}} {...props} />; +}; + +const ExternalLink = styled('a', ({styleProps = {}}) => ({ + backgroundColor: 'transparent', + ...styleProps.overrides, +})); + +const Nav = props => { + const {data, location, pathPrefix} = props; + + function matchPath(regexp) { + return location.pathname.match(regexp); + } + + function isActive(path) { + const reNews = new RegExp(`^${pathPrefix}${path}(/|$)`); + return matchPath(reNews); + } + + return ( + <MainNavContainer data-test="main-nav"> + {data.map((item, index) => { + const re = new RegExp(`^${pathPrefix}${item.pathPrefix}(/|$)`); + const isActive = matchPath(re); + + return ( + <InternalLink + key={index} + to={item.path || getPath(item.pathPrefix, item.children[0])} + > + {isActive ? ( + <Helmet + titleTemplate={`%s | ${item.title} | Fusion.js Engineering`} + title={item.title} + /> + ) : null} + <MainNavItem styleProps={{isActive}}>{item.title}</MainNavItem> + </InternalLink> + ); + })} + <InternalLink to="/support"> + <MainNavItem + styleProps={{ + isActive: isActive('/support'), + overrides: { + '@media (max-width: 474px)': { + display: 'none', + }, + }, + }} + > + Support + </MainNavItem> + </InternalLink> + </MainNavContainer> + ); +}; + +export default Nav; diff --git a/src/components/side-nav.js b/src/components/side-nav.js new file mode 100644 index 0000000..fcecccc --- /dev/null +++ b/src/components/side-nav.js @@ -0,0 +1,118 @@ +import React from 'react'; +import Link from 'gatsby-link'; +import Helmet from 'react-helmet'; +import { + SideNavContainer, + SideNavUl, + SideNavLi, + SideNavItem, +} from './styled-elements'; +import {getPath, stripTrailingSlash} from '../utils'; + +class NavItemGroup extends React.Component { + render() { + const {path, title, children, isActive} = this.props; + return ( + <SideNavLi> + <NavLink + path={path} + style={{textDecoration: 'none'}} + isActive={isActive} + > + {title} + </NavLink> + <SideNavUl styleProps={{overrides: {paddingLeft: '16px'}}}> + {children} + </SideNavUl> + </SideNavLi> + ); + } +} + +class NavItem extends React.Component { + render() { + return ( + <SideNavLi> + <NavLink {...this.props} /> + </SideNavLi> + ); + } +} + +class NavLink extends React.Component { + render() { + const {path, children, isActive} = this.props; + if (!path) { + return ( + <SideNavItem styleProps={{isActive: isActive, noLink: true}}> + {children} + </SideNavItem> + ); + } else { + return ( + <Link + to={path} + style={{textDecoration: 'none', backgroundColor: 'transparent'}} + > + <SideNavItem styleProps={{isActive: isActive}}> + {children} + </SideNavItem> + </Link> + ); + } + } +} + +class SideNav extends React.Component { + buildChildren(navData = {}) { + const {location, pathPrefix: routePrefix} = this.props; + const {children = [], pathPrefix = ''} = navData; + + return children.map((item, index) => { + const prefixedPath = `${pathPrefix}${item.path || ''}`; + const staticIndexStart = location.pathname.indexOf('/index.html'); + const locationPathWithoutStaticIndex = + staticIndexStart !== -1 + ? location.pathname.substr(0, staticIndexStart) + : location.pathname; + const isActive = + stripTrailingSlash(`${routePrefix}${prefixedPath}`) === + stripTrailingSlash(locationPathWithoutStaticIndex); + const newPathPrefix = `${pathPrefix}${item.pathPrefix || ''}`; + + if (item.children && item.children.length) { + return ( + <NavItemGroup + key={index} + path={item.path ? prefixedPath : null} + title={item.title} + isActive={isActive} + > + {isActive ? <Helmet title={item.title} /> : null} + {this.buildChildren({ + children: item.children, + pathPrefix: newPathPrefix, + })} + </NavItemGroup> + ); + } + return ( + <NavItem key={index} path={prefixedPath} isActive={isActive}> + {isActive ? <Helmet title={item.title} /> : null} + {item.title} + </NavItem> + ); + }); + } + + render() { + const {data} = this.props; + return ( + <SideNavContainer> + <SideNavUl>{this.buildChildren(data)}</SideNavUl> + </SideNavContainer> + ); + } +} + +export default SideNav; diff --git a/src/components/style-settings.js b/src/components/style-settings.js new file mode 100644 index 0000000..5f5d8a3 --- /dev/null +++ b/src/components/style-settings.js @@ -0,0 +1,13 @@ +export default { + primaryColor: '#4db5d9', // #277dd1 or #4db5d9 ? + primary10Color: '#f0f8fa', + whiteColor: '#fff', + white10Color: '#f8f8f9', + white40Color: '#f0f0ef', + white60Color: '#E5E5E4', + white120Color: '#999', + blackColor: '#000', + black90Color: '#041725', + fontFamily: `ff-clan-web-pro, "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, + Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", "sans-serif"`, +}; diff --git a/src/components/styled-elements.js b/src/components/styled-elements.js new file mode 100644 index 0000000..c1a80d4 --- /dev/null +++ b/src/components/styled-elements.js @@ -0,0 +1,91 @@ +const React = require('react'); +const {styled} = require('styletron-react'); +const { + primaryColor, + whiteColor, + blackColor, + black90Color, + white120Color, + fontFamily, +} = require('./style-settings'); + +exports.MainNavContainer = styled('div', {}); + +exports.MainNavItem = styled('span', ({styleProps = {}}) => ({ + display: 'inline-block', + fontFamily: fontFamily, + fontWeight: 'bold', + fontSize: '11px', + letterSpacing: '2px', + textTransform: 'uppercase', + textDecoration: 'none', + lineHeight: '1', + padding: '17px 15px 19px 15px', + borderTopWidth: '4px', + borderTopStyle: 'solid', + color: styleProps.isActive ? whiteColor : white120Color, + borderTopColor: styleProps.isActive ? primaryColor : 'transparent', + ':hover': { + color: whiteColor, + }, + '@media (max-width: 380px)': { + padding: '17px 8px 19px 8px', + }, + '@media (max-width: 348px)': { + padding: '17px 4px 19px 4px', + letterSpacing: '1px', + }, + ...styleProps.overrides, +})); + +exports.SideNavContainer = styled('div', ({styleProps = {}}) => ({ + position: 'relative', + width: '310px', + paddingRight: '24px', + paddingTop: '24px', + paddingBottom: '28px', + listStyle: 'none', + '@media (max-width: 374px)': { + ':hover': { + width: '100%', + boxSizing: 'border-box', + }, + }, + ...styleProps.overrides, +})); + +exports.SideNavUl = styled('ul', ({styleProps = {}}) => ({ + margin: '0', + listStyle: 'none', + ...styleProps.overrides, +})); + +exports.SideNavLi = styled('li', ({styleProps = {}}) => ({ + margin: '0', + ...styleProps.overrides, +})); + +exports.SideNavItem = styled('span', ({styleProps = {}}) => ({ + display: 'inline-block', + padding: '5px 11px', + fontSize: '16px', + fontWeight: styleProps.isActive ? 'bold' : '400', + borderLeft: '4px solid transparent', + color: styleProps.isActive ? blackColor : white120Color, + ':hover': styleProps.noLink + ? {} + : { + color: styleProps.isActive ? blackColor : primaryColor, + fontWeight: 'bold', + }, + ':before': { + content: '""', + display: 'inline-block', + position: 'absolute', + left: '0', + height: '28px', + borderLeft: '4px solid transparent', + borderLeftColor: styleProps.isActive ? primaryColor : 'transparent', + }, + ...styleProps.overrides, +})); diff --git a/src/components/uber-logo.js b/src/components/uber-logo.js new file mode 100644 index 0000000..f7fabbc --- /dev/null +++ b/src/components/uber-logo.js @@ -0,0 +1,57 @@ +import React from 'react'; +import {styled} from 'styletron-react'; + +const Svg = styled('svg', { + '@media (max-width: 562px)': { + display: 'none', + }, +}); + +const UberLogo = () => { + return ( + <Svg viewBox="0 0 80 17" height="16px"> + <g> + <title>Uber logo + + + + + + + + + ); +}; + +export default UberLogo; diff --git a/src/css/normalize.css b/src/css/normalize.css new file mode 100644 index 0000000..17aad45 --- /dev/null +++ b/src/css/normalize.css @@ -0,0 +1,332 @@ +/* Normalize */ +html { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +footer, +header, +nav, +section { + display: block; +} +h1 { + font-size: 2em; + margin: 0.67em 0; +} +figcaption, +figure, +main { + display: block; +} +figure { + margin: 1em 40px; +} +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} +pre { + font-family: monospace, monospace; + font-size: 1em; +} +abbr[title] { + border-bottom: none; + text-decoration: underline; + text-decoration: underline dotted; +} +b, +strong { + font-weight: inherit; +} +b, +strong { + font-weight: bolder; +} +code, +kbd, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +dfn { + font-style: italic; +} +mark { + background-color: #ff0; + color: #000; +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sub { + bottom: -0.25em; +} +sup { + top: -0.5em; +} +audio, +video { + display: inline-block; +} +audio:not([controls]) { + display: none; + height: 0; +} +img { + border-style: none; +} +svg:not(:root) { + overflow: hidden; +} +button, +input, +optgroup, +select, +textarea { + font-family: sans-serif; + font-size: 100%; + line-height: 1.15; + margin: 0; +} +button, +input { + overflow: visible; +} +button, +select { + text-transform: none; +} +button, +html [type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; +} +button::-moz-focus-inner, +[type='button']::-moz-focus-inner, +[type='reset']::-moz-focus-inner, +[type='submit']::-moz-focus-inner { + border-style: none; + padding: 0; +} +button:-moz-focusring, +[type='button']:-moz-focusring, +[type='reset']:-moz-focusring, +[type='submit']:-moz-focusring { + outline: 1px dotted ButtonText; +} +fieldset { + padding: 0.35em 0.75em 0.625em; +} +legend { + box-sizing: border-box; + color: inherit; + display: table; + max-width: 100%; + padding: 0; /* 3 */ + white-space: normal; +} +progress { + display: inline-block; + vertical-align: baseline; +} +textarea { + overflow: auto; +} +[type='checkbox'], +[type='radio'] { + box-sizing: border-box; + padding: 0; +} +[type='number']::-webkit-inner-spin-button, +[type='number']::-webkit-outer-spin-button { + height: auto; +} +[type='search'] { + -webkit-appearance: textfield; + outline-offset: -2px; +} +[type='search']::-webkit-search-cancel-button, +[type='search']::-webkit-search-decoration { + -webkit-appearance: none; +} +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} +details, +menu { + display: block; +} +summary { + display: list-item; +} +canvas { + display: inline-block; +} +template { + display: none; +} +[hidden] { + display: none; +} + +/* Style + ========================================================================== */ +html { + box-sizing: border-box; +} +* { + box-sizing: inherit; +} +*:before { + box-sizing: inherit; +} +*:after { + box-sizing: inherit; +} +body { + color: rgba(0, 0, 0, 0.8); + font-family: ff-clan-web-pro, '-apple-system', BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'sans-serif'; + font-weight: 400; + line-height: 1.7; + word-wrap: break-word; + font-kerning: normal; + -moz-font-feature-settings: 'kern', 'liga', 'clig', 'calt'; + -ms-font-feature-settings: 'kern', 'liga', 'clig', 'calt'; + -webkit-font-feature-settings: 'kern', 'liga', 'clig', 'calt'; + font-feature-settings: 'kern', 'liga', 'clig', 'calt'; +} +pre { + margin-left: 0; + margin-right: 0; + margin-top: 0; + padding-bottom: 0; + padding-left: 0; + padding-right: 0; + padding-top: 0; + margin-bottom: 1.45rem; + font-size: 0.85rem; + line-height: 1.42; + background: hsla(0, 0%, 0%, 0.04); + border-radius: 3px; + overflow: auto; + word-wrap: normal; + padding: 1.45rem; +} +code { + font-family: 'SFMono-Regular', Consolas, 'Roboto Mono', 'Droid Sans Mono', + 'Liberation Mono', Menlo, Courier, monospace; + font-size: 0.85rem; + line-height: 1.45rem; + background-color: hsla(0, 0%, 0%, 0.04); + border-radius: 3px; + padding: 0; + padding-top: 0.2em; + padding-bottom: 0.2em; +} +pre code { + background: none; + line-height: 1.42; +} +code:before, +code:after, +tt:before, +tt:after { + letter-spacing: -0.2em; + content: ' '; +} +pre code:before, +pre code:after, +pre tt:before, +pre tt:after { + content: ''; +} +ul { + margin-left: 1.45rem; + margin-right: 0; + margin-top: 0; + padding-bottom: 0; + padding-left: 0; + padding-right: 0; + padding-top: 0; + margin-bottom: 1.45rem; + list-style-position: outside; + list-style-image: none; +} +li { + margin-bottom: calc(1.45rem / 2); +} +ol li { + padding-left: 0; +} +ul li { + padding-left: 0; +} +li > ol { + margin-left: 1.45rem; + margin-bottom: calc(1.45rem / 2); + margin-top: calc(1.45rem / 2); +} +li > ul { + margin-left: 1.45rem; + margin-bottom: calc(1.45rem / 2); + margin-top: calc(1.45rem / 2); +} +li *:last-child { + margin-bottom: 0; +} +p *:last-child { + margin-bottom: 0; +} +li > p { + margin-bottom: calc(1.45rem / 2); +} +hr { + border: 0; + border-top: 1px solid #e5e5e4; +} +blockquote { + background: #F0F8FA; + margin-left: 0; + margin-right: 0; + padding: 16px 16px; + border-left-color: #4db5d9; + border-left-width: 6px; + border-left-style: solid; +} +blockquote *:first-child { + margin-top: 0; +} +blockquote *:last-child { + margin-bottom: 0; +} +table { + border-collapse: collapse; + width: 100%; + background: #f0f8fa; +} +thead { + text-align: left; +} +th, td { + border: 1px solid #e5e5e4; + font-size: 14px; + line-height: 1.3em; + padding: 10px; + text-align: left; +} diff --git a/src/css/prism.css b/src/css/prism.css new file mode 100644 index 0000000..0787800 --- /dev/null +++ b/src/css/prism.css @@ -0,0 +1,167 @@ +/* +Name: Duotone Light +Author: Simurai, adapted from DuoTone themes for Atom (http://simurai.com/projects/2016/01/01/duotone-themes) +Conversion: Bram de Haan (http://atelierbram.github.io/Base2Tone-prism/output/prism/prism-base2tone-morning-light.css) +Generated with Base16 Builder (https://github.com/base16-builder/base16-builder) +*/ + +code[class*="language-"], +pre[class*="language-"] { + font-family: Consolas, Menlo, Monaco, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", "Courier New", Courier, monospace; + font-size: 14px; + line-height: 1.375; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; + background: #faf8f5; + color: #728fcb; +} + +pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { + text-shadow: none; + background: #faf8f5; +} + +pre[class*="language-"]::selection, pre[class*="language-"] ::selection, +code[class*="language-"]::selection, code[class*="language-"] ::selection { + text-shadow: none; + background: #faf8f5; +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #b6ad9a; +} + +.token.punctuation { + color: #b6ad9a; +} + +.token.namespace { + opacity: .7; +} + +.token.tag, +.token.operator, +.token.number { + color: #063289; +} + +.token.property, +.token.function { + color: #b29762; +} + +.token.tag-id, +.token.selector, +.token.atrule-id { + color: #2d2006; +} + +code.language-javascript, +.token.attr-name { + color: #896724; +} + +code.language-css, +code.language-scss, +.token.boolean, +.token.string, +.token.entity, +.token.url, +.language-css .token.string, +.language-scss .token.string, +.style .token.string, +.token.attr-value, +.token.keyword, +.token.control, +.token.directive, +.token.unit, +.token.statement, +.token.regex, +.token.atrule { + color: #728fcb; +} + +.token.placeholder, +.token.variable { + color: #93abdc; +} + +.token.deleted { + text-decoration: line-through; +} + +.token.inserted { + border-bottom: 1px dotted #2d2006; + text-decoration: none; +} + +.token.italic { + font-style: italic; +} + +.token.important, +.token.bold { + font-weight: bold; +} + +.token.important { + color: #896724; +} + +.token.entity { + cursor: help; +} + +pre > code.highlight { + outline: .4em solid #896724; + outline-offset: .4em; +} + +/* overrides color-values for the Line Numbers plugin + * http://prismjs.com/plugins/line-numbers/ + */ +.line-numbers .line-numbers-rows { + border-right-color: #ece8de; +} + +.line-numbers-rows > span:before { + color: #cdc4b1; +} + +/* overrides color-values for the Line Highlight plugin + * http://prismjs.com/plugins/line-highlight/ + */ +.line-highlight { + background: rgba(45, 32, 6, 0.2); + background: -webkit-linear-gradient(left, rgba(45, 32, 6, 0.2) 70%, rgba(45, 32, 6, 0)); + background: linear-gradient(to right, rgba(45, 32, 6, 0.2) 70%, rgba(45, 32, 6, 0)); +} \ No newline at end of file diff --git a/src/html.js b/src/html.js new file mode 100644 index 0000000..144015c --- /dev/null +++ b/src/html.js @@ -0,0 +1,73 @@ +import React from 'react'; +import {primaryColor, primary10Color} from './components/style-settings'; +import {withPrefix} from 'gatsby-link'; + +let stylesStr; +if (process.env.NODE_ENV === `production`) { + try { + stylesStr = require(`!raw-loader!../public/styles.css`); + } catch (e) { + console.log(e); + } +} + +module.exports = class HTML extends React.Component { + render() { + let css; + if (process.env.NODE_ENV === `production`) { + css = ( + + {css} + {this.props.headComponents} +