├── .gitignore ├── .npmignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── dist ├── index.d.ts ├── index.js └── template │ ├── .env │ ├── .eslintrc.js │ ├── .prettierrc.js │ ├── babel.config.js │ ├── config-overrides.js │ ├── index.js │ ├── jest.config.js │ ├── src │ ├── AbstractionExample.tsx │ ├── AbstractionExample.web.css │ ├── AbstractionExample.web.tsx │ ├── App.tsx │ ├── index.css │ ├── index.ts │ ├── index.web.ts │ └── react-app-env.d.ts │ └── tsconfig.json ├── package.json ├── src └── index.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | node_modules/* 3 | yarn-error-log* 4 | .vscode 5 | .idea 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | useTabs: false, 7 | printWidth: 80, 8 | jsxBracketSameLine: false, 9 | }; 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 webRidge Design 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Demonstration of result of the app](http://img.youtube.com/vi/Pslo1WRpZz0/0.jpg)](https://www.youtube.com/watch?v=Pslo1WRpZz0 'Demonstration video') 2 | 3 | This package will help you in creating a React-Native app which runs on the web with React Native Web (used in the Twitter webapp: https://github.com/necolas/react-native-web) while using the best tools of both worlds. You'll always be using the latest version of both libraries. This program only merges some configs to give you a fast start and uses Typescript by default :) 4 | 5 | This package will help you in creating a React-Native app which runs on the web with React Native Web while using the best tools of both worlds so you can have one codebase for Android, iOS and Web. 6 | 7 | With react-native-web you can share more than 90% of your app between Android, iOS and web. But you'll need to create some abstractions for some packages. 8 | 9 | Used library in background for web: https://create-react-app.dev/ 10 | Used library for React-Native: React Native CLI https://reactnative.dev/docs/environment-setup 11 | 12 | ## Getting started 13 | 14 | You need to have React Native installed :) (Not Expo) 15 | You need to have yarn installed (feel free to make this configurable in a PR) 16 | Follow instructions on and click the 'React Native CLI Quickstart' 17 | https://reactnative.dev/docs/environment-setup 18 | 19 | And then you need to run this command (myapp can be something you desire) 20 | 21 | ``` 22 | npx create-react-native-web-application --name myappname 23 | ``` 24 | 25 | ## Commands 26 | 27 | ### Native commands 28 | 29 | ``` 30 | yarn android 31 | yarn ios 32 | yarn start 33 | yarn test 34 | yarn lint 35 | ``` 36 | 37 | ### Web commands 38 | 39 | ``` 40 | yarn web 41 | yarn web:build 42 | yarn web:test 43 | yarn web:eject 44 | ``` 45 | 46 | ## VSCode 47 | VSCode has some problems with the new create-react-app JSX transform. 48 | 49 | Set Typescript of editor to use workspace version ctrl | cmd + shift + p and type "Typescript" - select typescript version uses workspace 50 | 51 | 52 | ## Tips 53 | 54 | - Look up React Native Docs 55 | - Look up https://github.com/necolas/react-native-web 56 | - Enable Service Worker for an App-like experience on web https://create-react-app.dev/docs/making-a-progressive-web-app 57 | - Look for web support in React Native packages 58 | - Install the Prettier extension in Visual Code 59 | - Enable Hermes in build.gradle since it will give you a ~ 30% faster app on Android 60 | 61 | ``` 62 | // build.gradle in your Android folder 63 | project.ext.react = [ 64 | enableHermes: false, // clean and rebuild if changing 65 | ] 66 | ``` 67 | 68 | to 69 | 70 | ``` 71 | // build.gradle in your Android folder 72 | project.ext.react = [ 73 | enableHermes: true, // clean and rebuild if changing 74 | ] 75 | ``` 76 | 77 | ## Install React Native Web packages which support web 78 | 79 | You can add extra packages in `config-overrides.js` in the babelInclude plugin so react native packages will be compiled with babel. 80 | 81 | ## Install React Native Web packages which do not support web 82 | 83 | We can almost share a lot of things but when a package does support the web you will need to create an abstraction and convert an interface to another package which does the same thing but for React JS or create your own abstraction. 84 | 85 | Create a file package-name.ts 86 | 87 | ```typescript 88 | import NativeModule from 'react-native-module-without-web-support'; 89 | export default NativeModule; 90 | ``` 91 | 92 | Create a file package-name.web.ts 93 | 94 | ```typescript 95 | import React from 'react'; 96 | export function someLibraryFunc() { 97 | return webimplementation; 98 | } 99 | export default function YourImplemenation() {} 100 | ``` 101 | 102 | ## Updates or supported libraries 103 | 104 | We will publish more open-source in the fute also a cross platform abstraction for React Native Navigation so consider following us on Github or Twitter :-) 105 | 106 | https://twitter.com/web_ridge 107 | https://twitter.com/RichardLindhout 108 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | export {}; 3 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | var __importDefault = (this && this.__importDefault) || function (mod) { 4 | return (mod && mod.__esModule) ? mod : { "default": mod }; 5 | }; 6 | Object.defineProperty(exports, "__esModule", { value: true }); 7 | const LogColor = '\x1b[32m'; 8 | const cross_spawn_1 = __importDefault(require("cross-spawn")); 9 | const fs_extra_1 = __importDefault(require("fs-extra")); 10 | const path_1 = __importDefault(require("path")); 11 | const yargs_1 = __importDefault(require("yargs")); 12 | const argv = yargs_1.default 13 | .help() 14 | .option('name', { 15 | alias: 'n', 16 | description: 'Name of the app', 17 | type: 'string', 18 | }) 19 | .alias('help', 'h').argv; 20 | if (!argv.name) { 21 | console.log('You should specify the name of the app with --name'); 22 | process.exit(); 23 | } 24 | // run the app ;) 25 | app(); 26 | async function app() { 27 | const appName = argv.name; 28 | const appNameWeb = appName + '-web-will-be-deleted-afterwards'; 29 | logSpaced(` 30 | Creating ${appName}, brought to you by webRidge. 31 | 32 | Please wait till everything is finished :) 33 | 34 | `); 35 | try { 36 | await Promise.all([ 37 | createReactNativeApp(appName), 38 | createReactScriptsApp(appNameWeb), 39 | ]); 40 | } 41 | catch (error) { 42 | console.log('Could not create React Native project', { error }); 43 | } 44 | logSpaced("Created two projects in two directories. Let's merge them to one project ;)"); 45 | const webPackagePath = appNameWeb + '/package.json'; 46 | const webPackageFile = fs_extra_1.default.readFileSync(webPackagePath, 'utf8'); 47 | const webPackageJSON = JSON.parse(webPackageFile); 48 | const removePackages = ['web-vitals']; 49 | const webDependencies = Object.keys(webPackageJSON.dependencies) 50 | .filter((packageName) => !removePackages.includes(packageName)) 51 | .map((packageName) => ({ 52 | name: packageName, 53 | version: webPackageJSON.dependencies[packageName], 54 | isDev: packageName.includes('@testing-library'), 55 | })); 56 | const reactNativePackagePath = appName + '/package.json'; 57 | const reactNativePackageFile = fs_extra_1.default.readFileSync(reactNativePackagePath, 'utf8'); 58 | const reactNativePackageJSON = JSON.parse(reactNativePackageFile); 59 | let webScripts = replaceValuesOfObject(prefixObject(webPackageJSON.scripts, 'web:'), 'react-scripts', 'react-app-rewired'); 60 | // more like yarn android, yarn ios, yarn web 61 | //@ts-ignore 62 | let webStartCommand = webScripts['web:start']; 63 | delete webScripts['web:start']; 64 | //@ts-ignore 65 | webScripts.web = webStartCommand; 66 | // console.log({ webScripts }); 67 | const mergedPackageJSON = { 68 | ...reactNativePackageJSON, 69 | // we're gonna merge scripts and dependencies ourself :) 70 | ...excludeObjectKeys(webPackageJSON, ['dependencies', 'scripts', 'name']), 71 | scripts: { 72 | ...reactNativePackageJSON.scripts, 73 | ...webScripts, 74 | }, 75 | }; 76 | // write merged package.json down 77 | fs_extra_1.default.writeFileSync(reactNativePackagePath, JSON.stringify(mergedPackageJSON)); 78 | // install web packages to native project 79 | await installPackages([ 80 | ...webDependencies, 81 | { name: 'react-native-web' }, 82 | { name: 'react-app-rewired', isDev: true }, 83 | { name: 'customize-cra', isDev: true }, 84 | { name: 'typescript', isDev: true }, 85 | { name: '@types/react-native', isDev: true }, 86 | { name: '@types/react', isDev: true }, 87 | { name: 'babel-plugin-import', isDev: true }, 88 | ], appName); 89 | // copy template files 90 | const templateDir = path_1.default.dirname(require.main.filename) + '/template'; 91 | logSpaced({ templateDir }); 92 | fs_extra_1.default.copySync(templateDir, appName); 93 | fs_extra_1.default.copySync(appNameWeb + '/public', appName + '/public'); 94 | fs_extra_1.default.unlinkSync(appName + '/App.js'); 95 | fs_extra_1.default.removeSync(appNameWeb); 96 | logSpaced("Yeah!! We're done!"); 97 | logSpaced(` 98 | Start your app with by going to the created directory: 'cd ${appName}' 99 | 100 | yarn android 101 | npx pod-install && yarn ios 102 | yarn web 103 | 104 | If you have an import error on App.tsx restart your app, it's a cache issue. 105 | If you have red errors in VSCode read the README.md about this issue. 106 | `); 107 | } 108 | async function installPackages(packages, directory) { 109 | await installPackagesAdvanced(packages.filter((p) => p.isDev === true), directory, true); 110 | await installPackagesAdvanced(packages.filter((p) => !p.isDev), directory, false); 111 | } 112 | async function installPackagesAdvanced(packages, directory, dev) { 113 | return new Promise((resolve, reject) => { 114 | const joinedPackages = packages.map((p) => p.name + (p.version ? `@${p.version}` : ``)); 115 | // console.log({ joinedPackages }); 116 | const createReactNativeProcess = cross_spawn_1.default('yarn', [ 117 | '--cwd', 118 | directory, 119 | 'add', 120 | ...joinedPackages, 121 | dev ? '--dev' : undefined, 122 | ].filter((n) => !!n), { stdio: 'inherit', shell: true }); 123 | createReactNativeProcess.on('error', function (error) { 124 | reject(error); 125 | }); 126 | createReactNativeProcess.on('exit', function (response) { 127 | resolve(response); 128 | }); 129 | }); 130 | } 131 | async function createReactNativeApp(appName) { 132 | return new Promise((resolve, reject) => { 133 | const createReactNativeProcess = cross_spawn_1.default('npx', ['react-native', 'init', appName], { stdio: 'inherit', shell: true }); 134 | createReactNativeProcess.on('error', function (error) { 135 | reject(error); 136 | }); 137 | createReactNativeProcess.on('exit', function (response) { 138 | resolve(response); 139 | }); 140 | }); 141 | } 142 | async function createReactScriptsApp(appName) { 143 | return new Promise(function (resolve, reject) { 144 | const createReactNativeProcess = cross_spawn_1.default('npx', ['create-react-app', appName], { stdio: 'inherit', shell: true }); 145 | createReactNativeProcess.on('error', function (error) { 146 | reject(error); 147 | }); 148 | createReactNativeProcess.on('exit', function (response) { 149 | resolve(response); 150 | }); 151 | }); 152 | } 153 | function logSpaced(args) { 154 | console.log(''); 155 | console.log(LogColor, args); 156 | console.log(''); 157 | } 158 | function excludeObjectKeys(object, ignoredKeys) { 159 | let newObject = { ...object }; 160 | ignoredKeys.forEach(function (key) { 161 | delete newObject[key]; 162 | }); 163 | return newObject; 164 | } 165 | function replaceValuesOfObject(object, search, replace) { 166 | let newObject = {}; 167 | Object.keys(object).forEach((key) => { 168 | // console.log({ key }); 169 | const value = object[key]; 170 | // console.log({ value }); 171 | if (value) { 172 | newObject[key] = value.replace ? value.replace(search, replace) : value; 173 | } 174 | }); 175 | return newObject; 176 | } 177 | function prefixObject(object, prefix) { 178 | let newObject = {}; 179 | Object.keys(object).forEach((key) => { 180 | newObject[prefix + key] = object[key]; 181 | }); 182 | return newObject; 183 | } 184 | -------------------------------------------------------------------------------- /dist/template/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | # Enable React Refresh 3 | FAST_REFRESH=true -------------------------------------------------------------------------------- /dist/template/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: '@react-native-community', 5 | rules: { 6 | 'prettier/prettier': 0, 7 | 'react/jsx-uses-react': 0, 8 | 'react/react-in-jsx-scope': 0, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /dist/template/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | useTabs: false, 7 | printWidth: 80, 8 | jsxBracketSameLine: false, 9 | }; 10 | -------------------------------------------------------------------------------- /dist/template/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | plugins: [ 4 | [ 5 | '@babel/plugin-transform-react-jsx', 6 | { 7 | runtime: 'automatic', 8 | }, 9 | ], 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /dist/template/config-overrides.js: -------------------------------------------------------------------------------- 1 | // config-overrides.js 2 | const { 3 | addWebpackAlias, 4 | babelInclude, 5 | fixBabelImports, 6 | override, 7 | } = require('customize-cra'); 8 | 9 | const path = require('path'); 10 | 11 | module.exports = override( 12 | fixBabelImports('module-resolver', { 13 | alias: { 14 | '^react-native$': 'react-native-web', 15 | }, 16 | }), 17 | addWebpackAlias({ 18 | 'react-native': 'react-native-web', 19 | // here you can add extra packages 20 | }), 21 | babelInclude([ 22 | path.resolve('src'), 23 | path.resolve('app.json'), 24 | 25 | // any react-native modules you need babel to compile 26 | // e.g. path.resolve('./node_modules/react-native-vector-icons'), 27 | ]) 28 | ); 29 | -------------------------------------------------------------------------------- /dist/template/index.js: -------------------------------------------------------------------------------- 1 | // this will automatically resolve to the native or web AppEntry file 2 | // so we can add custom things inside the web / native version 3 | import './src'; 4 | -------------------------------------------------------------------------------- /dist/template/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 4 | }; 5 | -------------------------------------------------------------------------------- /dist/template/src/AbstractionExample.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet } from 'react-native'; 2 | 3 | export default function AbstractionExample() { 4 | return ( 5 | 6 | I'm only on native devices :) 7 | 8 | ); 9 | } 10 | 11 | const styles = StyleSheet.create({ 12 | container: { 13 | backgroundColor: '#EDEDED', 14 | padding: 10, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /dist/template/src/AbstractionExample.web.css: -------------------------------------------------------------------------------- 1 | .old-fashioned-div { 2 | background: linear-gradient(39deg, #ee00f0, #00f029, #f00092); 3 | background-size: 600% 600%; 4 | 5 | animation: animateBackground 27s ease infinite, 6 | wobble 5s ease-in-out alternate infinite; 7 | color: #fff; 8 | text-shadow: 1px 1px 5px #000; 9 | padding: 10px; 10 | border-radius: 5px; 11 | } 12 | 13 | @keyframes animateBackground { 14 | 0% { 15 | background-position: 0% 49%; 16 | } 17 | 50% { 18 | background-position: 100% 52%; 19 | } 20 | 100% { 21 | background-position: 0% 49%; 22 | } 23 | } 24 | 25 | @keyframes wobble { 26 | 50% { 27 | border-radius: 30px; 28 | } 29 | 100% { 30 | border-radius: 5px; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /dist/template/src/AbstractionExample.web.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet } from 'react-native'; 2 | import './AbstractionExample.web.css'; 3 | 4 | export default function AbstractionExample() { 5 | return ( 6 | 7 | I'm only on web devices :) 8 |
9 | This is just an old fashioned div with some crazy css 10 |
11 |
12 | ); 13 | } 14 | 15 | const styles = StyleSheet.create({ 16 | container: { 17 | backgroundColor: '#EDEDED', 18 | padding: 10, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /dist/template/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | StyleSheet, 6 | useWindowDimensions, 7 | TouchableHighlight, 8 | StatusBar, 9 | } from 'react-native'; 10 | 11 | import AbstractionExample from './AbstractionExample'; 12 | 13 | export default function App() { 14 | const { width } = useWindowDimensions(); 15 | const [backgroundColor, setState] = useState('#1275e6'); 16 | return ( 17 | <> 18 | 23 | 24 | 900 && styles.contenBig]}> 25 | Are you ready for React Native Web? 26 | 27 | It will save you a lot of time and you can almost always share more 28 | than 90% of your code base. 29 | 30 | 31 | You can always make an abstraction for the web version. Like the 32 | component below. 33 | 34 | 35 | 36 | 37 | Let's try add some some different background colors. 38 | 39 |