├── .gitignore ├── LICENSE ├── README.md ├── gulpfile.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── components │ │ └── Hello.tsx │ ├── routes.tsx │ └── views │ │ ├── AboutView.tsx │ │ ├── HomeView.tsx │ │ └── NotFoundView.tsx ├── client.tsx ├── pages │ └── MainPage.tsx ├── public │ └── favicon.ico ├── scripts │ └── example.js ├── server.tsx └── styles │ └── example.css └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Exclude Node.js packages 3 | node_modules/ 4 | 5 | # Exclude the build output directory 6 | dist/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016, Todd Lucas 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of react-tsx-starter nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Universal/Isomorphic React TypeScript Starter 3 | 4 | This project includes a working example of React, React Router, and TypeScript. 5 | 6 | All the code is in TypeScript, written as either `.ts` or `.tsx` files. 7 | The gulp-based build generates a browserified client file which is separate from the vendor file. 8 | The vendor file currently includes react and react-router. 9 | This separation speeds up the build process and can result in fewer client downloads when new builds are released. 10 | The gulp build process works with gulp.watch. 11 | 12 | This is a basic starter project with a minimal number of views and components. 13 | Many recent React examples are written in ES6 and make use of [Babel](https://babeljs.io/). 14 | These are largely compatible with this TypeScript based process. 15 | 16 | This starter also includes an example of how to use Redux with TypeScript. 17 | In order to keep the starter as clean as possible, the Redux example is on a branch. 18 | 19 | ## Features 20 | 21 | * React with React Router 22 | * Redux (on a separate branch) 23 | * TypeScript TSX 24 | * Isomorphic between server and client 25 | * Client app.js is browserified 26 | * Client vendor.js is browserified separately 27 | * Browserify-shim supports external scripts 28 | * Gulp based build with watch tasks 29 | 30 | ## Versions 31 | 32 | This template supports the following versions for key dependencies: 33 | 34 | * [React](https://facebook.github.io/react/) 16.8 35 | * [React Router](https://github.com/rackt/react-router) 5.0 36 | * [Redux](https://github.com/reactjs/redux) 4.0 ([redux branch](https://github.com/toddlucas/react-tsx-starter/tree/redux)) 37 | * [TypeScript](http://www.typescriptlang.org/) 3.4 38 | 39 | # Usage 40 | 41 | You'll need a few frameworks and utilities to be installed before starting. 42 | 43 | ## Prerequisites 44 | 45 | You'll need the following prior to setup: 46 | 47 | * [Node.js](https://nodejs.org/) 48 | * [TypeScript](http://www.typescriptlang.org/) 49 | * [Gulp](http://gulpjs.com/) 50 | 51 | ## Setup 52 | 53 | ### Install Node modules 54 | 55 | This will get all the packages required for development and run time, 56 | as defined in the `package.json` file. 57 | 58 | ``` 59 | > npm install 60 | ``` 61 | 62 | ## Build 63 | 64 | To run a full build, just run gulp with no arguments. 65 | 66 | ``` 67 | > gulp 68 | ``` 69 | 70 | You can also use `npm`. 71 | 72 | ``` 73 | > npm run build 74 | ``` 75 | 76 | ## Development 77 | 78 | Run watch and keep the console open. 79 | 80 | ``` 81 | > gulp watch 82 | ``` 83 | 84 | Gulp will automatically rebuild when a source file or CSS file changes. 85 | 86 | ## Running 87 | 88 | Run this command: 89 | 90 | ``` 91 | > npm run dev 92 | ``` 93 | 94 | Then open a browser and navigate to [http://localhost:3000](http://localhost:3000) to view. 95 | 96 | You can also run the server with automatic reloading using [nodemon](https://nodemon.io/) and [BrowserSync](https://www.browsersync.io/). 97 | 98 | ``` 99 | > gulp serve 100 | ``` 101 | 102 | This will launch at a different port since it proxies [Express](https://expressjs.com/). 103 | 104 | ## Related 105 | 106 | A simple starter project can be found at [react-tsx-lite](https://github.com/toddlucas/react-tsx-lite). 107 | 108 | ## License 109 | 110 | [BSD](https://github.com/toddlucas/react-tsx-starter/blob/master/LICENSE) (the same as React) 111 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /// 2 | 'use strict'; 3 | 4 | // process.env.BROWSERIFYSHIM_DIAGNOSTICS=1 5 | 6 | var gulp = require("gulp"), 7 | argv = require('yargs').argv, 8 | rimraf = require("rimraf"), 9 | concat = require("gulp-concat"), 10 | cssmin = require("gulp-cssmin"), 11 | gulpif = require("gulp-if"), 12 | rename = require("gulp-rename"), 13 | uglify = require("gulp-uglify"), 14 | less = require("gulp-less"), 15 | sourcemaps = require('gulp-sourcemaps'), 16 | browserify = require('browserify'), 17 | browserifyShim = require('browserify-shim'), 18 | typescript = require("gulp-typescript"), 19 | buffer = require('vinyl-buffer'), 20 | source = require('vinyl-source-stream'), 21 | nodemon = require('gulp-nodemon'), 22 | browserSync = require('browser-sync').create(); 23 | 24 | // 25 | // Configuration 26 | // 27 | 28 | var paths = { 29 | source: "./src/", 30 | output: "./dist/", 31 | public: "./dist/public/" 32 | }; 33 | 34 | var build = { 35 | input: { 36 | files: { 37 | // Source to compile 38 | ts: [ 39 | paths.source + '**/*.ts', 40 | paths.source + '**/*.tsx' 41 | ], 42 | 43 | // Styles 44 | styles: paths.source + "styles/**/*.css", 45 | stylesMin: paths.source + "styles/**/*.min.css", 46 | less: [paths.source + "styles/**/*.less"], 47 | app_less: [paths.source + "styles/**.less"], 48 | 49 | // Scripts 50 | scripts: paths.source + "scripts/**/*.js", 51 | scriptsMin: paths.source + "scripts/**/*.min.js", 52 | vendor_js: [ 53 | // 'history', 54 | 'react', 55 | 'react-dom', 56 | 'react-router', 57 | 'react-router-dom' 58 | ], 59 | extern_js: [ 60 | 'node_modules/q/q.js', 61 | ], 62 | polyfill_js: [ 63 | paths.source + 'polyfills/Object.assign.js' 64 | ], 65 | 66 | // Miscellaneous files to copy 67 | public: [paths.source + 'public/**/*'], 68 | } 69 | }, 70 | output: { 71 | files: { 72 | styles: paths.public + "styles/site.css", 73 | scripts: paths.public + "scripts/site.js", 74 | all: paths.public + "**/*", 75 | // An intermediate file; output from tsx, input to bundle. 76 | client_js: [paths.output + 'client.js'], 77 | server_js: paths.output + 'server.js', 78 | }, 79 | dirs: { 80 | ts: paths.output, 81 | public: paths.public, 82 | images: paths.public + 'images', 83 | styles: paths.public + 'styles', 84 | scripts: paths.public + 'scripts', 85 | polyfills: paths.object + 'polyfills', 86 | } 87 | }, 88 | other: { 89 | clean: ['dist/*'], 90 | } 91 | }; 92 | 93 | // 94 | // Setup 95 | // 96 | 97 | var typescriptProject = typescript.createProject("tsconfig.json"); 98 | 99 | var minify = argv.production || argv.staging; 100 | 101 | // 102 | // Basic tasks 103 | // 104 | 105 | gulp.task("clean", function (cb) { 106 | rimraf(paths.output, cb); 107 | }); 108 | 109 | gulp.task("scripts", function () { 110 | return gulp.src([build.input.files.scripts, "!" + build.input.files.scriptsMin], { base: "." }) 111 | .pipe(concat(build.output.files.scripts)) 112 | .pipe(gulpif(minify, uglify())) 113 | .pipe(gulpif(minify, rename({ suffix: '.min' }))) 114 | .pipe(gulp.dest(".")); 115 | }); 116 | 117 | gulp.task("styles", function () { 118 | return gulp.src([build.input.files.styles, "!" + build.input.files.stylesMin]) 119 | .pipe(concat(build.output.files.styles)) 120 | .pipe(gulpif(minify, cssmin())) 121 | .pipe(gulp.dest(".")); 122 | }); 123 | 124 | gulp.task('less', function () { 125 | return gulp.src(build.input.files.app_less) 126 | .pipe(gulpif(!minify, sourcemaps.init())) 127 | .pipe(less()).on('error', function (err) { 128 | console.error(err); 129 | this.emit('end'); // emit the end event, to properly end the task. 130 | }) 131 | .pipe(gulpif(!minify, sourcemaps.write())) 132 | .pipe(gulpif(minify, cssmin())) 133 | .pipe(gulpif(minify, rename({ suffix: '.min' }))) 134 | .pipe(gulp.dest(build.output.dirs.styles)); 135 | }); 136 | 137 | // 138 | // Compilation and packaging 139 | // 140 | 141 | gulp.task('typescript', function () { 142 | return gulp 143 | .src(build.input.files.ts) 144 | .pipe(typescriptProject()) 145 | .pipe(gulp.dest(build.output.dirs.ts)); 146 | }); 147 | 148 | gulp.task('vendor', function() { 149 | return browserify({ 150 | insertGlobals: true, 151 | }) 152 | .transform(browserifyShim) 153 | .require(build.input.files.vendor_js) 154 | .bundle() 155 | .on('error', console.error.bind(console)) 156 | .pipe(source('vendor.js')) 157 | // http://stackoverflow.com/questions/24992980/how-to-uglify-output-with-browserify-in-gulp 158 | // Convert from streaming to buffered vinyl file object for uglify 159 | .pipe(gulpif(minify, buffer())) 160 | .pipe(gulpif(minify, uglify())) 161 | .pipe(gulpif(minify, rename({ suffix: '.min' }))) 162 | .pipe(gulp.dest(build.output.dirs.scripts)); 163 | }); 164 | 165 | gulp.task('app', function() { 166 | return browserify({ 167 | insertGlobals: true, 168 | entries: build.output.files.client_js 169 | }) 170 | .transform(browserifyShim) 171 | .external(build.input.files.vendor_js) 172 | // .add(build.input.files.polyfill_js) 173 | .bundle() 174 | .on('error', console.error.bind(console)) 175 | .pipe(source('app.js')) 176 | // http://stackoverflow.com/questions/24992980/how-to-uglify-output-with-browserify-in-gulp 177 | // Convert from streaming to buffered vinyl file object for uglify 178 | .pipe(gulpif(minify, buffer())) 179 | .pipe(gulpif(minify, uglify())) 180 | .pipe(gulpif(minify, rename({ suffix: '.min' }))) 181 | .pipe(gulp.dest(build.output.dirs.scripts)); 182 | }); 183 | 184 | // 185 | // Copy tasks 186 | // 187 | 188 | gulp.task('public', function() { 189 | return gulp.src(build.input.files.public) 190 | .pipe(gulp.dest(build.output.dirs.public)); 191 | }); 192 | 193 | gulp.task('extern', function () { 194 | return gulp.src(build.input.files.extern_js) 195 | .pipe(gulp.dest(build.output.dirs.scripts)); 196 | }); 197 | 198 | gulp.task('polyfills', function() { 199 | return gulp.src(build.input.files.polyfill_js) 200 | .pipe(gulp.dest(build.output.dirs.polyfills)); 201 | }); 202 | 203 | gulp.task('copy', gulp.parallel('public', 'scripts', 'styles' /* 'extern', 'polyfills' */)); 204 | 205 | // 206 | // Build tasks 207 | // 208 | 209 | gulp.task('compile', 210 | gulp.series( 211 | gulp.parallel('typescript', 'less'), 212 | gulp.parallel('vendor', 'app'))); 213 | 214 | gulp.task('recompile', gulp.series('typescript', 'app')); 215 | 216 | gulp.task('watch', function() { 217 | gulp.watch(build.input.files.ts, gulp.series('recompile')); 218 | gulp.watch(build.input.files.styles, gulp.series('styles')); 219 | gulp.watch(build.input.files.less, gulp.series('less')); 220 | }); 221 | 222 | // 223 | // Hot reload tasks 224 | // 225 | 226 | gulp.task("nodemon", function (cb) { 227 | // https://gist.github.com/sogko/b53d33d4f3b40d3b4b2e#gistcomment-2795936 228 | return nodemon({ 229 | script: build.output.files.server_js, 230 | watch: [build.output.files.all], 231 | }).on("start", () => { 232 | cb(); 233 | }); 234 | }); 235 | 236 | gulp.task('browser-sync', function () { 237 | browserSync.init(null, { 238 | proxy: "http://localhost:3000", 239 | files: [build.output.files.all], 240 | port: 3002 241 | }); 242 | }); 243 | 244 | gulp.task("serve", gulp.parallel('watch', gulp.series('nodemon', 'browser-sync'))); 245 | 246 | // The default task (called when running 'gulp' from the command line). 247 | gulp.task('default', gulp.parallel('copy', 'compile')); 248 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tsx-starter", 3 | "version": "0.0.2", 4 | "description": "Universal/Isomorphic React TypeScript Starter Project", 5 | "main": "app.js", 6 | "author": { 7 | "name": "Todd Lucas", 8 | "email": "hi@toddlucas.net" 9 | }, 10 | "license": "BSD-3-Clause", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/toddlucas/react-tsx-starter.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/toddlucas/react-tsx-starter/issues" 17 | }, 18 | "scripts": { 19 | "start": "node server.js", 20 | "dev": "node ./dist/server.js", 21 | "dev:linux": "NODE_ENV=development node ./dist/server.js", 22 | "dev:windows": "SET NODE_ENV=development&& node dist\\server.js", 23 | "build": "gulp" 24 | }, 25 | "browserify-shim": {}, 26 | "dependencies": { 27 | "errorhandler": "^1.5.1", 28 | "express": "^4.17.0", 29 | "react": "^16.8.6", 30 | "react-dom": "^16.8.6", 31 | "react-router": "^5.0.0", 32 | "react-router-dom": "^5.0.0" 33 | }, 34 | "devDependencies": { 35 | "@types/errorhandler": "0.0.32", 36 | "@types/express": "^4.16.1", 37 | "@types/node": "^12.0.2", 38 | "@types/react": "^16.8.18", 39 | "@types/react-dom": "^16.8.4", 40 | "@types/react-router": "^5.0.0", 41 | "@types/react-router-dom": "^4.3.3", 42 | "browser-sync": "^2.26.5", 43 | "browserify": "^16.2.3", 44 | "browserify-shim": "^3.8.12", 45 | "gulp": "^4.0.2", 46 | "gulp-concat": "^2.6.0", 47 | "gulp-cssmin": "^0.2.0", 48 | "gulp-if": "^2.0.0", 49 | "gulp-less": "^4.0.1", 50 | "gulp-nodemon": "^2.4.2", 51 | "gulp-rename": "^1.2.2", 52 | "gulp-sourcemaps": "^2.6.5", 53 | "gulp-typescript": "^5.0.1", 54 | "gulp-uglify": "^3.0.2", 55 | "rimraf": "^2.6.3", 56 | "stream-browserify": "^2.0.2", 57 | "typescript": "^3.4.5", 58 | "vinyl-buffer": "^1.0.0", 59 | "vinyl-source-stream": "^2.0.0", 60 | "yargs": "^13.2.4" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app/components/Hello.tsx: -------------------------------------------------------------------------------- 1 |  2 | import * as React from 'react'; 3 | 4 | export interface HelloProps { 5 | name: string; 6 | } 7 | 8 | export const Hello = (props: HelloProps) =>

Hello, {props.name}!

; 9 | -------------------------------------------------------------------------------- /src/app/routes.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from 'react'; 3 | import { Route, Switch } from 'react-router'; 4 | 5 | import HomeView from './views/HomeView'; 6 | import AboutView from './views/AboutView'; 7 | import NotFoundView from './views/NotFoundView'; 8 | 9 | export const RouteMap = () => ( 10 |
11 | 12 | 13 | 14 | 15 | 16 |
17 | ); 18 | -------------------------------------------------------------------------------- /src/app/views/AboutView.tsx: -------------------------------------------------------------------------------- 1 |  2 | import * as React from 'react'; 3 | 4 | interface AboutState { 5 | loaded: boolean; 6 | } 7 | 8 | export default class AboutView extends React.Component<{}, AboutState> { 9 | constructor(props: Readonly<{}>) { 10 | super(props); 11 | this.state = { loaded: false }; 12 | } 13 | 14 | componentDidMount() { 15 | this.setState({ loaded: true }); 16 | } 17 | 18 | render() { 19 | const loading = this.state.loaded ? "" : " (loading...)"; 20 | return
21 |

About {loading}

22 |

23 | This project includes a working example of React, React Router, and TypeScript. 24 | It is hosted on Github. 25 |

26 |
; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/views/HomeView.tsx: -------------------------------------------------------------------------------- 1 |  2 | import * as React from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import { Hello } from '../components/Hello'; 5 | 6 | export interface HomeState { 7 | loaded: boolean; 8 | } 9 | 10 | export default class HomeView extends React.Component<{}, HomeState> { 11 | constructor(props: Readonly<{}>) { 12 | super(props); 13 | this.state = { loaded: false }; 14 | } 15 | 16 | componentDidMount() { 17 | this.setState({ loaded: true }); 18 | } 19 | 20 | render() { 21 | const loading = this.state.loaded ? "" : " (loading...)"; 22 | return
23 |

Home {loading}

24 | 25 |
About
26 |
; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/views/NotFoundView.tsx: -------------------------------------------------------------------------------- 1 |  2 | import * as React from 'react'; 3 | 4 | export default class NotFoundView extends React.Component { 5 | render() { 6 | return
7 |

404

8 |

Page not found

9 |
; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/client.tsx: -------------------------------------------------------------------------------- 1 |  2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import { RouteMap } from './app/routes'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('body')); 12 | -------------------------------------------------------------------------------- /src/pages/MainPage.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from 'react'; 3 | // import { Helmet } from 'react-helmet'; 4 | 5 | export interface MainProps { 6 | content: string; 7 | min: boolean; 8 | } 9 | 10 | export default class MainPage extends React.Component { 11 | constructor(props: any) { 12 | super(props); 13 | } 14 | 15 | render() { 16 | // Add helmet to control title at the view level 17 | // const helmet = Helmet.rewind(); 18 | const suffix = this.props.min ? '.min' : ''; 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | {/* 27 | {helmet.meta.toComponent()} 28 | {helmet.title.toComponent()} 29 | {helmet.link.toComponent()} 30 | */} 31 | Starter 32 | 33 | 34 | {/**/} 35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddlucas/react-tsx-starter/6b4fc1def6edd5c3eb27d4db4b9bc41b9bcdfb7c/src/public/favicon.ico -------------------------------------------------------------------------------- /src/scripts/example.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddlucas/react-tsx-starter/6b4fc1def6edd5c3eb27d4db4b9bc41b9bcdfb7c/src/scripts/example.js -------------------------------------------------------------------------------- /src/server.tsx: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as errorHandler from 'errorhandler'; 3 | import * as http from 'http'; 4 | import * as path from 'path'; 5 | import * as React from 'react'; 6 | import * as ReactDOMServer from 'react-dom/server'; 7 | import { StaticRouter } from 'react-router'; 8 | 9 | import MainPage from './pages/MainPage'; 10 | import { RouteMap } from './app/routes'; 11 | 12 | const app = express(); 13 | 14 | app.set('port', process.env.PORT || 3000); 15 | 16 | const env = process.env.NODE_ENV || 'development'; 17 | let min = true; 18 | 19 | if ('development' === env) { 20 | console.log('Running in development mode'); 21 | app.use(errorHandler()); 22 | min = false; 23 | } 24 | 25 | app.use(express.static(path.join(__dirname, 'public'))); 26 | 27 | app.use((req, res, next) => { 28 | const content = ReactDOMServer.renderToString( 29 | 30 | 31 | 32 | ); 33 | 34 | const html = ReactDOMServer.renderToString( 35 | 36 | ); 37 | 38 | res.send('\r\n' + html); 39 | }); 40 | 41 | http.createServer(app).listen(app.get('port'), () => { 42 | console.log('Express server listening on port ' + app.get('port')); 43 | }); 44 | -------------------------------------------------------------------------------- /src/styles/example.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Georgia,Cambria,"Times New Roman",Times,serif; 3 | } 4 | 5 | h1, h2, h3, h4, h5, h6 { 6 | font-family: "Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Verdana,sans-serif; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noImplicitAny": true 7 | } 8 | } 9 | --------------------------------------------------------------------------------