Skip to content

Commit

Permalink
docs(project): add project docs (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
kkemple committed Sep 25, 2017
1 parent 1f9e428 commit 21a9caf
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 20 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1 +1,2 @@
**/node_modules/**
packages/universal-components/storybook-static
45 changes: 43 additions & 2 deletions README.md
@@ -1,2 +1,43 @@
# universal-components-example
An example setup for universal components
# Universal Components Example
An example setup for building/testing/deploying universal components with React

## Folder Structure
This project uses [`Lerna`](https://github.com/lerna/lerna) to make managing multiple NPM packages easier. Often times you will end up with other packages related to your universal components package and using Lerna makes managing the inter-dependencies easier.

- `packages`: This directory holds the actual NPM packages you want to publish via Lerna (currently only the [universal-components](./packages/universal-components) package.
- `examples`: This directory holds a web and a native example app.

## Available Scripts

### lerna bootstrap
Hoists shared dependencies from any packges in `packages` directory. Also creates symlinks for inter-dependent packages in `packages` directory

### lerna publish
Publishes new tags and package versions for any packages in `packages` directory that have changed since last publish

## Running the Examples

### Web

This app was created using `create-react-app`. And although CRA supports using `react-native-web` out of the box, we need to eject so that we can [update Webpack config](./examples/web/config/webpack.config.dev.js) to also parse the universal components package (we must also do the same in [prod Wepback config](./examples/web/config/webpack.config.prod.js)).

You can see the universal component being included [here](./examples/web/src/App.js).

```
cd examples/web
yarn install
yarn start
```

### Native

This app was created using `create-react-native-app`. There were no special setup steps required.

You can see the universal component being included [here](./examples/native/App.js).

```
cd examples/native
yarn install
yarn start
```

1 change: 0 additions & 1 deletion examples/web/src/App.js
@@ -1,7 +1,6 @@
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

import { Button } from '@kkemple/universal-components';

class App extends Component {
Expand Down
3 changes: 3 additions & 0 deletions packages/universal-components/.flowconfig
Expand Up @@ -7,3 +7,6 @@

[libs]
flow-typed

[options]
module.name_mapper='\(react-native\)' -> 'react-native-web'
52 changes: 52 additions & 0 deletions packages/universal-components/.storybook/webpack.config.js
@@ -0,0 +1,52 @@
const path = require('path');
const webpack = require('webpack');
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
const MinifyPlugin = require('babel-minify-webpack-plugin');

const genDefaultConfig = require('@storybook/react/dist/server/config/defaults/webpack.config.js');

const DEV = process.env.NODE_ENV !== 'production';

const prodPlugins = [
new webpack.DefinePlugin({
// prettier-ignore
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.__REACT_NATIVE_DEBUG_ENABLED__': DEV,
}),
new webpack.optimize.OccurrenceOrderPlugin(),
new MinifyPlugin(),
];

module.exports = (baseConfig, env) => {
const config = genDefaultConfig(baseConfig, env);

const defaultPlugins = config.plugins.concat([
new FriendlyErrorsWebpackPlugin(),
]);

const overwrite = {
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: { cacheDirectory: true },
},
{
test: /\.css$/,
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
},
{
test: /\.(gif|jpe?g|png|svg|otf|ttf)$/,
loader: 'url-loader',
query: { name: '[name].[ext]' },
},
],
},
plugins: DEV ? defaultPlugins : prodPlugins,
};

return Object.assign(config, overwrite);
};
130 changes: 130 additions & 0 deletions packages/universal-components/README.md
@@ -0,0 +1,130 @@
# Universal Components Package

## What are universal components?

Universal components are React components that can render anywhere. They are platform agnostic, which means you can use them on the web and native without changing any lines of code.

Why architect your components this way? The main benefit is code reuse. By building your components with React Native primitives, you only have to write them once. Since other platforms such as VR & Sketch also share RN primitives, you can eventually expand universal components to include those platforms as well (although some issues such as cross-platform SVGs are still getting ironed out).

To achieve a fully universal component library, we use [react-native-web](https://github.com/necolas/react-native-web). RNW sits as a layer on top of DOM primitives; for example, `<View />` will render to a `<div/>` on the web. RNW has nearly complete feature parity with RN, so you can be confident that almost any code that works with RN will also work with RNW. Check the [RNW explorer](https://necolas.github.io/react-native-web/storybook/) for a comprehensive list of supported components & APIs.

## Workflow

Components can be developed in isolation with [Storybook](https://storybook.js.org/) to eliminate differences between projects. This project uses the web version of Storybook to make sure that all universal components will render on the web.

## Folder structure:

- `components/`: Where all your universal components live
- `.storybook`: Storybook specific configuration
- `storybook-static`: Where storybook exports builds to
- `flow-typed/`: [Shared Flow types](https://github.com/flowtype/flow-typed)

Components are exported in `index.js` and published to an NPM package (`@<YOUR_NPM_USERNAME>/universal-components`) so they can be consumed by multiple projects. In order to coordinate publishing multiple packages in this repo, you use [Lerna](https://lernajs.io/). Run `lerna publish` from root of project to publish a version of this package. Then, you can upgrade the package within the application (`yarn upgrade @<YOUR_NPM_USERNAME>/universal-components`) and import the components to use them.

## Available Scripts

### yarn storybook
Starts up storybook for development of components

### yarn build
Builds the production version of storybook

### yarn deploy
Deploys the production build of storybook to [`surge.sh`](https://surge.sh)

## Styling

For styling, the `StyleSheet` API is included in RN so you can write your styles as JS objects. RNW supports the `StyleSheet` API via CSS modules. Don't be alarmed if you open up dev tools and see a huge string of ugly looking class names attached to your DOM elements - this is RNW's way of memoizing styles for fast performance compared to other CSS in JS solutions.

Since the RNW styling API mirrors RN's, it follows the default styling properties of [Yoga](https://facebook.github.io/yoga/docs/learn-more/), Facebook's cross-platform layout engine. The main point to remember about Yoga is that Flexbox is enabled by default with `flexDirection: column`.

## Platform specific code

Sometimes, you might need to account for slight differences between platforms in your components. You can either use the Platform API or platform extensions to accomplish this.

### Platform API: Good for styling one-offs
```javascript
import { Platform } from 'react-native';

switch (Platform.OS) {
case 'ios':
case 'android':
// do something on native
case 'web':
default:
// do something on web
}
```

You can also write it like this:
```javascript
import { Platform } from 'react-native';

const styles = StyleSheet.create({
container: {
flex: 1,
...Platform.select({
ios: {
backgroundColor: 'red',
},
android: {
backgroundColor: 'blue',
},
}),
},
});
```

### Platform extensions: Good for differences in component implementation

```javascript
import { Button } from '../components/button'
// will resolve from '../components/button/index.web.js' on web, '../components/button/index.ios.js' on iOS, etc
```

This functionality is built into the React Native packager. We've also added the ability to resolve platform extensions on web in the babel config (see [`.babelrc`](./.babelrc) for an example).


## Storybook configuration
Storybook configuration can be found in [`./.storybook`](./.storybook). The two key parts to notice are:
- Stories live next to components ([see example](./components/button)) and we load them via `./.storybook/config.js`
- Because we use babel-minify over UglifyJS for minifying we have to use full control mode for Webpack `./.storybook/webpack.config.js`

## How to use React Native modules on the web

⚠️ Warning: This is slightly hacky, but if you should do it anyway because standardizing third party modules & component libraries across platforms is important.

Some examples of RN libraries that can be used on the web include `victory-native`, `react-native-calendars`, and `react-native-vector-icons`. We're able to use these because of RNW's high feature parity with RN, the ability to transpile, and alias with Webpack or Babel.

### Steps:

1. After you install the library, you will need to transpile it with Babel because all RN modules are ES6. To do this, add the module name to the RegExp in `babel-loader` in `.storybook/webpack.config.js`:
```javascript
{
test: /\.js$/,
exclude: /node_modules\/(?!svgs)(?!MODULE_NAME_HERE)/,
loader: 'babel-loader',
query: { cacheDirectory: true },
},
```
2. In some cases, you'll need to create an alias in the Babel config. For example, you alias `svgs` to `react-native-svg` for your cross-platform svg implementation. You'll almost always be aliasing a web library's name to its native counterpart because aliasing is currently harder to execute with the RN packager, [Metro](https://github.com/facebook/metro-bundler).

Add the alias to `"module-resolver"` plugin in `.babelrc` like this:
```javascript
"alias": {
"react-native": "react-native-web",
"react-native-svg": "svgs",
```
3. If you're aliasing, you will need to tell Flow how to resolve the module. Add the module name mapper to `.flowconfig`:
```
module.name_mapper='\(react-native\)' -> 'react-native-web'
module.name_mapper='\(react-native-svg\)' -> 'svgs'
```
5. If you've made it this far and everything is working, celebrate! 🎉 You can now import your components normally. If you're getting cryptic errors, you might want to reach out to the maintainer of the library. Usually, they're more than happy to offer their help with supporting web. 😊
## Resources
- [React as a Platform](https://www.youtube.com/watch?v=hNwQPJy-XZY)
- [Write Once, Render Anywhere](http://reactnyc-universal-components.surge.sh/#/)
- [The Road to Universal Components](https://labs.mlssoccer.com/the-road-to-universal-components-at-major-league-soccer-eeb7aac27e6c)
@@ -1,18 +1,17 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { View, Text, StyleSheet } from 'react-native';

import Button from '../';

storiesOf('Shared Components', module).add('Button', () => {
storiesOf('Universal Components', module).add('Button', () => {
return (
<View style={styles.container}>
<Text style={styles.title}>Button</Text>
<View style={styles.example}>
<Text style={styles.exampleTitle}>Example</Text>
<View style={styles.exampleWrapper}>
<Button text="Press Me!" onPress={action('Button Pressed!')} />
<Button text="Press Me!" onPress={() => alert('Button Pressed!')} />
</View>
</View>
</View>
Expand Down
16 changes: 4 additions & 12 deletions packages/universal-components/package.json
Expand Up @@ -14,29 +14,20 @@
"type": "git",
"url": "git+https://github.com/kkemple/universal-components-example.git"
},
"keywords": [
"universal",
"components",
"react",
"react-native",
"react-vr"
],
"keywords": ["universal", "components", "react", "react-native", "react-vr"],
"author": "Kurtis Kemple <kurtiskemple@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/kkemple/universal-components-example/issues"
},
"homepage": "https://github.com/kkemple/universal-components-example#readme",
"jest": {
"moduleFileExtensions": [
"web.js",
"js"
]
"moduleFileExtensions": ["web.js", "js"]
},
"devDependencies": {
"@storybook/addon-actions": "^3.2.10",
"@storybook/addon-options": "^3.2.10",
"@storybook/react": "^3.2.10",
"babel-minify-webpack-plugin": "^0.2.0",
"babel-plugin-module-resolver": "^2.7.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
Expand All @@ -47,6 +38,7 @@
"enzyme-to-json": "^2.0.0",
"flow-bin": "^0.55.0",
"flow-typed": "^2.1.5",
"friendly-errors-webpack-plugin": "^1.6.1",
"jest": "^21.1.0",
"prop-types": "^15.5.10",
"react": "^15.6.1",
Expand Down
36 changes: 34 additions & 2 deletions packages/universal-components/yarn.lock
Expand Up @@ -469,7 +469,7 @@ babel-code-frame@^6.11.0, babel-code-frame@^6.26.0:
esutils "^2.0.2"
js-tokens "^3.0.2"

babel-core@^6.0.0, babel-core@^6.26.0:
babel-core@^6.0.0, babel-core@^6.24.1, babel-core@^6.26.0:
version "6.26.0"
resolved "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8"
dependencies:
Expand Down Expand Up @@ -681,6 +681,14 @@ babel-messages@^6.23.0:
dependencies:
babel-runtime "^6.22.0"

babel-minify-webpack-plugin@^0.2.0:
version "0.2.0"
resolved "https://registry.npmjs.org/babel-minify-webpack-plugin/-/babel-minify-webpack-plugin-0.2.0.tgz#ef9694d11a1b8ab8f3204d89f5c9278dd28fc2a9"
dependencies:
babel-core "^6.24.1"
babel-preset-minify "^0.2.0"
webpack-sources "^1.0.1"

babel-plugin-check-es2015-constants@^6.22.0:
version "6.22.0"
resolved "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a"
Expand Down Expand Up @@ -2443,6 +2451,12 @@ error-ex@^1.2.0:
dependencies:
is-arrayish "^0.2.1"

error-stack-parser@^2.0.0:
version "2.0.1"
resolved "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.1.tgz#a3202b8fb03114aa9b40a0e3669e48b2b65a010a"
dependencies:
stackframe "^1.0.3"

es-abstract@^1.4.3, es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0, es-abstract@^1.8.2:
version "1.8.2"
resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.8.2.tgz#25103263dc4decbda60e0c737ca32313518027ee"
Expand Down Expand Up @@ -2889,6 +2903,14 @@ fresh@0.5.1:
version "0.5.1"
resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.1.tgz#c3a08bcec0fcdcc223edf3b23eb327f1f9fcbf5c"

friendly-errors-webpack-plugin@^1.6.1:
version "1.6.1"
resolved "https://registry.npmjs.org/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.6.1.tgz#e32781c4722f546a06a9b5d7a7cfa28520375d70"
dependencies:
chalk "^1.1.3"
error-stack-parser "^2.0.0"
string-length "^1.0.1"

fs-extra@^4.0.0:
version "4.0.2"
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.2.tgz#f91704c53d1b461f893452b0c307d9997647ab6b"
Expand Down Expand Up @@ -5436,7 +5458,7 @@ react-modal@^2.2.4:
prop-types "^15.5.10"
react-dom-factories "^1.0.0"

react-native-web@^0.0.129:
react-native-web@>=0.0.129:
version "0.0.129"
resolved "https://registry.npmjs.org/react-native-web/-/react-native-web-0.0.129.tgz#76f010e1507d4bee89f0069db9955c20ea93fa2c"
dependencies:
Expand Down Expand Up @@ -6050,6 +6072,10 @@ stack-trace@0.0.x:
version "0.0.10"
resolved "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"

stackframe@^1.0.3:
version "1.0.4"
resolved "https://registry.npmjs.org/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b"

"statuses@>= 1.3.1 < 2", statuses@~1.3.1:
version "1.3.1"
resolved "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
Expand Down Expand Up @@ -6079,6 +6105,12 @@ strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"

string-length@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac"
dependencies:
strip-ansi "^3.0.0"

string-length@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
Expand Down

0 comments on commit 21a9caf

Please sign in to comment.