diff --git a/.editorconfig b/.editorconfig index 259cf3d46..959db8a94 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,5 +16,5 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.md] -trim_trailing_whitespace = false +# editorconfig-tools is unable to ignore longs strings or urls +max_line_length = null diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index a828fbb28..000000000 --- a/.eslintrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "parser": "babel-eslint", - "extends": "airbnb", - "globals": { - "__DEV__": true - }, - "env": { - "browser": true, - "jest": true - }, - "rules": { - "no-confusing-arrow": 0, - "react/jsx-quotes": 0, - "jsx-quotes": [2, "prefer-double"] - } -} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..3efc19186 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,105 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-present Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +// ESLint configuration +// http://eslint.org/docs/user-guide/configuring +module.exports = { + parser: 'babel-eslint', + + extends: [ + 'airbnb', + 'plugin:flowtype/recommended', + 'plugin:css-modules/recommended', + 'prettier', + 'prettier/flowtype', + 'prettier/react', + ], + + plugins: ['flowtype', 'css-modules', 'prettier', 'jest'], + + globals: { + __DEV__: true, + }, + + env: { + browser: true, + jest: true, + }, + + rules: { + // Forbid the use of extraneous packages + // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-extraneous-dependencies.md + 'import/no-extraneous-dependencies': ['error', { packageDir: '.' }], + + // Recommend not to leave any console.log in your code + // Use console.error, console.warn and console.info instead + // https://eslint.org/docs/rules/no-console + 'no-console': [ + 'error', + { + allow: ['warn', 'error', 'info'], + }, + ], + + // Prefer destructuring from arrays and objects + // http://eslint.org/docs/rules/prefer-destructuring + 'prefer-destructuring': [ + 'error', + { + VariableDeclarator: { + array: false, + object: true, + }, + AssignmentExpression: { + array: false, + object: false, + }, + }, + { + enforceForRenamedProperties: false, + }, + ], + + // Ensure tags are valid + // https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/anchor-is-valid.md + 'jsx-a11y/anchor-is-valid': [ + 'error', + { + components: ['Link'], + specialLink: ['to'], + aspects: ['noHref', 'invalidHref', 'preferButton'], + }, + ], + + // Allow .js files to use JSX syntax + // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-filename-extension.md + 'react/jsx-filename-extension': ['error', { extensions: ['.js', '.jsx'] }], + + // Functional and class components are equivalent from React’s point of view + // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/prefer-stateless-function.md + 'react/prefer-stateless-function': 'off', + + // ESLint plugin for prettier formatting + // https://github.com/prettier/eslint-plugin-prettier + 'prettier/prettier': 'error', + + 'react/forbid-prop-types': 'off', + 'react/destructuring-assignment': 'off', + }, + + settings: { + // Allow absolute paths in imports, e.g. import Button from 'components/Button' + // https://github.com/benmosher/eslint-plugin-import/tree/master/resolvers + 'import/resolver': { + node: { + moduleDirectory: ['node_modules', 'src'], + }, + }, + }, +}; diff --git a/.flowconfig b/.flowconfig index fadd349b1..f111649a9 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,7 +1,11 @@ [ignore] .*/build -.*/config +.*/docs .*/node_modules -.*/gulpfile.js +.*/public [include] + +[options] +module.system.node.resolve_dirname=node_modules +module.system.node.resolve_dirname=src diff --git a/.gitattributes b/.gitattributes index 1d4170456..ae3b4c513 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,14 +7,19 @@ # (this is required in order to prevent newline related issues like, # for example, after the build script is run) .* text eol=lf -*.css text eol=lf *.html text eol=lf -*.jade text eol=lf -*.js text eol=lf -*.json text eol=lf +*.css text eol=lf *.less text eol=lf +*.styl text eol=lf *.scss text eol=lf +*.sass text eol=lf +*.sss text eol=lf +*.js text eol=lf +*.jsx text eol=lf +*.json text eol=lf *.md text eol=lf +*.mjs text eol=lf *.sh text eol=lf +*.svg text eol=lf *.txt text eol=lf *.xml text eol=lf diff --git a/.gitignore b/.gitignore index a03526900..1a0fe2e26 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,29 @@ -# Include your project-specific ignores in this file -# Read about how to use .gitignore: https://help.github.com/articles/ignoring-files +# See https://help.github.com/ignore-files/ for more about ignoring files. +# Dependencies +node_modules/ + +# Compiled output build -node_modules -ncp-debug.log -npm-debug.log + +# Runtime data +database.sqlite + +# Test coverage +coverage + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editors and IDEs +.idea +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# Misc +.DS_Store diff --git a/.jscsrc b/.jscsrc deleted file mode 100644 index bad58619f..000000000 --- a/.jscsrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "preset": "airbnb", - "excludeFiles": ["build/**", "node_modules/**"], - "validateQuoteMarks": null, - "disallowSpacesInsideTemplateStringPlaceholders": null -} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..ec6d3cdd7 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +package.json diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000..227072ef1 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,16 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-present Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +// Prettier configuration +// https://prettier.io/docs/en/configuration.html +module.exports = { + printWidth: 80, + singleQuote: true, + trailingComma: 'all', +}; diff --git a/.stylelintrc.js b/.stylelintrc.js new file mode 100644 index 000000000..3477d9a3d --- /dev/null +++ b/.stylelintrc.js @@ -0,0 +1,62 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-present Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +// stylelint configuration +// https://stylelint.io/user-guide/configuration/ +module.exports = { + // The standard config based on a handful of CSS style guides + // https://github.com/stylelint/stylelint-config-standard + extends: 'stylelint-config-standard', + + plugins: [ + // stylelint plugin to sort CSS rules content with specified order + // https://github.com/hudochenkov/stylelint-order + 'stylelint-order', + ], + + rules: { + 'property-no-unknown': [ + true, + { + ignoreProperties: [ + // CSS Modules composition + // https://github.com/css-modules/css-modules#composition + 'composes', + ], + }, + ], + + 'selector-pseudo-class-no-unknown': [ + true, + { + ignorePseudoClasses: [ + // CSS Modules :global scope + // https://github.com/css-modules/css-modules#exceptions + 'global', + 'local', + ], + }, + ], + + // Opinionated rule, you can disable it if you want + 'string-quotes': 'single', + + // https://github.com/hudochenkov/stylelint-order/blob/master/rules/order/README.md + 'order/order': [ + 'custom-properties', + 'dollar-variables', + 'declarations', + 'at-rules', + 'rules', + ], + + // https://github.com/hudochenkov/stylelint-order/blob/master/rules/properties-order/README.md + 'order/properties-order': [], + }, +}; diff --git a/.travis.yml b/.travis.yml index 09cbb6d21..6105d4a58 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,9 @@ -sudo: false language: node_js node_js: - - '5' + - 'stable' + - '12' + - '10' + - '8' env: - CXX=g++-4.8 addons: @@ -10,3 +12,8 @@ addons: - ubuntu-toolchain-r-test packages: - g++-4.8 +cache: yarn +script: + - yarn lint + - yarn test + - yarn build --release diff --git a/CHANGELOG.md b/CHANGELOG.md index c71c6d3a4..d6803765d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,16 +4,39 @@ All notable changes to this project will be documented in this file. ### [Unreleased][unreleased] +- Split the `App` component into `App` setting context variables and `Layout` setting general look and feel of the app (BREAKING CHANGE) +- Upgrade `history` npm module to v4.x, update `Link` component (BREAKING CHANGE) +- Remove `core/createHistory.js` in favor of initializing a new history instance inside `server.js` and `client.js` (BREAKING CHANGE) +- Remove Jade dependency in favor of React-based templates: `src/views/index.jade => src/components/Html` + (BREAKING CHANGE) [#711](https://github.com/kriasoft/react-starter-kit/pull/711) +- Update `isomorphic-style-loader` to `v1.0.0`, it adds comparability with ES2015+ decorators. + Code such as `export default withStyles(MyComponent, style1, style2)` must be replaced with + `export default withStyles(style1, style2)(MyComponent)` (BREAKING CHANGE). +- Replace Jest with Mocha, Chai, Sinon. Unit test files must be renamed from + `MyComponent/__test__/MyComponent-test.js` to `MyComponent/MyComponent.test.js` (BREAKING CHANGE). +- Remove `actions`, `stores` folders since there is no Flux library included into the kit +- Rename `server` variable in `server.js` to `app` +- Integrate [Sequelize](http://docs.sequelizejs.com/) to make the project compatible with different types of databases +- Rename `onSetTitle`, `onSetMeta` context variables to `setTitle`, `setMeta` +- Move `Content` component to `src/routes/content` +- Move `ErrorPage` component to `src/routes/error` +- Move the list of top-level routes to `src/routes/index` +- Update routing to use `universal-router` library +- Move Babel, ESLint and JSCS configurations to `package.json` [#497](https://github.com/kriasoft/react-starter-kit/pull/497) - Convert `Feedback`, `Footer`, `Header`, and `Navigation` to functional stateless components -- Move page / screen components into the `src/routes` folder along with the routing information for them [BREAKING CHANGE]. [6553936](https://github.com/kriasoft/react-starter-kit/commit/6553936e693e24a8ac6178f4962af15e0ea87dfd) +- Move page / screen components into the `src/routes` folder along with the routing information for them (BREAKING CHANGE). [6553936](https://github.com/kriasoft/react-starter-kit/commit/6553936e693e24a8ac6178f4962af15e0ea87dfd) -### [v0.5.1] - 2016-03-02 +### [v0.5.1] -- Remove `Html` React component in favor of compiled Jade templates (`src/views`) [BREAKING CHANGE]. [e188388](https://github.com/kriasoft/react-starter-kit/commit/e188388f87069cdc7d501b385d6b0e46c98fed60) +> 2016-03-02 + +- Remove `Html` React component in favor of compiled Jade templates (`src/views`) (BREAKING CHANGE). [e188388](https://github.com/kriasoft/react-starter-kit/commit/e188388f87069cdc7d501b385d6b0e46c98fed60) - Add global error handling in Node.js/Express app. [e188388](https://github.com/kriasoft/react-starter-kit/commit/e188388f87069cdc7d501b385d6b0e46c98fed60) - Add support for Markdown and HTML for static pages. [#469](https://github.com/kriasoft/react-starter-kit/pull/469), [#477](https://github.com/kriasoft/react-starter-kit/pull/477) -### [v0.5.0] - 2016-02-27 +### [v0.5.0] + +> 2016-02-27 - Replace RESTful API endpoint (`src/api`) with GraphQL (`src/data`) - Add a sample GraphQL endpoint [localhost:3000/graphql](https://localhost:3000/graphql) @@ -32,9 +55,11 @@ All notable changes to this project will be documented in this file. - Add support of `--release` and `--verbose` flags to build scripts - Add `CHANGELOG.md` file with a list of notable changes to this project -### [v0.4.1] - 2015-10-04 +### [v0.4.1] + +> 2015-10-04 -- Replace React Hot Loader (depricated) with React Transform +- Replace React Hot Loader (deprecated) with React Transform - Replace `index.html` template with `Html` (shell) React component - Update the deployment script (`tools/deploy.js`), add Git-based deployment example - Update ESLint and JSCS settings to use AirBnb JavaScript style guide diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 701723713..24e66a167 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ React Starter Kit is currently the most widely adopted Node.js/React boilerplate used by many tech startups around the globe. We're working hard to keep it up to date, making sure that it follows best practices and high coding standards, paying extremely close attention to details. - + Your contributions are welcome and are greatly appreciated! Every little bit helps, and credit will always be given. @@ -21,7 +21,7 @@ Feedback is the breakfast for champions! We'd love to hear your opinions, discus improvements, architecture, theory, internal implementation, etc. Please, join or start a new conversation in our [issue tracker](https://github.com/kriasoft/react-starter-kit/issues), [Gitter](https://gitter.im/kriasoft/react-starter-kit) chat room, or let's talk face-to-face on -[Appear.in](https://appear.in/react) or [Skype](http://hatscripts.com/addskype?koistya). +[Appear.in](https://appear.in/react) or [Skype](http://hatscripts.com/addskype?koistya). ### Documentation @@ -33,10 +33,10 @@ files right from GitHub website as described [here](https://help.github.com/arti Before opening an issue, please: -* Check the [Getting Started](https://github.com/kriasoft/react-starter-kit/blob/master/docs/getting-started.md) guide. -* Search the [issue tracker](https://github.com/kriasoft/react-starter-kit/issues) to make sure +- Check the [Getting Started](https://github.com/kriasoft/react-starter-kit/blob/master/docs/getting-started.md) guide. +- Search the [issue tracker](https://github.com/kriasoft/react-starter-kit/issues) to make sure your issue hasn’t already been reported. -* If your issue sounds more like a question, please post it on StackOverflow.com instead with the +- If your issue sounds more like a question, please post it on StackOverflow.com instead with the tag [react-starter-kit](http://stackoverflow.com/questions/tagged/react-starter-kit). ### Pull Requests @@ -44,16 +44,17 @@ Before opening an issue, please: Before you submit a [pull request](https://help.github.com/articles/using-pull-requests/) from your forked repo, check that it meets these guidelines: -* If the pull request adds functionality, the docs should be updated as part of the same PR. -* Create a separate PR for each small feature or bug fix. -* [Squash](http://stackoverflow.com/questions/5189560/squash-my-last-x-commits-together-using-git) +- If the pull request adds functionality, the docs should be updated as part of the same PR. +- Create a separate PR for each small feature or bug fix. +- [Squash](http://stackoverflow.com/questions/5189560/squash-my-last-x-commits-together-using-git) your commits into one for each PR. -* Run `npm test` to make sure that your code style is OK and there are no any regression bugs. +- Run `yarn test` to make sure that your code style is OK and there are no any regression bugs. +- When contributing to an opt-in feature, apply the `[feature/...]` tag as a prefix to your PR title #### Style Guide Our linter will catch most styling issues that may exist in your code. You can check the status -of your code styling by simply running: `npm run lint` +of your code styling by simply running: `yarn lint` However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb's Style Guide](https://github.com/airbnb/javascript) will guide you diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..d8f277125 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM node:8.16.2-alpine + +# Set a working directory +WORKDIR /usr/src/app + +COPY ./build/package.json . +COPY ./build/yarn.lock . + +# Install Node.js dependencies +RUN yarn install --production --no-progress + +# Copy application files +COPY ./build . + +# Set permissions for "node" user +RUN chown -R node:node /usr/src/app +RUN chmod 755 /usr/src/app + +# Run the container under "node" user by default +USER node + +# Set NODE_ENV env variable to "production" for faster expressjs +ENV NODE_ENV production + +CMD [ "node", "server.js" ] diff --git a/LICENSE.txt b/LICENSE.txt index 5f421b42f..2e4e7b16b 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ - The MIT License +The MIT License -Copyright (c) 2014-2016 Konstantin Tarkus, KriaSoft LLC. +Copyright (c) 2014-present Konstantin Tarkus, Kriasoft LLC. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8372aca17..7e33bc201 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,228 @@ -## React Starter Kit — "isomorphic" web app boilerplate - -[![Support us on Bountysource](https://dl.dropboxusercontent.com/u/16006521/react-starter-kit/banner.png)](https://salt.bountysource.com/teams/react-starter-kit)
- -> [React Starter Kit](https://www.reactstarterkit.com) is an opinionated -> boilerplate for web development built on top of Facebook's -> [React](https://facebook.github.io/react/) library, -> [Node.js](https://nodejs.org/) / [Express](http://expressjs.com/) server -> and [Flux](http://facebook.github.io/flux/) architecture. Containing -> modern web development tools such as [Webpack](http://webpack.github.io/), -> [Babel](http://babeljs.io/) and [BrowserSync](http://www.browsersync.io/). -> Helping you to stay productive following the best practices. A solid starting -> point for both professionals and newcomers to the industry. - -See [demo](http://demo.reactstarterkit.com)  |  -[docs](https://github.com/kriasoft/react-starter-kit/tree/master/docs)  |  -[to-do list](https://waffle.io/kriasoft/react-starter-kit)  |  -join [#react-starter-kit](https://gitter.im/kriasoft/react-starter-kit) chatroom to stay up to date  |  -visit our sponsors: - -[![Rollbar - Full-stack error tracking for all apps in any language](https://dl.dropboxusercontent.com/u/16006521/react-starter-kit/rollbar.png)](https://rollbar.com/?utm_source=reactstartkit(github)&utm_medium=link&utm_campaign=reactstartkit(github))    -[![Localize - Translate your web app in minutes](https://dl.dropboxusercontent.com/u/16006521/react-starter-kit/localize.png)](https://localizejs.com/?cid=802&utm_source=rsk) +## React Starter Kit — "[isomorphic](http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/)" web app boilerplate  
+ +[React Starter Kit](https://www.reactstarterkit.com) is an opinionated boilerplate for web +development built on top of [Node.js](https://nodejs.org/), +[Express](http://expressjs.com/), [GraphQL](http://graphql.org/) and +[React](https://facebook.github.io/react/), containing modern web development +tools such as [Webpack](http://webpack.github.io/), [Babel](http://babeljs.io/) +and [Browsersync](http://www.browsersync.io/). Helping you to stay productive +following the best practices. A solid starting point for both professionals +and newcomers to the industry. + +**See** [getting started guide](./docs/getting-started.md), [demo][demo], +[docs](https://github.com/kriasoft/react-starter-kit/tree/master/docs), +[roadmap](https://github.com/kriasoft/react-starter-kit/projects/1)  |  +**Join** [#react-starter-kit][chat] chat room on Gitter  |  +**Visit our sponsors**:

+ +

+ + + Hiring +

### Getting Started - * Follow the [getting started guide](./docs/getting-started.md) to download and run the project - * Check the [code recipes](./docs/recipes) used in this boilerplate, or share yours - -### Directory Layout - -``` -. -├── /build/ # The folder for compiled output -├── /docs/ # Documentation files for the project -├── /node_modules/ # 3rd-party libraries and utilities -├── /src/ # The source code of the application -│ ├── /actions/ # Action creators that allow to trigger a dispatch to stores -│ ├── /components/ # React components -│ ├── /constants/ # Constants (action types etc.) -│ ├── /content/ # Static content (plain HTML or Markdown, Jade, you name it) -│ ├── /core/ # Core framework and utility functions -│ ├── /data/ # GraphQL server schema -│ ├── /decorators/ # Higher-order React components -│ ├── /public/ # Static files which are copied into the /build/public folder -│ ├── /routes/ # Page/screen components along with the routing information -│ ├── /stores/ # Stores contain the application state and logic -│ ├── /views/ # Express.js views for index and error pages -│ ├── /client.js # Client-side startup script -│ ├── /config.js # Global application settings -│ ├── /routes.js # Universal (isomorphic) application routes -│ └── /server.js # Server-side startup script -├── /tools/ # Build automation scripts and utilities -│ ├── /lib/ # Library for utility snippets -│ ├── /build.js # Builds the project from source to output (build) folder -│ ├── /bundle.js # Bundles the web resources into package(s) through Webpack -│ ├── /clean.js # Cleans up the output (build) folder -│ ├── /copy.js # Copies static files to output (build) folder -│ ├── /deploy.js # Deploys your web application -│ ├── /run.js # Helper function for running build automation tasks -│ ├── /runServer.js # Launches (or restarts) Node.js server -│ ├── /start.js # Launches the development web server with "live reload" -│ └── /webpack.config.js # Configurations for client-side and server-side bundles -└── package.json # The list of 3rd party libraries and utilities -``` +- Follow the [getting started guide](./docs/getting-started.md) to download and run the project + ([Node.js](https://nodejs.org/) >= 8.16.2) +- Check the [code recipes](./docs/recipes) used in this boilerplate, or share yours + +### Customization + +The `master` branch of React Starter Kit doesn't include a Flux implementation or any other +advanced integrations. Nevertheless, we have some integrations available to you in _feature_ +branches that you can use either as a reference or merge into your project: + +- [feature/redux](https://github.com/kriasoft/react-starter-kit/tree/feature/redux) ([PR](https://github.com/kriasoft/react-starter-kit/pull/1084)) + — isomorphic Redux by [Pavel Lang](https://github.com/langpavel) + (see [how to integrate Redux](./docs/recipes/how-to-integrate-redux.md)) (based on `master`) +- [feature/apollo](https://github.com/kriasoft/react-starter-kit/tree/feature/apollo) ([PR](https://github.com/kriasoft/react-starter-kit/pull/1147)) + — isomorphic Apollo Client by [Pavel Lang](https://github.com/langpavel) + (see [Tracking PR #1147](https://github.com/kriasoft/react-starter-kit/pull/1147)) (based on `feature/redux`) +- [feature/react-intl](https://github.com/kriasoft/react-starter-kit/tree/feature/react-intl) ([PR](https://github.com/kriasoft/react-starter-kit/pull/1135)) + — isomorphic Redux and React Intl by [Pavel Lang](https://github.com/langpavel) + (see [how to integrate React Intl](./docs/recipes/how-to-integrate-react-intl.md)) (based on `feature/apollo`) +- [feature/apollo-pure](https://github.com/kriasoft/react-starter-kit/tree/feature/apollo-pure) ([PR](https://github.com/kriasoft/react-starter-kit/pull/1666)) + — Apollo devtools and TypeScript integration by [piglovesyou](https://github.com/piglovesyou) (based on `master`) + +You can see status of most reasonable merge combination as [PRs labeled as `TRACKING`](https://github.com/kriasoft/react-starter-kit/labels/TRACKING) + +If you think that any of these features should be on `master`, or vice versa, some features should +removed from the `master` branch, please [let us know](https://gitter.im/kriasoft/react-starter-kit). +We love your feedback! + +### Comparison + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  +

React Starter Kit

+ + +
+

React Static Boilerplate

+ + +
+

ASP.NET Core Starter Kit

+ + +
App typeIsomorphic (universal)Single-page applicationSingle-page application
Frontend
LanguageJavaScript (ES2015+, JSX)JavaScript (ES2015+, JSX)JavaScript (ES2015+, JSX)
Libraries + React, + History, + Universal Router + + React, + History, + Redux + + React, + History, + Redux +
RoutesImperative (functional)DeclarativeDeclarative, cross-stack
Backend
LanguageJavaScript (ES2015+, JSX)n/aC#, F#
Libraries + Node.js, + Express, + Sequelize,
+ GraphQL
n/a + ASP.NET Core, + EF Core,
+ ASP.NET Identity +
SSRYesn/an/a
Data APIGraphQLn/aWeb API
+ +### Backers + +♥ React Starter Kit? Help us keep it alive by donating funds to cover project +expenses via [OpenCollective](https://opencollective.com/react-starter-kit) or +[Bountysource](https://salt.bountysource.com/teams/react-starter-kit)! + + + lehneres + + + Tarkan Anlar + + + Morten Olsen + + + Adam + + + David Ernst + + + Zane Hitchcox + + + + + +### How to Contribute + +Anyone and everyone is welcome to [contribute](CONTRIBUTING.md) to this project. The best way to +start is by checking our [open issues](https://github.com/kriasoft/react-starter-kit/issues), +[submit a new issue](https://github.com/kriasoft/react-starter-kit/issues/new?labels=bug) or +[feature request](https://github.com/kriasoft/react-starter-kit/issues/new?labels=enhancement), +participate in discussions, upvote or downvote the issues you like or dislike, send [pull +requests](CONTRIBUTING.md#pull-requests). -### Related Projects +### Learn More - * [Membership Database](https://github.com/membership/membership.db) — SQL schema boilerplate for user accounts, profiles, roles, and auth claims - * [React Static Boilerplate](https://github.com/koistya/react-static-boilerplate) — Generates static websites from React components - * [Babel Starter Kit](https://github.com/kriasoft/babel-starter-kit) — Boilerplate for authoring JavaScript/React.js libraries +- [Getting Started with React.js](http://facebook.github.io/react/) +- [Getting Started with GraphQL and Relay](https://quip.com/oLxzA1gTsJsE) +- [React.js Questions on StackOverflow](http://stackoverflow.com/questions/tagged/reactjs) +- [React.js Discussion Board](https://discuss.reactjs.org/) +- [Flux Architecture for Building User Interfaces](http://facebook.github.io/flux/) +- [Enzyme — JavaScript Testing utilities for React](http://airbnb.io/enzyme/) +- [Flow — A static type checker for JavaScript](http://flowtype.org/) +- [The Future of React](https://github.com/reactjs/react-future) +- [Learn ES6](https://babeljs.io/docs/learn-es6/), [ES6 Features](https://github.com/lukehoban/es6features#readme) -### Learn More +### Related Projects - * [Getting Started with React.js](http://facebook.github.io/react/) - * [Getting Started with GraphQL and Relay](https://quip.com/oLxzA1gTsJsE) - * [React.js Questions on StackOverflow](http://stackoverflow.com/questions/tagged/reactjs) - * [React.js Discussion Board](https://discuss.reactjs.org/) - * [Flux Architecture for Building User Interfaces](http://facebook.github.io/flux/) - * [Jest - Painless Unit Testing](http://facebook.github.io/jest/) - * [Flow - A static type checker for JavaScript](http://flowtype.org/) - * [The Future of React](https://github.com/reactjs/react-future) - * [Learn ES6](https://babeljs.io/docs/learn-es6/), [ES6 Features](https://github.com/lukehoban/es6features#readme) +- [GraphQL Starter Kit](https://github.com/kriasoft/graphql-starter-kit) — Boilerplate for building data APIs with Node.js, JavaScript (via Babel) and GraphQL +- [Membership Database](https://github.com/membership/membership.db) — SQL schema boilerplate for user accounts, profiles, roles, and auth claims +- [Babel Starter Kit](https://github.com/kriasoft/babel-starter-kit) — Boilerplate for authoring JavaScript/React.js libraries ### Support - * [#react-starter-kit](http://stackoverflow.com/questions/tagged/react-starter-kit) on Stack Overflow — Questions and answers - * [#react-starter-kit](https://gitter.im/kriasoft/react-starter-kit) on Gitter — Watch announcements, share ideas and feedback - * [GitHub issues](https://github.com/kriasoft/react-starter-kit/issues), or [Scrum board](https://waffle.io/kriasoft/react-starter-kit) — File issues, send feature requests - * [appear.in/react](https://appear.in/react) — Open hours! Exchange ideas and experiences (React, GraphQL, Startups, etc.) - * [@koistya](https://twitter.com/koistya) on [Codementor](https://www.codementor.io/koistya), or [Skype](http://hatscripts.com/addskype?koistya) — Private consulting +- [#react-starter-kit](http://stackoverflow.com/questions/tagged/react-starter-kit) on Stack Overflow — Questions and answers +- [#react-starter-kit](https://gitter.im/kriasoft/react-starter-kit) on Gitter — Watch announcements, share ideas and feedback +- [GitHub issues](https://github.com/kriasoft/react-starter-kit/issues), or [Scrum board](https://waffle.io/kriasoft/react-starter-kit) — File issues, send feature requests +- [appear.in/react](https://appear.in/react) — Open hours! Exchange ideas and experiences (React, GraphQL, startups and pet projects) +- [@koistya](https://twitter.com/koistya) on [Codementor](https://www.codementor.io/koistya), or [Skype](http://hatscripts.com/addskype?koistya) — Private consulting ### License -Copyright © 2014-2016 Kriasoft, LLC. This source code is licensed under the MIT +Copyright © 2014-present Kriasoft, LLC. This source code is licensed under the MIT license found in the [LICENSE.txt](https://github.com/kriasoft/react-starter-kit/blob/master/LICENSE.txt) file. The documentation to the project is licensed under the [CC BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/) license. --- + Made with ♥ by Konstantin Tarkus ([@koistya](https://twitter.com/koistya)) and [contributors](https://github.com/kriasoft/react-starter-kit/graphs/contributors) + +[rsk]: https://www.reactstarterkit.com +[demo]: http://demo.reactstarterkit.com +[node]: https://nodejs.org +[chat]: https://gitter.im/kriasoft/react-starter-kit diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 000000000..fdbbc882a --- /dev/null +++ b/babel.config.js @@ -0,0 +1,30 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-present Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +// Babel configuration +// https://babeljs.io/docs/usage/api/ +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + }, + ], + '@babel/preset-flow', + '@babel/preset-react', + ], + plugins: [ + '@babel/plugin-proposal-class-properties', + '@babel/plugin-syntax-dynamic-import', + ], + ignore: ['node_modules', 'build'], +}; diff --git a/docs/README.md b/docs/README.md index a3c529cf7..b261f203b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,17 +2,22 @@ ### General -* [Getting Started](./getting-started.md) -* [React Style Guide](./react-style-guide.md) -* [How to configure text editors and IDEs](./how-to-configure-text-editors.md) -* [Data fetching with WHATWG Fetch](./data-fetching.md) +- [Getting Started](./getting-started.md) +- [React Style Guide](./react-style-guide.md) +- [How to configure text editors and IDEs](./how-to-configure-text-editors.md) +- [Data fetching with WHATWG Fetch](./data-fetching.md) +- [Testing your application](./testing-your-application.md) ### Questions -* [Which module bundler should I use?](https://github.com/kriasoft/react-starter-kit/issues/3) -* [Which Flux implementation should I use?](https://github.com/kriasoft/react-starter-kit/issues/22) +- [Which module bundler should I use?](https://github.com/kriasoft/react-starter-kit/issues/3) +- [Which Flux implementation should I use?](https://github.com/kriasoft/react-starter-kit/issues/22) ### Recipes -* [How to Implement Routing and Navigation](./recipes/how-to-implement-routing.md) -* [How to Integrate Disqus](./recipes/how-to-integrate-disqus.md) +- [How to Implement Routing and Navigation](./recipes/how-to-implement-routing.md) +- [How to Integrate Redux](./recipes/how-to-integrate-redux.md) +- [How to Integrate React Intl](./recipes/how-to-integrate-react-intl.md) +- [How to Integrate Disqus](./recipes/how-to-integrate-disqus.md) +- [How to Use Sass/SCSS](./recipes/how-to-use-sass.md) +- [How to Configure Facebook Login](./recipes/how-to-configure-facebook-login.md) diff --git a/docs/data-fetching.md b/docs/data-fetching.md index b7a3ffc6d..cdb0eb9e5 100644 --- a/docs/data-fetching.md +++ b/docs/data-fetching.md @@ -1,32 +1,71 @@ -## Data Fetching with WHATWG Fetch +## Data Fetching -There is isomorphic `core/fetch` module that can be used the same way in both -client-side and server-side code as follows: +At a bare minimum you may want to use [HTML5 Fetch API][fetch] as an HTTP client +utility for making Ajax request to the [data API server][nodeapi]. This API is +supported natively in all the major browsers except for IE (note, that Edge +browser does support Fetch). -```jsx -import fetch from '../core/fetch'; +**React Starter Kit** is pre-configured with [`whatwg-fetch`][wfetch] polyfill +for the browser environment and [`node-fetch`][nfetch] module for the +server-side environment (see [`src/createFetch.js`](../src/createFetch.js)), +allowing you to use the `fetch(url, options)` method universally in both the +client-side and server-side code bases. -export const path = '/products'; -export const action = async () => { - const response = await fetch('http://github.com/graphql?query={products{id,name}}'); - const data = await response.json(); - return ; -}; +In order to avoid the amount of boilerplate code needed when using the raw +`fetch(..)` function, a simple wrapper was created that provides a base URL of +the data API server, credentials (cookies), CORS etc. For example, in a browser +environment the base URL of the data API server might be an empty string, so +when you make an Ajax request to the `/graphql` endpoint it's being sent to the +same origin, and when the same code is executed on the server, during +server-side rendering, it fetches data from the `http://api:8080/graphql` +endpoint (`node-fetch` doesn't support relative URLs for obvious reasons). + +Because of these subtle differences of how the `fetch` method works internally, +it makes total sense to pass it as a `context` variable to your React +application, so it can be used from either routing level or from inside your +React components as follows: + +#### Route Example + +```js +{ + path: '/posts/:id', + async action({ params, fetch }) { + const resp = await fetch(`/api/posts/${params.id}`, { method: 'GET' }); + const data = await resp.json(); + return { title: data.title, component: }; + } +} ``` -When this code executes on the client, the Ajax request will be sent via -GitHub's [fetch](https://github.com/github/fetch) library (`whatwg-fetch`), -that itself uses XHMLHttpRequest behind the scene unless `fetch` is supported -natively by the user's browser. +#### React Component -Whenever the same code executes on the server, it uses -[node-fetch](https://github.com/bitinn/node-fetch) module behind the scene that -itself sends an HTTP request via Node.js `http` module. It also converts -relative URLs to absolute (see `./core/fetch/fetch.server.js`). +```js +import {useContext} from 'react'; +import ApplicationContext from '../ApplicationContext'; -Both `whatwg-fetch` and `node-fetch` modules have almost identical API. If -you're new to this API, the following article may give you a good introduction: +function Post() { + const {context} = useContext(ApplicationContext); + return ( +
+ ... + { + event.preventDefault(); + const id = event.target.dataset['id']; + // Use context.fetch to make it work in both server-side and client-side + context.fetch(`/api/posts/${id}`, { method: 'DELETE' }).then(...); + }}>Delete +
+ ); +} +``` -https://jakearchibald.com/2015/thats-so-fetch/ +#### Related articles +- [That's so fetch!](https://jakearchibald.com/2015/thats-so-fetch/) by + [Jake Archibald](https://twitter.com/jaffathecake) +[fetch]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch +[wfetch]: https://github.com/github/fetchno +[nfetch]: https://github.com/bitinn/node-fetch +[nodeapi]: https://github.com/kriasoft/nodejs-api-starter diff --git a/docs/getting-started.md b/docs/getting-started.md index cdecea31c..a461a1d44 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -2,11 +2,52 @@ ### Requirements - * Mac OS X, Windows, or Linux - * [Node.js](https://nodejs.org/) v5.0 or newer - * `npm` v3.3 or newer (new to [npm](https://docs.npmjs.com/)?) - * `node-gyp` prerequisites mentioned [here](https://github.com/nodejs/node-gyp) - * Text editor or IDE pre-configured with React/JSX/Flow/ESlint ([learn more](./how-to-configure-text-editors.md)) +- Mac OS X, Windows, or Linux +- [Yarn](https://yarnpkg.com/) package + [Node.js](https://nodejs.org/) v8.16.2 or + newer +- Text editor or IDE pre-configured with React/JSX/Flow/ESlint + ([learn more](./how-to-configure-text-editors.md)) + +### Directory Layout + +Before you start, take a moment to see how the project structure looks like: + +``` +. +├── /build/ # The folder for compiled output +├── /docs/ # Documentation files for the project +├── /node_modules/ # 3rd-party libraries and utilities +├── /public/ # Static files which are copied into the /build/public folder +├── /src/ # The source code of the application +│ ├── /components/ # React components +│ ├── /data/ # GraphQL server schema and data models +│ ├── /routes/ # Page/screen components along with the routing information +│ ├── /client.js # Client-side startup script +│ ├── /config.js # Global application settings +│ ├── /server.js # Server-side startup script +│ └── ... # Other core framework modules +├── /test/ # Unit and end-to-end tests +├── /tools/ # Build automation scripts and utilities +│ ├── /lib/ # Library for utility snippets +│ ├── /build.js # Builds the project from source to output (build) folder +│ ├── /bundle.js # Bundles the web resources into package(s) through Webpack +│ ├── /clean.js # Cleans up the output (build) folder +│ ├── /copy.js # Copies static files to output (build) folder +│ ├── /deploy.js # Deploys your web application +│ ├── /postcss.config.js # Configuration for transforming styles with PostCSS plugins +│ ├── /run.js # Helper function for running build automation tasks +│ ├── /runServer.js # Launches (or restarts) Node.js server +│ ├── /start.js # Launches the development web server with "live reload" +│ └── /webpack.config.js # Configurations for client-side and server-side bundles +├── Dockerfile # Commands for building a Docker image for production +├── package.json # The list of 3rd party libraries and utilities +└── yarn.lock # Fixed versions of all the dependencies +``` + +**Note**: The current version of RSK does not contain a Flux implementation. It +can be easily integrated with any Flux library of your choice. The most commonly +used Flux libraries are [Flux](http://facebook.github.io/flux/), +[Redux](http://redux.js.org/) and [Relay](http://facebook.github.io/relay/). ### Quick Start @@ -16,62 +57,75 @@ You can start by cloning the latest version of React Starter Kit (RSK) on your local machine by running: ```shell -$ git clone -o react-starter-kit -b master --single-branch \ - https://github.com/kriasoft/react-starter-kit.git MyApp +$ git clone -o react-starter-kit -b master --single-branch https://github.com/kriasoft/react-starter-kit.git MyApp $ cd MyApp ``` Alternatively, you can start a new project based on RSK right from -[WebStorm IDE](https://www.jetbrains.com/webstorm/help/create-new-project-react-starter-kit.html), -or by using [Yeoman generator](https://www.npmjs.com/package/generator-react-fullstack). +[WebStorm IDE](https://www.jetbrains.com/help/webstorm/generating-a-project-from-a-framework-template.html#d88767e51), +or by using +[Yeoman generator](https://www.npmjs.com/package/generator-react-fullstack). -#### 2. Run `npm install` +#### 2. Run `yarn install` This will install both run-time project dependencies and developer tools listed in [package.json](../package.json) file. -#### 3. Run `npm start` +#### 3. Run `yarn start` This command will build the app from the source files (`/src`) into the output `/build` folder. As soon as the initial build completes, it will start the -Node.js server (`node build/server.js`) and [Browsersync](https://browsersync.io/) -with [HMR](https://webpack.github.io/docs/hot-module-replacement) on top of it. - -> [http://localhost:3000/](http://localhost:3000/) — Node.js server (`build/server.js`)
-> [http://localhost:3000/graphql](http://localhost:3000/graphql) — GraphQL server and IDE
-> [http://localhost:3001/](http://localhost:3001/) — BrowserSync proxy with HMR, React Hot Transform
-> [http://localhost:3002/](http://localhost:3002/) — BrowserSync control panel (UI) - -Now you can open your web app in a browser, on mobile devices and start -hacking. Whenever you modify any of the source files inside the `/src` folder, -the module bundler ([Webpack](http://webpack.github.io/)) will recompile the -app on the fly and refresh all the connected browsers. +Node.js server (`node build/server.js`) and +[Browsersync](https://browsersync.io/) with +[HMR](https://webpack.github.io/docs/hot-module-replacement) on top of it. + +> [http://localhost:3000/](http://localhost:3000/) — Node.js server +> (`build/server.js`) with Browsersync and HMR enabled\ +> [http://localhost:3000/graphql](http://localhost:3000/graphql) — GraphQL server +> and IDE\ +> [http://localhost:3001/](http://localhost:3001/) — Browsersync control panel +> (UI) + +Now you can open your web app in a browser, on mobile devices and start hacking. +Whenever you modify any of the source files inside the `/src` folder, the module +bundler ([Webpack](http://webpack.github.io/)) will recompile the app on the fly +and refresh all the connected browsers. ![browsersync](https://dl.dropboxusercontent.com/u/16006521/react-starter-kit/brwosersync.jpg) -Note that the `npm start` command launches the app in `development` mode, -the compiled output files are not optimized and minimized in this case. -You can use `--release` command line argument to check how your app works -in release (production) mode: +Note that the `yarn start` command launches the app in `development` mode, the +compiled output files are not optimized and minimized in this case. You can use +`--release` command line argument to check how your app works in release +(production) mode: ```shell -$ npm start -- --release +$ yarn start --release ``` +_NOTE: double dashes are required_ + ### How to Build, Test, Deploy If you need just to build the app (without running a dev server), simply run: ```shell -$ npm run build +$ yarn run build ``` or, for a production build: ```shell -$ npm run build -- --release +$ yarn run build --release ``` +or, for a production docker build: + +```shell +$ yarn run build --release --docker +``` + +_NOTE: double dashes are required_ + After running this command, the `/build` folder will contain the compiled version of the app. For example, you can launch Node.js server normally by running `node build/server.js`. @@ -79,41 +133,43 @@ running `node build/server.js`. To check the source code for syntax errors and potential issues run: ```shell -$ npm run lint +$ yarn run lint ``` To launch unit tests: ```shell -$ npm test +$ yarn run test # Run unit tests with Jest +$ yarn run test-watch # Launch unit test runner and start watching for changes ``` -Test any javascript module by creating a `__tests__/` directory where -the file is. Append `-test.js` to the filename and -[Jest](https://facebook.github.io/jest/) will do the rest. +By default, [Jest](https://jestjs.io/) test runner is looking for test files +matching the `src/**/*.test.js` pattern. Take a look at +`src/components/Layout/Layout.test.js` as an example. To deploy the app, run: ```shell -$ npm run deploy +$ yarn run deploy ``` The deployment script `tools/deploy.js` is configured to push the contents of the `/build` folder to a remote server via Git. You can easily deploy your app -to [Azure Web Apps](https://azure.microsoft.com/en-us/services/app-service/web/), -or [Heroku](https://www.heroku.com/) this way. Both will execute `npm install --production` -upon receiving new files from you. Note, you should only deploy the contents -of the `/build` folder to a remote server. +to +[Azure Web Apps](https://azure.microsoft.com/en-us/services/app-service/web/), +or [Heroku](https://www.heroku.com/) this way. Both will execute `yarn install --production` upon receiving new files from you. Note, you should only deploy +the contents of the `/build` folder to a remote server. ### How to Update If you need to keep your project up to date with the recent changes made to RSK, -you can always fetch and merge them from [this repo](https://github.com/kriasoft/react-starter-kit) -back into your own project by running: +you can always fetch and merge them from +[this repo](https://github.com/kriasoft/react-starter-kit) back into your own +project by running: ```shell $ git checkout master $ git fetch react-starter-kit $ git merge react-starter-kit/master -$ npm install +$ yarn install ``` diff --git a/docs/how-to-configure-text-editors.md b/docs/how-to-configure-text-editors.md index 38750cf43..786dc041d 100644 --- a/docs/how-to-configure-text-editors.md +++ b/docs/how-to-configure-text-editors.md @@ -7,32 +7,35 @@ Create a new project based on **React Starter Kit template** -![react-project-template-in-webstorm](https://dl.dropboxusercontent.com/u/16006521/react-starter-kit/webstorm-new-project.png) +[react-project-template-in-webstorm](https://plugins.jetbrains.com/plugin/7648-react-templates) -Make sure that **JSX** support is enabled in your project. This is set by default, if you create a new project based on React.js template. +Make sure that **JSX** support is enabled in your project. This is set by +default, if you create a new project based on React.js template. -![jsx-support-in-webstorm](https://dl.dropboxusercontent.com/u/16006521/react-starter-kit/webstorm-jsx.png) +[jsx-support-in-webstorm](https://blog.jetbrains.com/webstorm/2015/10/working-with-reactjs-in-webstorm-coding-assistance/) Configure JavaScript libraries for **auto-complete** -![javascript-libraries-in-webstorm](https://dl.dropboxusercontent.com/u/16006521/react-starter-kit/webstorm-libraries.png) +[javascript-libraries-in-webstorm](https://blog.jetbrains.com/webstorm/2017/08/how-to-configure-code-completion-in-full-stack-javascript-projects/) Enable **ESLint** support -![eslint-support-in-webstorm](https://dl.dropboxusercontent.com/u/16006521/react-starter-kit/webstorm-eslint.png) +[eslint-support-in-webstorm](https://www.jetbrains.com/help/webstorm/eslint.html) -Enable **CSSComb** by installing CSSReorder plug-in +Enable **CSSComb** by following the instructions +[here](https://github.com/csscomb/jetbrains-csscomb). -![csscomb-in-webstorm](https://dl.dropboxusercontent.com/u/16006521/react-starter-kit/webstorm-csscomb.png) +**If you have trouble with autoreloading** try to disable "safe write" in +`File > Settings > System Settings > Use "safe write" (save changes to a temporary file first)` ### Atom Install atom packages -* [linter](https://atom.io/packages/linter) -* [linter-eslint](https://atom.io/packages/linter-eslint) -* [linter-stylelint](https://atom.io/packages/linter-stylelint) -* [react](https://atom.io/packages/react) +- [linter](https://atom.io/packages/linter) +- [linter-eslint](https://atom.io/packages/linter-eslint) +- [linter-stylelint](https://atom.io/packages/linter-stylelint) +- [react](https://atom.io/packages/react) ```shell apm install linter linter-eslint react linter-stylelint @@ -40,40 +43,45 @@ apm install linter linter-eslint react linter-stylelint Install local npm packages -* [eslint](https://www.npmjs.com/package/eslint) -* [babel-eslint](https://www.npmjs.com/package/babel-eslint) -* [eslint-plugin-react](https://www.npmjs.com/package/eslint-plugin-react) -* [stylelint](https://www.npmjs.com/package/stylelint) +- [eslint](https://www.npmjs.com/package/eslint) +- [babel-eslint](https://www.npmjs.com/package/babel-eslint) +- [eslint-plugin-react](https://www.npmjs.com/package/eslint-plugin-react) +- [stylelint](https://www.npmjs.com/package/stylelint) ```shell -npm install --save-dev eslint babel-eslint eslint-plugin-react stylelint +yarn add --dev eslint babel-eslint eslint-plugin-react stylelint ``` -*You may need to restart atom for changes to take effect* +_You may need to restart atom for changes to take effect_ ### SublimeText -Install SublimeText packages -Easiest with [Package Control](https://packagecontrol.io/) and then "Package Control: Install Package" (Ctrl+Shift+P) +Install SublimeText packages\ +Easiest with [Package Control](https://packagecontrol.io/) and then "Package Control: +Install Package" (Ctrl+Shift+P) -* [Babel](https://packagecontrol.io/packages/Babel) -* [Sublime-linter](http://www.sublimelinter.com/en/latest/) -* [SublimeLinter-contrib-eslint](https://packagecontrol.io/packages/SublimeLinter-contrib-eslint) -* [SublimeLinter-contrib-stylelint](https://packagecontrol.io/packages/SublimeLinter-contrib-stylelint) +- [Babel](https://packagecontrol.io/packages/Babel) +- [Sublime-linter](http://www.sublimelinter.com/en/latest/) +- [SublimeLinter-contrib-eslint](https://packagecontrol.io/packages/SublimeLinter-contrib-eslint) +- [SublimeLinter-contrib-stylelint](https://packagecontrol.io/packages/SublimeLinter-contrib-stylelint) -You can also use [SublimeLinter-contrib-eslint_d](https://packagecontrol.io/packages/SublimeLinter-contrib-eslint_d) for faster linting. +You can also use +[SublimeLinter-contrib-eslint_d](https://packagecontrol.io/packages/SublimeLinter-contrib-eslint_d) +for faster linting. Set Babel as default syntax for a particular extension: -* Open a file with that extension, -* Select `View` from the menu, -* Then `Syntax` `->` `Open all with current extension as...` `->` `Babel` `->` `JavaScript (Babel)`. -* Repeat this for each extension (e.g.: .js and .jsx). +- Open a file with that extension, +- Select `View` from the menu, +- Then `Syntax` `->` `Open all with current extension as...` `->` `Babel` `->` + `JavaScript (Babel)`. +- Repeat this for each extension (e.g.: .js and .jsx). Install local npm packages + ``` -npm install eslint@latest -npm install babel-eslint@latest -npm install eslint-plugin-react -npm install stylelint +yarn add --dev eslint@latest +yarn add --dev babel-eslint@latest +yarn add --dev eslint-plugin-react +yarn add --dev stylelint ``` diff --git a/docs/react-style-guide.md b/docs/react-style-guide.md index a54d2da8d..c4d71fe53 100644 --- a/docs/react-style-guide.md +++ b/docs/react-style-guide.md @@ -1,27 +1,28 @@ ## React Style Guide -> This style guide comes as an addition to [Airbnb React/JSX Guide](https://github.com/airbnb/javascript/tree/master/react). -> Feel free to modify it to suit your project's needs. +> This style guide comes as an addition to +> [Airbnb React/JSX Guide](https://github.com/airbnb/javascript/tree/master/react). +> Feel free to modify it to suit your project's needs. ### Table of Contents -* [Separate folder per UI component](#separate-folder-per-ui-component) -* [Prefer using functional components](#prefer-using-functional-components) -* [Use CSS Modules](#use-css-modules) -* [Use higher-order components](#use-higher-order-components) +- [Separate folder per UI component](#separate-folder-per-ui-component) +- [Prefer using functional components](#prefer-using-functional-components) +- [Use CSS Modules](#use-css-modules) +- [Use higher-order components](#use-higher-order-components) ### Separate folder per UI component -* Place each major UI component along with its resources in a separate folder
- This will make it easier to find related resources for any particular UI - element (CSS, images, unit tests, localization files etc.). Removing such - components during refactorings should also be easy. -* Avoid having CSS, images and other resource files shared between multiple components.
+- Place each major UI component along with its resources in a separate folder\ + This will make it easier to find related resources for any particular UI element + (CSS, images, unit tests, localization files etc.). Removing such components during + refactorings should also be easy. +- Avoid having CSS, images and other resource files shared between multiple + components.\ This will make your code more maintainable, easy to refactor. -* Add `package.json` file into each component's folder.
- This will allow to easily reference such components from other places in - your code.
- `import Nav from '../Nav'` vs `import Nav from '../Nav/Nav.js'` +- Add `package.json` file into each component's folder.\ + This will allow to easily reference such components from other places in your code.\ + Use `import Nav from '../Navigation'` instead of `import Nav from '../Navigation/Navigation.js'` ``` /components/Navigation/icon.svg @@ -33,18 +34,19 @@ ``` ``` -// components/Navigation/package.json +// components/Navigation/package.json { "name:": "Navigation", "main": "./Navigation.js" } ``` -For more information google for [component-based UI development](https://google.com/search?q=component-based+ui+development). +For more information google for +[component-based UI development](https://google.com/search?q=component-based+ui+development). ### Prefer using functional components -* Prefer using stateless functional components whenever possible.
+- Prefer using stateless functional components whenever possible.\ Components that don't use state are better to be written as simple pure functions. ```jsx @@ -67,13 +69,17 @@ Navigation.propTypes = { items: PropTypes.array.isRequired }; ### Use CSS Modules -* Use CSS Modules
+- Use CSS Modules\ This will allow using short CSS class names and at the same time avoid conflicts. -* Keep CSS simple and declarative. Avoid loops, mixins etc. -* Feel free to use variables in CSS via [precss](https://github.com/jonathantneal/precss) plugin for [PostCSS](https://github.com/postcss/postcss) -* Prefer CSS class selectors instead of element and `id` selectors (see [BEM](https://bem.info/)) -* Avoid nested CSS selectors (see [BEM](https://bem.info/)) -* When in doubt, use `.root { }` class name for the root elements of your components +- Keep CSS simple and declarative. Avoid loops, mixins etc. +- Feel free to use variables in CSS via + [precss](https://github.com/jonathantneal/precss) plugin for + [PostCSS](https://github.com/postcss/postcss) +- Prefer CSS class selectors instead of element and `id` selectors (see + [BEM](https://bem.info/)) +- Avoid nested CSS selectors (see [BEM](https://bem.info/)) +- When in doubt, use `.root { }` class name for the root elements of your + components ```scss // Navigation.scss @@ -103,7 +109,7 @@ Navigation.propTypes = { items: PropTypes.array.isRequired }; color: $default-color; text-decoration: none; line-height: 25px; - transition: background-color .3s ease; + transition: background-color 0.3s ease; &, .items:hover & { @@ -119,19 +125,25 @@ Navigation.propTypes = { items: PropTypes.array.isRequired }; ```jsx // Navigation.js -import React, { PropTypes } from 'react'; -import withStyles from 'isomorphic-style-loader/lib/withStyles'; +import React from 'react'; +import PropTypes from 'prop-types'; +import useStyles from 'isomorphic-style-loader/useStyles'; import s from './Navigation.scss'; -function Navigation() { +export default function Navigation() { + useStyles(s); return ( @@ -139,13 +151,11 @@ function Navigation() { } Navigation.propTypes = { className: PropTypes.string }; - -export default withStyles(Navigation, s); ``` ### Use higher-order components -* Use higher-order components (HOC) to extend existing React components.
+- Use higher-order components (HOC) to extend existing React components.\ Here is an example: ```js @@ -155,11 +165,10 @@ import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'; function withViewport(ComposedComponent) { return class WithViewport extends Component { - state = { - viewport: canUseDOM ? - {width: window.innerWidth, height: window.innerHeight} : - {width: 1366, height: 768} // Default size for server-side rendering + viewport: canUseDOM + ? { width: window.innerWidth, height: window.innerHeight } + : { width: 1366, height: 768 }, // Default size for server-side rendering }; componentDidMount() { @@ -173,33 +182,33 @@ function withViewport(ComposedComponent) { } handleResize = () => { - let viewport = {width: window.innerWidth, height: window.innerHeight}; - if (this.state.viewport.width !== viewport.width || - this.state.viewport.height !== viewport.height) { + let viewport = { width: window.innerWidth, height: window.innerHeight }; + if ( + this.state.viewport.width !== viewport.width || + this.state.viewport.height !== viewport.height + ) { this.setState({ viewport }); } }; render() { - return ; + return ( + + ); } - }; -}; +} export default withViewport; ``` ```js // MyComponent.js -import React from 'react'; import withViewport from './withViewport'; -class MyComponent { - render() { - let { width, height } = this.props.viewport; - return
{`Viewport: ${width}x${height}`}
; - } +function MyComponent(props) { + const { width, height } = props.viewport; + return
{`Viewport: ${width}x${height}`}
; } export default withViewport(MyComponent); diff --git a/docs/recipes/how-to-configure-facebook-login.md b/docs/recipes/how-to-configure-facebook-login.md new file mode 100644 index 000000000..fe1be8e4f --- /dev/null +++ b/docs/recipes/how-to-configure-facebook-login.md @@ -0,0 +1,11 @@ +How to configure Facebook Login + +1. Navigate and Login to https://developers.facebook.com/apps +2. Click "Add New App" +3. Enter your app name and contact email +4. In App Dashboard, click `Set Up` in Facebook Login section +5. Chose "Web" as your Platform +6. Set Site URL to `http://localhost:3000/` for local testing. (Port of your Node server, not the port of the BrowserSync proxy) +7. Click Facebook Login on the left panel +8. Turn on Client OAuth Login and Web OAuth Login. Enter `http://localhost:3000/login/facebook/return` for Valid OAuth redirect URIs. +9. Get your App ID and Secret and copy it to [`src/config.js`](../src/config.js) diff --git a/docs/recipes/how-to-implement-routing.md b/docs/recipes/how-to-implement-routing.md index 0f498fe1c..f0eff5869 100644 --- a/docs/recipes/how-to-implement-routing.md +++ b/docs/recipes/how-to-implement-routing.md @@ -1,137 +1,228 @@ -## How to Implement Routing and Navigation [![img](https://img.shields.io/badge/discussion-join-green.svg?style=flat-square)](https://github.com/kriasoft/react-starter-kit/issues/116) +## How to Implement Routing and Navigation - * [Step 1: Basic Routing](#step-1-basic-routing) - * [Step 2: Asynchronous Routes](#step-2-asynchronous-routes) - * [Step 3: Parameterized Routes](#step-3-parameterized-routes) - * Step 4: Handling Redirects - * Step 5: Setting Page Title and Meta Tags - * Step 6: Code Splitting - * Step 7: Nested Routes - * Step 8: Integration with Flux - * Step 9: Server-side Rendering +Let's see how a custom routing solution under 100 lines of code may look like. -### Step 1: Basic Routing +First, you will need to implement the **list of application routes** in which each route can be +represented as an object with properties of `path` (a parametrized URL path string), `action` +(a function), and optionally `children` (a list of sub-routes, each of which is a route object). +The `action` function returns anything - a string, a React component, etc. For example: -In its simplest form the routing looks like a collection of URLs where each URL -is mapped to a React component: +#### `src/routes/index.js` ```js -// client.js -import React from 'react'; -import Layout from './components/Layout'; -import HomePage from './components/HomePage'; -import AboutPage from './components/AboutPage'; -import NotFoundPage from './components/NotFoundPage'; -import ErrorPage from './components/ErrorPage'; - -const routes = { - '/': , - '/about': -}; - -const container = document.getElementById('app'); - -function render() { - try { - const path = window.location.hash.substr(1) || '/'; - const component = routes[path] || ; - React.render(component, container); - } catch (err) { - React.render(, container); +export default [ + { + path: '/tasks', + action() { + const resp = await fetch('http://github.com/api/tasks'); + const data = await resp.json(); + return data && { + title: `To-do (${data.length})`, + component: + }; + } + }, + { + path: '/tasks/:id', + action({ params }) { + const resp = await fetch(`/api/tasks/${params.id}`); + const data = await resp.json(); + return data && { + title: data.title, + component: + }; + } } -} - -window.addEventListener('hashchange', () => render()); -render(); +]; ``` -### Step 2: Asynchronous Routes +Next, implement a **URL Matcher** function that will be responsible for matching a parametrized +path string to the actual URL. For example, calling `matchURI('/tasks/:id', '/tasks/123')` must +return `{ id: '123' }` while calling `matchURI('/tasks/:id', '/foo')` must return `null`. +Fortunately, there is a great library called [`path-to-regexp`](https://github.com/pillarjs/path-to-regexp) +that makes this task very easy. Here is how a URL matcher function may look like: -Just wrap React components inside your routes into asynchronous functions: +#### `src/router.js` ```js -import React from 'react'; -import fetch from './core/fetch'; -import Layout from './components/Layout'; -import HomePage from './components/HomePage'; -import AboutPage from './components/AboutPage'; -import NotFoundPage from './components/NotFoundPage'; -import ErrorPage from './components/ErrorPage'; - -const routes = { - '/': async () => { - const response = await fetch('http://github.com/graphql?query={content(path:"/"){title,html}}'); - const data = await response.json(); - return - }, - '/about': async () => { - const response = await fetch('http://github.com/graphql?query={content(path:"/about"){title,html}}'); - const data = await response.json(); - return ; +import toRegExp from 'path-to-regexp'; + +function matchURI(path, uri) { + const keys = []; + const pattern = toRegExp(path, keys); // TODO: Use caching + const match = pattern.exec(uri); + if (!match) return null; + const params = Object.create(null); + for (let i = 1; i < match.length; i++) { + params[keys[i - 1].name] = match[i] !== undefined ? match[i] : undefined; } -}; - -const container = document.getElementById('app'); - -async function render() { - try { - const path = window.location.hash.substr(1) || '/'; - const route = routes[path]; - const component = route ? await route() : ; - React.render(component, container); - } catch (err) { - React.render(, container); + return params; +} +``` + +Finally, implement a **Route Resolver** function that given a list of routes and a URL/context +should find the first route matching the provided URL string, execute its action method, and if the +action method returns anything other than `null` or `undefined` return that to the caller. +Otherwise, it should continue iterating over the remaining routes. If none of the routes match to the +provided URL string, it should throw an exception (Not found). Here is how this function may look like: + +#### `src/router.js` + +```js +import toRegExp from 'path-to-regexp'; + +function matchURI(path, uri) { ... } // See above + +async function resolve(routes, context) { + for (const route of routes) { + const uri = context.error ? '/error' : context.pathname; + const params = matchURI(route.path, uri); + if (!params) continue; + const result = await route.action({ ...context, params }); + if (result) return result; } + const error = new Error('Not found'); + error.status = 404; + throw error; } -window.addEventListener('hashchange', () => render()); -render(); +export default { resolve }; ``` -### Step 3: Parameterized Routes +That's it! Here is a usage example: -**(1)** Convert the list of routes from hash table to an array, this way the -order of routes will be preserved. **(2)** Wrap this collection into a Router -class, where you can put `.match(url)` async method. **(3)** Use [path-to-regexp](https://github.com/pillarjs/path-to-regexp) -to convert Express-like path strings into regular expressions which are used -for matching URL paths to React components. +```js +import router from './router'; +import routes from './routes'; + +router.resolve(routes, { pathname: '/tasks' }).then(result => { + console.log(result); + // => { title: 'To-do', component: } +}); +``` + +While you can use this as it is on the server, in a browser environment it must be combined with a +client-side navigation solution. You can use [`history`](https://github.com/ReactTraining/history) +npm module to handles this task for you. It is the same library used in React Router, sort of a +wrapper over [HTML5 History API](https://developer.mozilla.org/docs/Web/API/History_API) that +handles all the tricky browser compatibility issues related to client-side navigation. + +First, create `src/history.js` file that will initialize a new instance of the `history` module +and export is as a singleton: + +#### `src/history.js` ```js -import React from 'react'; -import Router from 'react-routing/src/Router'; -import fetch from './core/fetch'; -import Layout from './components/Layout'; -import ProductListing from './components/ProductListing'; -import ProductInfo from './components/ProductInfo'; -import NotFoundPage from './components/NotFoundPage'; -import ErrorPage from './components/ErrorPage'; - -const router = new Router(on => { - on('/products', async () => { - const response = await fetch('http://github.com/graphql?query={products{id,name}}'); - const data = await response.json(); - return - }); - on('/products/:id', async ({ params }) => { - const response = await fetch('http://github.com/graphql?query={product(id:"${params.id}"){name,summary}}'); - const data = await response.json(); - return ; +import createHistory from 'history/lib/createBrowserHistory'; +import useQueries from 'history/lib/useQueries'; +export default useQueries(createHistory)(); +``` + +Then plug it in, in your client-side bootstrap code as follows: + +#### `src/client.js` + +```js +import ReactDOM from 'react-dom'; +import history from './history'; +import router from './router'; +import routes from './routes'; + +const container = document.getElementById('root'); + +function renderRouteOutput({ title, component }) { + ReactDOM.render(component, container, () => { + document.title = title; }); -}]); +} + +function render(location) { + router + .resolve(routes, location) + .then(renderRouteOutput) + .catch(error => + router.resolve(routes, { ...location, error }).then(renderRouteOutput), + ); +} -const container = document.getElementById('app'); +render(history.getCurrentLocation()); // render the current URL +history.listen(render); +``` + +Whenever a new location is pushed into the `history` stack, the `render()` method will be called, +that itself calls the router's `resolve()` method and renders the returned from it React component +into the DOM. + +In order to trigger client-side navigation without causing full-page refresh, you need to use +`history.push()` method, for example: -async function render() { - const state = { path: window.location.hash.substr(1) || '/' }; - await router.dispatch(state, component => { - React.render(component, container); +```js +import history from '../history'; + +function transition(event) { + event.preventDefault(); + history.push({ + pathname: event.currentTarget.pathname, + search: event.currentTarget.search, }); } -window.addEventListener('hashchange', () => render()); -render(); +function App() { + return ( + + ); +} ``` -### Step 4. Handling Redirects +Though, it is a common practice to extract that transitioning functionality into a stand-alone +(`Link`) component that can be used as follows: + +```html +View Task #123 +``` + +### Routing in React Starter Kit + +React Starter Kit (RSK) uses [Universal Router](https://github.com/kriasoft/universal-router) npm +module that is built around the same concepts demonstrated earlier with the major differences that +it supports nested routes and provides you with the helper `Link` React component. It can be seen as +a lightweight more flexible alternative to React Router. + +- It has simple code with minimum dependencies (just `path-to-regexp` and `babel-runtime`) +- It can be used with any JavaScript framework such as React, Vue.js etc +- It uses the same middleware approach used in Express and Koa, making it easy to learn +- It uses the exact same API and implementation to be used in both Node.js and browser environments + +The [Getting Started page](https://github.com/kriasoft/universal-router/blob/master/docs/getting-started.md) +has a few examples how to use it. + +### Related Articles + +- [You might not need React Router](https://medium.freecodecamp.com/you-might-not-need-react-router-38673620f3d) by Konstantin Tarkus + +### Related Projects + +- [`path-to-regexp`](https://github.com/pillarjs/path-to-regexp) +- [`history`](https://github.com/ReactTraining/history) +- [Universal Router](https://github.com/kriasoft/universal-router) + +### Related Discussions -Coming soon. Stay tuned! +- [How to Implement Routing and Navigation](https://github.com/kriasoft/react-starter-kit/issues/748) +- [How to Add a Route to RSK?](https://github.com/kriasoft/react-starter-kit/issues/754) diff --git a/docs/recipes/how-to-integrate-disqus.md b/docs/recipes/how-to-integrate-disqus.md index 9add5c4b5..f0983dfa0 100644 --- a/docs/recipes/how-to-integrate-disqus.md +++ b/docs/recipes/how-to-integrate-disqus.md @@ -5,7 +5,8 @@ https://disqus.com/admin/create/ #### `DisqusThread.js` ```js -import React, { PropTypes } from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; const SHORTNAME = 'example'; const WEBSITE_URL = 'http://www.example.com/'; @@ -17,22 +18,23 @@ function renderDisqus() { script.src = 'https://' + SHORTNAME + '.disqus.com/embed.js'; document.getElementsByTagName('head')[0].appendChild(script); } else { - window.DISQUS.reset({reload: true}); + window.DISQUS.reset({ reload: true }); } } -class DisqusThread { - +class DisqusThread extends React.Component { static propTypes = { id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, - path: PropTypes.string.isRequired + path: PropTypes.string.isRequired, }; shouldComponentUpdate(nextProps) { - return this.props.id !== nextProps.id || + return ( + this.props.id !== nextProps.id || this.props.title !== nextProps.title || - this.props.path !== nextProps.path; + this.props.path !== nextProps.path + ); } componentDidMount() { @@ -44,20 +46,17 @@ class DisqusThread { } render() { - let { id, title, path, ...other} = this.props; + let { id, title, path, ...other } = this.props; if (process.env.BROWSER) { - /* eslint-disable camelcase */ window.disqus_shortname = SHORTNAME; window.disqus_identifier = id; window.disqus_title = title; window.disqus_url = WEBSITE_URL + path; - /* eslint-enable camelcase */ } return
; } - } export default DisqusThread; @@ -66,22 +65,17 @@ export default DisqusThread; #### `MyComponent.js` ```js -import React from 'react'; import DisqusThread from './DisqusThread.js'; -class MyComponent { - - render() { - return ( -
- -
- ); - } - +export default function MyComponent() { + return ( +
+ +
+ ); } - -export default MyComponent; ``` diff --git a/docs/recipes/how-to-integrate-react-intl.md b/docs/recipes/how-to-integrate-react-intl.md new file mode 100644 index 000000000..455d01020 --- /dev/null +++ b/docs/recipes/how-to-integrate-react-intl.md @@ -0,0 +1,133 @@ +## How to Integrate [React Intl](https://github.com/yahoo/react-intl#react-intl) + +1. Merge `feature/react-intl` branch with git. + Because react-intl integration is built on top of `feature/redux`, you'll also get all the features. + +2. Adjust `INTL_REQUIRE_DESCRIPTIONS` constant in `tools/webpack.config.js` around line 17: + + ```js + const INTL_REQUIRE_DESCRIPTIONS = true; + ``` + + When this boolean is set to true, the build will only succeed if a `description` is set for every message descriptor. + +3. Adjust `locales` settings in `src/config.js`: + + ```js + // default locale is the first one + export const locales = ['en-GB', 'cs-CZ']; + ``` + + Note that you should follow + [BCP 47](https://tools.ietf.org/html/bcp47) + ([RFC 5646](https://tools.ietf.org/html/rfc5646)). + +4. Add locale support in `src/client.js`: + + ```js + import en from 'react-intl/locale-data/en'; + import cs from 'react-intl/locale-data/cs'; + ... + + [en, cs].forEach(addLocaleData); + ``` + +5. Execute `yarn run messages` or `yarn start` to strip out messages. + Message files are created in `src/messages` directory. + +6. Edit `src/messages/*.json` files, change only `message` property. + +7. Execute `yarn run build`, + your translations should be copied to `build/messages/` directory. + +### How to write localizable components + +Just import the appropriate [component](https://github.com/yahoo/react-intl/wiki#the-react-intl-module) from `react-intl` + +- For localizable text use + [``](https://github.com/yahoo/react-intl/wiki/Components#formattedmessage). +- You can also use it with + the [`defineMessages()`](https://github.com/yahoo/react-intl/wiki/API#definemessages) helper. + +- For date and time: + [``](https://github.com/yahoo/react-intl/wiki/Components#formatteddate) + [``](https://github.com/yahoo/react-intl/wiki/Components#formattedtime) + [``](https://github.com/yahoo/react-intl/wiki/Components#formattedrelative) + +- For numbers and currencies: + [``](https://github.com/yahoo/react-intl/wiki/Components#formattednumber) + [``](https://github.com/yahoo/react-intl/wiki/Components#formattedplural) + +- If possible, do not use ``, see how to use _Rich Text Formatting_ with + [``](https://github.com/yahoo/react-intl/wiki/Components#formattedmessage) + +- When you need an imperative formatting API, use the [`injectIntl`](https://github.com/yahoo/react-intl/wiki/API#injectintl) High-Order Component. + +#### Example + +```jsx +import { + defineMessages, + FormattedMessage, + injectIntl, + intlShape, +} from 'react-intl'; + +const messages = defineMessages({ + text: { + id: 'example.text', + defaultMessage: 'Example text', + description: 'Hi Pavel', + }, + textTemplate: { + id: 'example.text.template', + defaultMessage: 'Example text template', + description: 'Hi {name}', + }, +}); + +function Example(props) { + const text = props.intl.formatMessage(messages.textTemplate, { + name: 'Pavel', + }); + return ( +
+ + + Pavel, + }} + /> +
+ ); +} + +Example.propTypes = { + intl: intlShape, +}; + +export default injectIntl(Example); +``` + +### Updating translations + +When running the development server, every source file is watched and parsed for changed messages. + +Messages files are updated on the fly. +If a new definition is found, this definition is added to the end of every used `src/messages/xx-XX.json` file so when committing, new translations will be at the tail of file. + +When an untranslated message is removed and its `message` field is empty as well, the message will be deleted from all translation files. This is why the `files` array is present. + +When editing a translation file, it should be copied to `build/messages/` directory. + +### Other References + +- [`Intl documentation on MDN`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Intl) +- [express-request-language](https://github.com/tinganho/express-request-language#readme) + – for more details how initial language negotiation works. diff --git a/docs/recipes/how-to-integrate-redux.md b/docs/recipes/how-to-integrate-redux.md new file mode 100644 index 000000000..f7b7b6fc6 --- /dev/null +++ b/docs/recipes/how-to-integrate-redux.md @@ -0,0 +1,52 @@ +## How to Integrate [Redux](http://redux.js.org/index.html) + +Merge `feature/redux` branch with Git. If you are interested in `feature/react-intl`, merge that +branch instead as it also includes Redux. + +**If you don't know Redux well, you should [read about it first](http://redux.js.org/docs/basics/index.html).** + +### Creating Actions + +1. Go to `src/constants/index.js` and define action name there. + +2. Go to `src/actions/` and create file with appropriate name. You can copy + `src/actions/runtime.js` as a template. + +3. If you need async actions, use [`redux-thunk`](https://github.com/gaearon/redux-thunk#readme). + For inspiration on how to create async actions you can look at + [`setLocale`](https://github.com/kriasoft/react-starter-kit/blob/feature/react-intl/src/actions/intl.js) + action from `feature/react-intl`. + See [Async Flow](http://redux.js.org/docs/advanced/AsyncFlow.html) for more information on this + topic. + +### Creating Reducer (aka Store) + +1. Go to [`src/reducers/`](https://github.com/kriasoft/react-starter-kit/tree/feature/redux/src/reducers) and create new file there. + + You can copy [`src/reducers/runtime.js`](https://github.com/kriasoft/react-starter-kit/tree/feature/redux/src/reducers/runtime.js) as a template. + + - Do not forget to always return `state`. + - Never mutate provided `state`. + If you mutate state, rendering of connected component will not be triggered because of `===` equality. + Always return new value if you perform state update. + You can use this construct: `{ ...state, updatedKey: action.payload.value, }` + - Keep in mind that store state _must_ be repeatable by replaying actions on it. + For example, when you store timestamp, pass it into _action payload_. + If you call REST API, do it in action. _Never do this in reducer!_ + +2. Edit [`src/reducers/index.js`](https://github.com/kriasoft/react-starter-kit/tree/feature/redux/src/reducers/index.js), import your reducer and add it to root reducer created by + [`combineReducers`](http://redux.js.org/docs/api/combineReducers.html) + +### Connecting Components + +You can use [`connect()`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) High-Order Component from [`react-redux`](https://github.com/reactjs/react-redux#readme) package. + +See [Usage With React](http://redux.js.org/docs/basics/UsageWithReact.html) on redux.js.org. + +For an example you can look at +[``](https://github.com/kriasoft/react-starter-kit/blob/feature/react-intl/src/components/LanguageSwitcher/LanguageSwitcher.js) +component from `feature/react-intl` branch. It demonstrates both subscribing to store and dispatching actions. + +### Dispatching Actions On Server + +See source of `src/server.js` diff --git a/docs/recipes/how-to-use-sass.md b/docs/recipes/how-to-use-sass.md new file mode 100644 index 000000000..61b11e1c0 --- /dev/null +++ b/docs/recipes/how-to-use-sass.md @@ -0,0 +1,75 @@ +## How to Use Sass/SCSS + +> **Note**: Using plain CSS via [PostCSS](http://postcss.org/) is recommended approach because it +> reduces the size of the tech stack used in the project, enforces you to learn vanilla CSS syntax +> with modern CSS Level 3+ features that allow you doing everything you would normally do with +> Sass/SCSS. Also compilation of plain `.css` files should work faster with `postcss` pre-processor +> than `node-sass`. + +### Step 1 + +Install [`node-sass`](https://github.com/sass/node-sass) +(includes [node-gyp](https://github.com/nodejs/node-gyp#readme) +and [prerequisites](https://github.com/nodejs/node-gyp#installation)) and +[`sass-loader`](https://github.com/jtangelder/sass-loader) modules as dev dependencies: + +```sh +$ yarn add node-sass --dev +$ yarn add sass-loader --dev +``` + +### Step 2 + +Update [`webpack.config.js`](../../tools/webpack.config.js) file to use `sass-loader` for `.scss` files: + +```js +const config = { + ... + module: { + rules: [ + ... + { + test: /\.scss$/, + use: [ + { + loader: 'isomorphic-style-loader', + }, + { + loader: 'css-loader', + options: { + sourceMap: isDebug, + minimize: !isDebug, + }, + }, + { + loader: 'postcss-loader', + options: { + config: { + path: './tools/postcss.sass.js', + }, + }, + }, + { + loader: 'sass-loader', + }, + ], + }, + ... + ] + } + ... +} +``` + +### Step 3 + +Add one more configuration (`tools/postcss.sass.js`) for [PostCSS](https://github.com/postcss/postcss) to +enable [Autoprefixer](https://github.com/postcss/autoprefixer) for your `.scss` files: + +```js +module.exports = () => ({ + plugins: [require('autoprefixer')()], +}); +``` + +For more information visit https://github.com/jtangelder/sass-loader and https://github.com/sass/node-sass diff --git a/docs/recipes/using-npm-and-webpack-as-a-build-tool.md b/docs/recipes/using-npm-and-webpack-as-a-build-tool.md index fcdb0f009..ec32be86b 100644 --- a/docs/recipes/using-npm-and-webpack-as-a-build-tool.md +++ b/docs/recipes/using-npm-and-webpack-as-a-build-tool.md @@ -1,6 +1,6 @@ -## Using NPM and Webpack as a Build Tool +## Using Yarn and Webpack as a Build Tool -The [npm](https://docs.npmjs.com/) command line utility that comes with Node.js +The [Yarn](https://yarnpkg.com/) command line utility that comes with Node.js allows you to run arbitrary scripts and [Node.js modules](https://www.npmjs.com/) without them being globally installed. This is very convenient, because other developers in your team don't need to worry about having some set of tools @@ -11,7 +11,7 @@ For example, if you need to lint your JavaScript code with [ESLint](http://eslin and [JSCS](http://jscs.info/), you just install them as project's dependencies: ```shell -$ npm install eslint jscs --save-dev +$ yarn add eslint jscs --dev ``` Add a new command line to `package.json/scripts`: @@ -31,7 +31,7 @@ Add a new command line to `package.json/scripts`: And execute it by running: ```shell -$ npm run lint # npm run +$ yarn run lint # yarn run ``` Which will be the same as running `./node_modules/bin/eslint src && ./node_modules/bin/jscs src`, @@ -89,5 +89,5 @@ The `npm` script for it may look like this: You can run it as follows: ```shell -$ npm run build +$ yarn run build ``` diff --git a/docs/testing-your-application.md b/docs/testing-your-application.md new file mode 100644 index 000000000..6c72bc692 --- /dev/null +++ b/docs/testing-your-application.md @@ -0,0 +1,102 @@ +## Testing your application + +### Used libraries + +RSK comes with the following libraries for testing purposes: + +- [Jest](https://facebook.github.io/jest/) - JavaScript testing library +- [Enzyme](https://github.com/airbnb/enzyme) - Testing utilities for React + +You may also want to take a look at the following related packages: + +- [jsdom](https://github.com/tmpvar/jsdom) +- [react-addons-test-utils](https://www.npmjs.com/package/react-addons-test-utils) + +### Running tests + +To test your application simply run the +[`yarn test`](https://github.com/kriasoft/react-starter-kit/blob/9014614edcb2f44b23298ca3287b9af3a14b6076/package.json#L152) +command which will: + +- recursively find all files ending with `.test.js` in your `src/` directory + +```bash +yarn test +``` + +### Conventions + +- test filenames MUST end with `test.js` or `yarn test` will not be able to + detect them +- test filenames SHOULD be named after the related component (e.g. create + `Login.test.js` for `Login.js` component) + +### Basic example + +To help you on your way RSK comes with the following +[basic test case](https://github.com/kriasoft/react-starter-kit/blob/master/src/components/Layout/Layout.test.js) +you can use as a starting point: + +```js +import React from 'react'; +import renderer from 'react-test-renderer'; +import App from '../App'; +import Layout from './Layout'; + +describe('Layout', () => { + test('renders children correctly', () => { + const wrapper = renderer + .create( + {}, fetch: () => {}, pathname: '' }}> + +
+ + , + ) + .toJSON(); + + expect(wrapper).toMatchSnapshot(); + }); +}); +``` + +### React-intl exampleß + +React-intl users MUST render/wrap components inside an IntlProvider like the +example below: + +The example below example is a drop-in test for the RSK `Header` component: + +```js +import React from 'react'; +import Header from './Header'; +import IntlProvider from 'react-intl'; +import Navigation from '../../components/Navigation'; + +describe('A test suite for
', () => { + it('should contain a component', () => { + it('rendering', () => { + const wrapper = renderIntoDocument( + +
+ , + ); + expect(wrapper.find(Navigation)).to.have.length(1); + }); + }); +}); +``` + +Please note that NOT using IntlProvider will produce the following error: + +> Invariant Violation: [React Intl] Could not find required `intl` object. +> needs to exist in the component ancestry. + +### Linting + +In order to check if your JavaScript and CSS code follows the suggested style +guidelines run: + +```bash +yarn run lint +``` diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..7cf656483 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,102 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-present Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +// Jest configuration +// https://facebook.github.io/jest/docs/en/configuration.html +module.exports = { + // Modules can be explicitly auto-mocked using jest.mock(moduleName). + // https://facebook.github.io/jest/docs/en/configuration.html#automock-boolean + automock: false, // [boolean] + + // Respect Browserify's "browser" field in package.json when resolving modules. + // https://facebook.github.io/jest/docs/en/configuration.html#browser-boolean + browser: false, // [boolean] + + // This config option can be used here to have Jest stop running tests after the first failure. + // https://facebook.github.io/jest/docs/en/configuration.html#bail-boolean + bail: false, // [boolean] + + // The directory where Jest should store its cached dependency information. + // https://facebook.github.io/jest/docs/en/configuration.html#cachedirectory-string + // cacheDirectory: '/tmp/', // [string] + + // Indicates whether the coverage information should be collected while executing the test. + // Because this retrofits all executed files with coverage collection statements, + // it may significantly slow down your tests. + // https://facebook.github.io/jest/docs/en/configuration.html#collectcoverage-boolean + // collectCoverage: false, // [boolean] + + // https://facebook.github.io/jest/docs/en/configuration.html#collectcoveragefrom-array + collectCoverageFrom: [ + 'src/**/*.{js,jsx}', + '!**/node_modules/**', + '!**/vendor/**', + ], + + // https://facebook.github.io/jest/docs/en/configuration.html#coveragedirectory-string + coverageDirectory: '/coverage', // [string] + + // coveragePathIgnorePatterns: // [array] + // coverageReporters: [], // [array] + // coverageThreshold: {}, // [object] + + globals: { + __DEV__: true, + }, + + // https://facebook.github.io/jest/docs/en/configuration.html#mapcoverage-boolean + // mapCoverage: false, // [boolean] + + // The default extensions Jest will look for. + // https://facebook.github.io/jest/docs/en/configuration.html#modulefileextensions-array-string + moduleFileExtensions: ['js', 'json', 'jsx', 'node'], + + // moduleDirectories: // [array] + + // A map from regular expressions to module names that allow to stub out resources, + // like images or styles with a single module. + moduleNameMapper: { + '\\.(css|less|styl|scss|sass|sss)$': 'identity-obj-proxy', + }, + + // modulePathIgnorePatterns: // [array] + // modulePaths: // [array] + // notify: false, // [boolean] + // preset: // [string] + // projects: // [array] + // clearMocks: // [boolean] + // reporters: // [array] + // resetMocks: // [boolean] + // resetModules: // [boolean] + // resolver: // [string] + // rootDir: // [string] + // roots: // [array] + // setupFiles: // [array] + // setupTestFrameworkScriptFile: // [string] + // snapshotSerializers: // [array] + // testEnvironment: // [string] + // testMatch: // [array] + // testPathIgnorePatterns: // [array] + // testRegex: // [string] + // testResultsProcessor: // [string] + // testRunner: // [string] + // testURL: // [string] + // timers: // [string] + + transform: { + '\\.(js|jsx|mjs)$': '/node_modules/babel-jest', + '^(?!.*\\.(js|jsx|json|css|less|styl|scss|sass|sss)$)': + '/tools/lib/fileTransformer.js', + }, + + // transformIgnorePatterns: // [array] + // unmockedModulePathPatterns: // [array] + + verbose: true, // [boolean] +}; diff --git a/package.json b/package.json index d33eb873b..05c63de8f 100644 --- a/package.json +++ b/package.json @@ -1,132 +1,158 @@ { + "name": "web", + "version": "0.0.0", "private": true, "engines": { - "node": ">=5.0 <6", - "npm": ">=3.3 <4" + "node": ">=8.16.2", + "npm": ">=6.4.1" }, + "browserslist": [ + ">1%", + "last 4 versions", + "Firefox ESR", + "not ie < 9" + ], "dependencies": { - "babel-polyfill": "6.7.4", - "babel-runtime": "6.6.1", - "bluebird": "3.3.4", - "body-parser": "1.15.0", - "classnames": "2.2.3", - "cookie-parser": "1.4.1", - "core-js": "2.2.1", - "eventemitter3": "1.2.0", - "express": "4.13.4", - "express-graphql": "0.4.13", - "express-jwt": "3.3.0", - "fastclick": "1.0.6", - "fbjs": "0.8.0-alpha.3", - "front-matter": "2.0.6", - "graphiql": "0.6.6", - "graphql": "0.4.18", - "history": "2.0.1", - "isomorphic-style-loader": "0.0.12", - "jade": "1.11.0", - "jsonwebtoken": "5.7.0", - "markdown-it": "6.0.0", - "node-fetch": "1.4.1", - "normalize.css": "4.0.0", - "passport": "0.3.2", - "passport-facebook": "2.1.0", - "pg": "4.5.1", - "pretty-error": "2.0.0", - "react": "15.0.0-rc.2", - "react-dom": "15.0.0-rc.2", - "react-routing": "0.0.7", - "source-map-support": "0.4.0", - "whatwg-fetch": "0.11.0" + "@babel/polyfill": "^7.7.0", + "body-parser": "^1.19.0", + "classnames": "^2.2.6", + "cookie-parser": "^1.4.3", + "express": "^4.16.3", + "express-graphql": "~0.8.0", + "express-jwt": "^5.3.1", + "graphql": "^14.5.8", + "history": "^4.7.2", + "isomorphic-style-loader": "^5.1.0", + "jsonwebtoken": "^8.3.0", + "node-fetch": "^2.6.1", + "normalize.css": "^8.0.0", + "passport": "^0.4.0", + "passport-facebook": "^3.0.0", + "pretty-error": "^2.1.1", + "prop-types": "^15.6.2", + "query-string": "^6.9.0", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "sequelize": "^5.21.2", + "serialize-javascript": "^3.1.0", + "source-map-support": "^0.5.9", + "sqlite3": "^4.0.8", + "universal-router": "^8.3.0", + "whatwg-fetch": "^3.0.0" }, "devDependencies": { - "assets-webpack-plugin": "^3.4.0", - "autoprefixer": "^6.3.4", - "babel-cli": "^6.6.5", - "babel-eslint": "^6.0.0", - "babel-jest": "^9.0.3", - "babel-loader": "^6.2.4", - "babel-plugin-react-transform": "^2.0.2", - "babel-plugin-transform-runtime": "^6.6.0", - "babel-preset-es2015": "^6.6.0", - "babel-preset-node5": "^11.0.1", - "babel-preset-react": "^6.5.0", - "babel-preset-stage-0": "^6.5.0", - "browser-sync": "^2.11.2", - "css-loader": "^0.23.1", - "del": "^2.2.0", - "enzyme": "^2.2.0", - "eslint": "^2.5.1", - "eslint-config-airbnb": "^6.2.0", - "eslint-loader": "^1.3.0", - "eslint-plugin-react": "^4.2.3", - "estraverse-fb": "^1.3.1", - "extend": "^3.0.0", - "file-loader": "^0.8.5", - "gaze": "^1.0.0", - "git-repository": "^0.1.4", - "glob": "^7.0.3", - "jade-loader": "^0.8.0", - "jest-cli": "^0.9.2", - "jscs": "^2.11.0", - "json-loader": "^0.5.4", + "@babel/core": "^7.7.2", + "@babel/node": "^7.7.0", + "@babel/plugin-proposal-class-properties": "^7.7.0", + "@babel/plugin-syntax-dynamic-import": "^7.0.0", + "@babel/plugin-transform-react-constant-elements": "^7.0.0", + "@babel/plugin-transform-react-inline-elements": "^7.0.0", + "@babel/preset-env": "^7.7.1", + "@babel/preset-flow": "^7.0.0", + "@babel/preset-react": "^7.7.0", + "babel-core": "^7.0.0-bridge.0", + "babel-eslint": "^10.0.3", + "babel-jest": "^24.9.0", + "babel-loader": "^8.0.0", + "babel-plugin-transform-react-remove-prop-types": "^0.4.18", + "browser-sync": "^2.24.7", + "chokidar": "^3.3.0", + "css-loader": "^3.2.0", + "cssnano": "^4.1.10", + "enzyme": "^3.6.0", + "eslint": "^5.6.0", + "eslint-config-airbnb": "^17.1.0", + "eslint-config-prettier": "^6.6.0", + "eslint-import-resolver-node": "^0.3.2", + "eslint-loader": "^3.0.2", + "eslint-plugin-css-modules": "^2.9.1", + "eslint-plugin-flowtype": "^3.13.0", + "eslint-plugin-import": "^2.14.0", + "eslint-plugin-jest": "^23.0.4", + "eslint-plugin-jsx-a11y": "^6.1.1", + "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-react": "^7.11.1", + "execa": "^3.3.0", + "file-loader": "^4.2.0", + "flow-bin": "^0.112.0", + "front-matter": "^3.0.2", + "glob": "^7.1.6", + "husky": "^3.0.9", + "identity-obj-proxy": "^3.0.0", + "jest": "^24.9.0", + "lint-staged": "^9.4.3", + "log-symbols": "^3.0.0", + "markdown-it": "^10.0.0", "mkdirp": "^0.5.1", - "ncp": "^2.0.0", - "postcss": "^5.0.19", - "postcss-import": "^8.0.2", - "postcss-loader": "^0.8.2", - "postcss-scss": "^0.1.7", - "precss": "^1.4.0", - "raw-loader": "^0.5.1", - "react-addons-test-utils": "^15.0.0-rc.2", - "react-transform-catch-errors": "^1.0.2", - "react-transform-hmr": "^1.0.4", - "redbox-react": "^1.2.2", - "stylelint": "^5.2.1", - "stylelint-config-standard": "^4.0.1", - "url-loader": "^0.5.7", - "webpack": "^1.12.14", - "webpack-hot-middleware": "^2.10.0", - "webpack-middleware": "^1.5.1" + "null-loader": "^3.0.0", + "opn-cli": "^5.0.0", + "pixrem": "^5.0.0", + "pleeease-filters": "^4.0.0", + "postcss": "^7.0.2", + "postcss-calc": "^7.0.1", + "postcss-flexbugs-fixes": "^4.1.0", + "postcss-import": "^12.0.0", + "postcss-loader": "^3.0.0", + "postcss-preset-env": "^6.6.0", + "postcss-pseudoelements": "^5.0.0", + "prettier": "^1.19.1", + "puppeteer": "^2.0.0", + "raw-loader": "^3.1.0", + "react-deep-force-update": "^2.1.3", + "react-dev-utils": "^9.1.0", + "react-error-overlay": "^6.0.3", + "react-test-renderer": "^16.12.0", + "rimraf": "^3.0.0", + "stylelint": "^12.0.0", + "stylelint-config-standard": "^19.0.0", + "stylelint-order": "^3.1.1", + "svg-url-loader": "^3.0.2", + "terminate": "^2.1.2", + "url-loader": "^2.2.0", + "wait-on": "^3.3.0", + "webpack": "^4.19.1", + "webpack-assets-manifest": "^3.0.2", + "webpack-bundle-analyzer": "^3.3.2", + "webpack-dev-middleware": "^3.3.0", + "webpack-hot-middleware": "^2.24.2", + "webpack-node-externals": "^1.7.2" }, - "babel": { - "presets": [ - "react", - "node5", - "stage-0" - ] - }, - "jest": { - "rootDir": "./src", - "testPathDirs": [ - "", - "/../test/" + "lint-staged": { + "*.{js,jsx}": [ + "eslint --no-ignore --fix", + "git add --force" ], - "moduleNameMapper": { - "\\.scss$": "SCSSStub" - }, - "unmockedModulePathPatterns": [ - "react", - "enzyme", - "core-js" + "*.{json,md,graphql}": [ + "prettier --write", + "git add --force" + ], + "*.{css,less,styl,scss,sass,sss}": [ + "stylelint --fix", + "git add --force" ] }, - "stylelint": { - "extends": "stylelint-config-standard", - "rules": { - "string-quotes": "single" - } - }, "scripts": { - "eslint": "eslint src tools", - "jscs": "jscs src tools --verbose", - "stylelint": "stylelint \"src/**/*.scss\" --syntax scss", - "lint": "npm run eslint && npm run jscs && npm run stylelint", - "test": "npm run lint && jest", + "precommit": "lint-staged", + "lint-js": "eslint --ignore-path .gitignore --ignore-pattern \"!**/.*\" .", + "lint-css": "stylelint \"src/**/*.{css,less,styl,scss,sass,sss}\"", + "lint": "yarn run lint-js && yarn run lint-css", + "fix-js": "yarn run lint-js --fix", + "fix-css": "yarn run lint-css --fix", + "fix": "yarn run fix-js && yarn run fix-css", + "flow": "flow", + "flow:check": "flow check", + "test": "jest", + "test-watch": "yarn run test --watch --notify", + "test-cover": "yarn run test --coverage", + "coverage": "yarn run test-cover && opn coverage/lcov-report/index.html", "clean": "babel-node tools/run clean", "copy": "babel-node tools/run copy", "bundle": "babel-node tools/run bundle", "build": "babel-node tools/run build", + "build-stats": "yarn run build --release --analyse", "deploy": "babel-node tools/run deploy", + "render": "babel-node tools/run render", + "serve": "babel-node tools/run runServer", "start": "babel-node tools/run start" } } diff --git a/src/public/browserconfig.xml b/public/browserconfig.xml similarity index 100% rename from src/public/browserconfig.xml rename to public/browserconfig.xml diff --git a/src/public/crossdomain.xml b/public/crossdomain.xml similarity index 100% rename from src/public/crossdomain.xml rename to public/crossdomain.xml diff --git a/src/public/favicon.ico b/public/favicon.ico similarity index 100% rename from src/public/favicon.ico rename to public/favicon.ico diff --git a/public/humans.txt b/public/humans.txt new file mode 100644 index 000000000..52ce2eeb1 --- /dev/null +++ b/public/humans.txt @@ -0,0 +1,15 @@ +# humanstxt.org/ +# The humans responsible & technology colophon + +# TEAM + + -- -- + +# THANKS + + + +# TECHNOLOGY COLOPHON + + CSS3, HTML5, JavaScript + React Starter Kit -- https://reactstarter.com/ diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 000000000..177a3d855 Binary files /dev/null and b/public/icon.png differ diff --git a/src/public/robots.txt b/public/robots.txt similarity index 100% rename from src/public/robots.txt rename to public/robots.txt diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 000000000..3992b48d7 --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1,10 @@ +{ + "icons": [ + { + "src": "icon.png", + "sizes": "192x192", + "type": "image/png" + } + ], + "start_url": "/" +} diff --git a/src/public/tile-wide.png b/public/tile-wide.png similarity index 100% rename from src/public/tile-wide.png rename to public/tile-wide.png diff --git a/src/public/tile.png b/public/tile.png similarity index 100% rename from src/public/tile.png rename to public/tile.png diff --git a/src/DOMUtils.js b/src/DOMUtils.js new file mode 100644 index 000000000..11c132f0d --- /dev/null +++ b/src/DOMUtils.js @@ -0,0 +1,38 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-present Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +export function updateTag(tagName, keyName, keyValue, attrName, attrValue) { + const node = document.head.querySelector( + `${tagName}[${keyName}="${keyValue}"]`, + ); + if (node && node.getAttribute(attrName) === attrValue) return; + + // Remove and create a new tag in order to make it work with bookmarks in Safari + if (node) { + node.parentNode.removeChild(node); + } + if (typeof attrValue === 'string') { + const nextNode = document.createElement(tagName); + nextNode.setAttribute(keyName, keyValue); + nextNode.setAttribute(attrName, attrValue); + document.head.appendChild(nextNode); + } +} + +export function updateMeta(name, content) { + updateTag('meta', 'name', name, 'content', content); +} + +export function updateCustomMeta(property, content) { + updateTag('meta', 'property', property, 'content', content); +} + +export function updateLink(rel, href) { + updateTag('link', 'rel', rel, 'href', href); +} diff --git a/src/actions/.gitignore b/src/actions/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/client.js b/src/client.js index aaaefb6a1..4b0a7b6fb 100644 --- a/src/client.js +++ b/src/client.js @@ -1,113 +1,167 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present Kriasoft, LLC. All rights reserved. * * This source code is licensed under the MIT license found in the * LICENSE.txt file in the root directory of this source tree. */ -import 'babel-polyfill'; +import 'whatwg-fetch'; +import React from 'react'; import ReactDOM from 'react-dom'; -import FastClick from 'fastclick'; -import Router from './routes'; -import Location from './core/Location'; -import { addEventListener, removeEventListener } from './core/DOMUtils'; +import deepForceUpdate from 'react-deep-force-update'; +import queryString from 'query-string'; +import { createPath } from 'history'; +import App from './components/App'; +import createFetch from './createFetch'; +import history from './history'; +import { updateMeta } from './DOMUtils'; +import router from './router'; -let cssContainer = document.getElementById('css'); -const appContainer = document.getElementById('app'); +// Enables critical path CSS rendering +// https://github.com/kriasoft/isomorphic-style-loader +const insertCss = (...styles) => { + // eslint-disable-next-line no-underscore-dangle + const removeCss = styles.map(x => x._insertCss()); + return () => { + removeCss.forEach(f => f()); + }; +}; + +// Global (context) variables that can be easily accessed from any React component +// https://facebook.github.io/react/docs/context.html const context = { - insertCss: styles => styles._insertCss(), - onSetTitle: value => (document.title = value), - onSetMeta: (name, content) => { - // Remove and create a new tag in order to make it work - // with bookmarks in Safari - const elements = document.getElementsByTagName('meta'); - Array.from(elements).forEach((element) => { - if (element.getAttribute('name') === name) { - element.parentNode.removeChild(element); - } - }); - const meta = document.createElement('meta'); - meta.setAttribute('name', name); - meta.setAttribute('content', content); - document - .getElementsByTagName('head')[0] - .appendChild(meta); - }, + // Universal HTTP client + fetch: createFetch(fetch, { + baseUrl: window.App.apiUrl, + }), }; -// Google Analytics tracking. Don't send 'pageview' event after the first -// rendering, as it was already sent by the Html component. -let trackPageview = () => (trackPageview = () => window.ga('send', 'pageview')); - -function render(state) { - Router.dispatch(state, (newState, component) => { - ReactDOM.render(component, appContainer, () => { - // Restore the scroll position if it was saved into the state - if (state.scrollY !== undefined) { - window.scrollTo(state.scrollX, state.scrollY); - } else { - window.scrollTo(0, 0); - } - - trackPageview(); - - // Remove the pre-rendered CSS because it's no longer used - // after the React app is launched - if (cssContainer) { - cssContainer.parentNode.removeChild(cssContainer); - cssContainer = null; - } - }); - }); -} +const container = document.getElementById('app'); +let currentLocation = history.location; +let appInstance; -function run() { - let currentLocation = null; - let currentState = null; - - // Make taps on links and buttons work fast on mobiles - FastClick.attach(document.body); - - // Re-render the app when window.location changes - const unlisten = Location.listen(location => { - currentLocation = location; - currentState = Object.assign({}, location.state, { - path: location.pathname, - query: location.query, - state: location.state, - context, - }); - render(currentState); - }); +const scrollPositionsHistory = {}; - // Save the page scroll position into the current location's state - const supportPageOffset = window.pageXOffset !== undefined; - const isCSS1Compat = ((document.compatMode || '') === 'CSS1Compat'); - const setPageOffset = () => { - currentLocation.state = currentLocation.state || Object.create(null); - if (supportPageOffset) { - currentLocation.state.scrollX = window.pageXOffset; - currentLocation.state.scrollY = window.pageYOffset; - } else { - currentLocation.state.scrollX = isCSS1Compat ? - document.documentElement.scrollLeft : document.body.scrollLeft; - currentLocation.state.scrollY = isCSS1Compat ? - document.documentElement.scrollTop : document.body.scrollTop; - } +// Re-render the app when window.location changes +async function onLocationChange(location, action) { + // Remember the latest scroll position for the previous location + scrollPositionsHistory[currentLocation.key] = { + scrollX: window.pageXOffset, + scrollY: window.pageYOffset, }; + // Delete stored scroll position for next page if any + if (action === 'PUSH') { + delete scrollPositionsHistory[location.key]; + } + currentLocation = location; - addEventListener(window, 'scroll', setPageOffset); - addEventListener(window, 'pagehide', () => { - removeEventListener(window, 'scroll', setPageOffset); - unlisten(); - }); + const isInitialRender = !action; + try { + context.pathname = location.pathname; + context.query = queryString.parse(location.search); + + // Traverses the list of routes in the order they are defined until + // it finds the first route that matches provided URL path string + // and whose action method returns anything other than `undefined`. + const route = await router.resolve(context); + + // Prevent multiple page renders during the routing process + if (currentLocation.key !== location.key) { + return; + } + + if (route.redirect) { + history.replace(route.redirect); + return; + } + + const renderReactApp = isInitialRender ? ReactDOM.hydrate : ReactDOM.render; + appInstance = renderReactApp( + + {route.component} + , + container, + () => { + if (isInitialRender) { + // Switch off the native scroll restoration behavior and handle it manually + // https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration + if (window.history && 'scrollRestoration' in window.history) { + window.history.scrollRestoration = 'manual'; + } + + const elem = document.getElementById('css'); + if (elem) elem.parentNode.removeChild(elem); + return; + } + + document.title = route.title; + + updateMeta('description', route.description); + // Update necessary tags in at runtime here, ie: + // updateMeta('keywords', route.keywords); + // updateCustomMeta('og:url', route.canonicalUrl); + // updateCustomMeta('og:image', route.imageUrl); + // updateLink('canonical', route.canonicalUrl); + // etc. + + let scrollX = 0; + let scrollY = 0; + const pos = scrollPositionsHistory[location.key]; + if (pos) { + scrollX = pos.scrollX; + scrollY = pos.scrollY; + } else { + const targetHash = location.hash.substr(1); + if (targetHash) { + const target = document.getElementById(targetHash); + if (target) { + scrollY = window.pageYOffset + target.getBoundingClientRect().top; + } + } + } + + // Restore the scroll position if it was saved into the state + // or scroll to the given #hash anchor + // or scroll to top of the page + window.scrollTo(scrollX, scrollY); + + // Google Analytics tracking. Don't send 'pageview' event after + // the initial rendering, as it was already sent + if (window.ga) { + window.ga('send', 'pageview', createPath(location)); + } + }, + ); + } catch (error) { + if (__DEV__) { + throw error; + } + + console.error(error); + + // Do a full page reload if error occurs during client-side navigation + if (!isInitialRender && currentLocation.key === location.key) { + console.error('RSK will reload your page after error'); + window.location.reload(); + } + } } -// Run the application when both DOM is ready and page content is loaded -if (['complete', 'loaded', 'interactive'].includes(document.readyState) && document.body) { - run(); -} else { - document.addEventListener('DOMContentLoaded', run, false); +// Handle client-side navigation by using HTML5 History API +// For more information visit https://github.com/mjackson/history#readme +history.listen(onLocationChange); +onLocationChange(currentLocation); + +// Enable Hot Module Replacement (HMR) +if (module.hot) { + module.hot.accept('./router', () => { + if (appInstance && appInstance.updater.isMounted(appInstance)) { + // Force-update the whole tree, including components that refuse to update + deepForceUpdate(appInstance); + } + + onLocationChange(currentLocation); + }); } diff --git a/src/components/App.js b/src/components/App.js new file mode 100644 index 000000000..2952f5ddf --- /dev/null +++ b/src/components/App.js @@ -0,0 +1,62 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-present Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import StyleContext from 'isomorphic-style-loader/StyleContext'; +import ApplicationContext from './ApplicationContext'; + +/** + * The top-level React component setting context (global) variables + * that can be accessed from all the child components. + * + * https://facebook.github.io/react/docs/context.html + * + * Usage example: + * + * const context = { + * history: createBrowserHistory(), + * store: createStore(), + * }; + * + * ReactDOM.render( + * {}}> + * + * + * + * , + * container, + * ); + */ + +export default function App({ context, insertCss, children }) { + // NOTE: If you need to add or modify header, footer etc. of the app, + // please do that inside the Layout component. + return ( + + + {React.Children.only(children)} + + + ); +} + +App.propTypes = { + // Enables critical path CSS rendering + // https://github.com/kriasoft/isomorphic-style-loader + insertCss: PropTypes.func.isRequired, + context: PropTypes.shape({ + // Universal HTTP client + fetch: PropTypes.func.isRequired, + pathname: PropTypes.string.isRequired, + query: PropTypes.object, + }).isRequired, + children: PropTypes.element.isRequired, +}; diff --git a/src/components/App/App.js b/src/components/App/App.js deleted file mode 100644 index 82fb058ba..000000000 --- a/src/components/App/App.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * React Starter Kit (https://www.reactstarterkit.com/) - * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE.txt file in the root directory of this source tree. - */ - -import React, { Component, PropTypes } from 'react'; -import emptyFunction from 'fbjs/lib/emptyFunction'; -import s from './App.scss'; -import Header from '../Header'; -import Feedback from '../Feedback'; -import Footer from '../Footer'; - -class App extends Component { - - static propTypes = { - context: PropTypes.shape({ - insertCss: PropTypes.func, - onSetTitle: PropTypes.func, - onSetMeta: PropTypes.func, - onPageNotFound: PropTypes.func, - }), - children: PropTypes.element.isRequired, - error: PropTypes.object, - }; - - static childContextTypes = { - insertCss: PropTypes.func.isRequired, - onSetTitle: PropTypes.func.isRequired, - onSetMeta: PropTypes.func.isRequired, - onPageNotFound: PropTypes.func.isRequired, - }; - - getChildContext() { - const context = this.props.context; - return { - insertCss: context.insertCss || emptyFunction, - onSetTitle: context.onSetTitle || emptyFunction, - onSetMeta: context.onSetMeta || emptyFunction, - onPageNotFound: context.onPageNotFound || emptyFunction, - }; - } - - componentWillMount() { - const { insertCss } = this.props.context; - this.removeCss = insertCss(s); - } - - componentWillUnmount() { - this.removeCss(); - } - - render() { - return !this.props.error ? ( -
-
- {this.props.children} - -
-
- ) : this.props.children; - } - -} - -export default App; diff --git a/src/components/App/__tests__/App-test.js b/src/components/App/__tests__/App-test.js deleted file mode 100644 index 9bf856893..000000000 --- a/src/components/App/__tests__/App-test.js +++ /dev/null @@ -1,16 +0,0 @@ -jest.unmock('../App'); - -import App from '../App'; -import React from 'react'; -import { shallow } from 'enzyme'; - -describe('App', () => { - it('renders children correctly', () => { - const wrapper = shallow( - {} }}> -
- - ); - expect(wrapper.contains(
)).toBe(true); - }); -}); diff --git a/src/components/ApplicationContext.js b/src/components/ApplicationContext.js new file mode 100644 index 000000000..beb4844f5 --- /dev/null +++ b/src/components/ApplicationContext.js @@ -0,0 +1,9 @@ +import React from 'react'; + +const ApplicationContext = React.createContext({ + fetch: () => { + throw new Error('Fetch method not initialized.'); + }, +}); + +export default ApplicationContext; diff --git a/src/components/ContentPage/ContentPage.js b/src/components/ContentPage/ContentPage.js deleted file mode 100644 index a32d6a771..000000000 --- a/src/components/ContentPage/ContentPage.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * React Starter Kit (https://www.reactstarterkit.com/) - * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE.txt file in the root directory of this source tree. - */ - -import React, { Component, PropTypes } from 'react'; -import withStyles from 'isomorphic-style-loader/lib/withStyles'; -import s from './ContentPage.scss'; - -class ContentPage extends Component { - - static propTypes = { - path: PropTypes.string.isRequired, - content: PropTypes.string.isRequired, - title: PropTypes.string, - }; - - static contextTypes = { - onSetTitle: PropTypes.func.isRequired, - }; - - componentWillMount() { - this.context.onSetTitle(this.props.title); - } - - render() { - return ( -
-
- {this.props.path === '/' ? null :

{this.props.title}

} -
-
-
- ); - } - -} - -export default withStyles(ContentPage, s); diff --git a/src/components/ContentPage/package.json b/src/components/ContentPage/package.json deleted file mode 100644 index 26aa4558a..000000000 --- a/src/components/ContentPage/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "ContentPage", - "version": "0.0.0", - "private": true, - "main": "./ContentPage.js" -} diff --git a/src/components/ErrorPage/ErrorPage.js b/src/components/ErrorPage/ErrorPage.js deleted file mode 100644 index ffb07a5ed..000000000 --- a/src/components/ErrorPage/ErrorPage.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * React Starter Kit (https://www.reactstarterkit.com/) - * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE.txt file in the root directory of this source tree. - */ - -import React, { Component, PropTypes } from 'react'; -import withStyles from 'isomorphic-style-loader/lib/withStyles'; -import s from './ErrorPage.scss'; - -const title = 'Error'; - -class ErrorPage extends Component { - - static contextTypes = { - onSetTitle: PropTypes.func.isRequired, - onPageNotFound: PropTypes.func.isRequired, - }; - - componentWillMount() { - this.context.onSetTitle(title); - } - - render() { - return ( -
-

{title}

-

Sorry, an critical error occurred on this page.

-
- ); - } - -} - -export default withStyles(ErrorPage, s); diff --git a/src/components/ErrorPage/ErrorPage.scss b/src/components/ErrorPage/ErrorPage.scss deleted file mode 100644 index 9764560c9..000000000 --- a/src/components/ErrorPage/ErrorPage.scss +++ /dev/null @@ -1,54 +0,0 @@ -/** - * React Starter Kit (https://www.reactstarterkit.com/) - * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE.txt file in the root directory of this source tree. - */ - -* { - margin: 0; - line-height: 1.2; -} - -html { - display: table; - width: 100%; - height: 100%; - color: #888; - text-align: center; - font-family: sans-serif; -} - -body { - display: table-cell; - margin: 2em auto; - vertical-align: middle; -} - -h1 { - color: #555; - font-weight: 400; - font-size: 2em; -} - -p { - margin: 0 auto; - width: 280px; -} - -@media only screen and (max-width: 280px) { - - body, - p { - width: 95%; - } - - h1 { - font-size: 1.5em; - margin: 0 0 0.3em; - - } - -} diff --git a/src/components/ErrorPage/package.json b/src/components/ErrorPage/package.json deleted file mode 100644 index 41d6d9a4b..000000000 --- a/src/components/ErrorPage/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "ErrorPage", - "version": "0.0.0", - "private": true, - "main": "./ErrorPage.js" -} diff --git a/src/components/Feedback/Feedback.scss b/src/components/Feedback/Feedback.css similarity index 80% rename from src/components/Feedback/Feedback.scss rename to src/components/Feedback/Feedback.css index 9b9ffcb0c..e67a9eb3e 100644 --- a/src/components/Feedback/Feedback.scss +++ b/src/components/Feedback/Feedback.css @@ -1,13 +1,13 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present Kriasoft, LLC. All rights reserved. * * This source code is licensed under the MIT license found in the * LICENSE.txt file in the root directory of this source tree. */ -@import 'http://github.com/variables.scss'; +@import 'http://github.com/variables.css'; .root { background: #f5f5f5; @@ -17,7 +17,7 @@ .container { margin: 0 auto; padding: 20px 8px; - max-width: $max-content-width; + max-width: var(--max-content-width); text-align: center; font-size: 1.5em; /* ~24px */ } diff --git a/src/components/Feedback/Feedback.js b/src/components/Feedback/Feedback.js index 3688c506e..de21a8c4d 100644 --- a/src/components/Feedback/Feedback.js +++ b/src/components/Feedback/Feedback.js @@ -1,32 +1,35 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present Kriasoft, LLC. All rights reserved. * * This source code is licensed under the MIT license found in the * LICENSE.txt file in the root directory of this source tree. */ +import useStyles from 'isomorphic-style-loader/useStyles'; import React from 'react'; -import withStyles from 'isomorphic-style-loader/lib/withStyles'; -import s from './Feedback.scss'; +import s from './Feedback.css'; -function Feedback() { +export default function Feedback() { + useStyles(s); return (
Ask a question + > + Ask a question + | Report an issue + > + Report an issue +
); } - -export default withStyles(Feedback, s); diff --git a/src/components/Footer/Footer.scss b/src/components/Footer/Footer.css similarity index 76% rename from src/components/Footer/Footer.scss rename to src/components/Footer/Footer.css index d6fe535ee..3297798db 100644 --- a/src/components/Footer/Footer.scss +++ b/src/components/Footer/Footer.css @@ -1,13 +1,13 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present Kriasoft, LLC. All rights reserved. * * This source code is licensed under the MIT license found in the * LICENSE.txt file in the root directory of this source tree. */ -@import 'http://github.com/variables.scss'; +@import 'http://github.com/variables.css'; .root { background: #333; @@ -17,7 +17,7 @@ .container { margin: 0 auto; padding: 20px 15px; - max-width: $max-content-width; + max-width: var(--max-content-width); text-align: center; } @@ -25,11 +25,6 @@ color: rgba(255, 255, 255, 0.5); } -.textMuted { - composes: text; - color: rgba(255, 255, 255, 0.3); -} - .spacer { color: rgba(255, 255, 255, 0.3); } diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.js index b1a680d5e..5062f56d5 100644 --- a/src/components/Footer/Footer.js +++ b/src/components/Footer/Footer.js @@ -1,31 +1,41 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present Kriasoft, LLC. All rights reserved. * * This source code is licensed under the MIT license found in the * LICENSE.txt file in the root directory of this source tree. */ +import useStyles from 'isomorphic-style-loader/useStyles'; import React from 'react'; -import withStyles from 'isomorphic-style-loader/lib/withStyles'; -import s from './Footer.scss'; +import s from './Footer.css'; import Link from '../Link'; -function Footer() { +export default function Footer() { + useStyles(s); + return (
© Your Company · - Home + + Home + + · + + Admin + · - Privacy + + Privacy + · - Not Found + + Not Found +
); } - -export default withStyles(Footer, s); diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.css similarity index 73% rename from src/components/Header/Header.scss rename to src/components/Header/Header.css index 34dce84b0..6d878562d 100644 --- a/src/components/Header/Header.scss +++ b/src/components/Header/Header.css @@ -1,15 +1,17 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present Kriasoft, LLC. All rights reserved. * * This source code is licensed under the MIT license found in the * LICENSE.txt file in the root directory of this source tree. */ -@import 'http://github.com/variables.scss'; +@import 'http://github.com/variables.css'; -$brand-color: #61dafb; +:root { + --brand-color: #61dafb; +} .root { background: #373277; @@ -19,11 +21,11 @@ $brand-color: #61dafb; .container { margin: 0 auto; padding: 20px 0; - max-width: $max-content-width; + max-width: var(--max-content-width); } .brand { - color: color($brand-color lightness(+10%)); + color: color(var(--brand-color) lightness(+10%)); text-decoration: none; font-size: 1.75em; /* ~28px */ } @@ -32,11 +34,6 @@ $brand-color: #61dafb; margin-left: 10px; } -.nav { - float: right; - margin-top: 6px; -} - .banner { text-align: center; } diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index 7b337e52b..9750bcc3e 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -1,25 +1,34 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present Kriasoft, LLC. All rights reserved. * * This source code is licensed under the MIT license found in the * LICENSE.txt file in the root directory of this source tree. */ +import useStyles from 'isomorphic-style-loader/useStyles'; import React from 'react'; -import withStyles from 'isomorphic-style-loader/lib/withStyles'; -import s from './Header.scss'; +import s from './Header.css'; import Link from '../Link'; import Navigation from '../Navigation'; +import logoUrl from './logo-small.png'; +import logoUrl2x from './logo-small@2x.png'; -function Header() { +export default function Header() { + useStyles(s); return (
- + - React + React Your Company
@@ -30,5 +39,3 @@ function Header() {
); } - -export default withStyles(Header, s); diff --git a/src/components/Html.js b/src/components/Html.js new file mode 100644 index 000000000..fb23d700b --- /dev/null +++ b/src/components/Html.js @@ -0,0 +1,92 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-present Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import serialize from 'serialize-javascript'; +import config from '../config'; + +/* eslint-disable react/no-danger */ + +export default function Html({ + title, + description, + styles, + scripts, + app, + children, +}) { + return ( + + + + + {title} + + + {scripts.map(script => ( + + ))} + + + {styles.map(style => ( +