├── .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 |
--------------------------------------------------------------------------------