From 130f128cac2a520f33c90e70ec101e3316f65c8f Mon Sep 17 00:00:00 2001 From: John Reilly Date: Mon, 15 May 2017 21:12:03 +0100 Subject: [PATCH] Added more complex ts-checker example --- .../README.md | 16 +++ .../gulp/clean.js | 29 +++++ .../gulp/inject.js | 55 +++++++++ .../gulp/staticFiles.js | 31 +++++ .../gulp/tests.js | 45 +++++++ .../gulp/webpack.js | 112 ++++++++++++++++++ .../gulpFile.js | 54 +++++++++ .../karma.conf.js | 70 +++++++++++ .../package.json | 87 ++++++++++++++ .../src/actions/GreetingActions.ts | 17 +++ .../src/components/App.tsx | 45 +++++++ .../src/components/Greeting.tsx | 33 ++++++ .../src/components/WhoToGreet.tsx | 49 ++++++++ .../src/dispatcher/AppDispatcher.ts | 11 ++ .../src/index.html | 19 +++ .../src/main.tsx | 7 ++ .../src/stores/FluxStore.ts | 54 +++++++++ .../src/stores/GreetingStore.ts | 38 ++++++ .../src/types/GreetingState.ts | 6 + .../test/components/App.tests.tsx | 32 +++++ .../test/components/Greeting.tests.tsx | 45 +++++++ .../test/components/WhoToGreet.tests.tsx | 68 +++++++++++ .../test/main.js | 5 + .../test/stores/GreetingStore.tests.ts | 44 +++++++ .../tsconfig.json | 22 ++++ .../tslint.json | 91 ++++++++++++++ .../webpack.config.js | 65 ++++++++++ examples/react-babel-karma-gulp/README.md | 16 +++ 28 files changed, 1166 insertions(+) create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/README.md create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/gulp/clean.js create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/gulp/inject.js create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/gulp/staticFiles.js create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/gulp/tests.js create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/gulp/webpack.js create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/gulpFile.js create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/karma.conf.js create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/package.json create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/src/actions/GreetingActions.ts create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/src/components/App.tsx create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/src/components/Greeting.tsx create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/src/components/WhoToGreet.tsx create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/src/dispatcher/AppDispatcher.ts create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/src/index.html create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/src/main.tsx create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/src/stores/FluxStore.ts create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/src/stores/GreetingStore.ts create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/src/types/GreetingState.ts create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/test/components/App.tests.tsx create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/test/components/Greeting.tests.tsx create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/test/components/WhoToGreet.tests.tsx create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/test/main.js create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/test/stores/GreetingStore.tests.ts create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/tsconfig.json create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/tslint.json create mode 100644 examples/fork-ts-checker-react-babel-karma-gulp/webpack.config.js create mode 100644 examples/react-babel-karma-gulp/README.md diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/README.md b/examples/fork-ts-checker-react-babel-karma-gulp/README.md new file mode 100644 index 000000000..2683ed8ff --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/README.md @@ -0,0 +1,16 @@ +# TypeScript, Babel, React, and Karma Sample + +## Getting started + +You'll need [node / npm](https://nodejs.org/) installed. To get up and running just enter: + +``` +npm install +npm run serve +``` + +This will: + +1. Download the npm packages you need (including the type definitions from DefinitelyTyped) +2. Compile the code and serve it up at [http://localhost:8080](http://localhost:8080) + diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/gulp/clean.js b/examples/fork-ts-checker-react-babel-karma-gulp/gulp/clean.js new file mode 100644 index 000000000..c51b4e416 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/gulp/clean.js @@ -0,0 +1,29 @@ +'use strict'; + +var del = require('del'); +var gutil = require('gulp-util'); +var fs = require('fs'); + +function run(done) { + fs.stat('./dist', function(err){ + if (err) { + // Never existed + done(); + } + else { + del(['./dist'], { force: true }) + .then(function(paths) { + gutil.log('Deleted files/folders:\n', paths.join('\n')); + done(); + }) + .catch(function(error) { + gutil.log('Problem deleting:\n', error); + done(); + }); + } + }); +} + +module.exports = { + run: function(done) { return run(done); } +}; diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/gulp/inject.js b/examples/fork-ts-checker-react-babel-karma-gulp/gulp/inject.js new file mode 100644 index 000000000..e4133e565 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/gulp/inject.js @@ -0,0 +1,55 @@ +'use strict'; + +var gulp = require('gulp'); +var inject = require('gulp-inject'); +var glob = require('glob'); + +function injectIndex(options) { + function run() { + var target = gulp.src('./src/index.html'); + var sources = gulp.src([ + //'./dist/styles/main*.css', + './dist/scripts/vendor*.js', + './dist/scripts/main*.js' + ], { read: false }); + + return target + .pipe(inject(sources, { ignorePath: '/dist/', addRootSlash: false, removeTags: true })) + .pipe(gulp.dest('./dist')); + } + + var jsCssGlob = 'dist/**/*.{js,css}'; + + function checkForInitialFilesThenRun() { + glob(jsCssGlob, function (er, files) { + var filesWeNeed = ['dist/scripts/main', 'dist/scripts/vendor'/*, 'dist/styles/main'*/]; + + function fileIsPresent(fileWeNeed) { + return files.some(function(file) { + return file.indexOf(fileWeNeed) !== -1; + }); + } + + if (filesWeNeed.every(fileIsPresent)) { + run('initial build'); + } else { + checkForInitialFilesThenRun(); + } + }); + } + + checkForInitialFilesThenRun(); + + if (options.shouldWatch) { + gulp.watch(jsCssGlob, function(evt) { + if (evt.path && evt.type === 'changed') { + run(evt.path); + } + }); + } +} + +module.exports = { + build: function() { return injectIndex({ shouldWatch: false }); }, + watch: function() { return injectIndex({ shouldWatch: true }); } +}; diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/gulp/staticFiles.js b/examples/fork-ts-checker-react-babel-karma-gulp/gulp/staticFiles.js new file mode 100644 index 000000000..20327f272 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/gulp/staticFiles.js @@ -0,0 +1,31 @@ +'use strict'; + +var gulp = require('gulp'); +var cache = require('gulp-cached'); + +var targets = [ + { description: 'INDEX', src: './src/index.html', dest: './dist' } +]; + +function copy(options) { + function run(target) { + gulp.src(target.src) + .pipe(cache(target.description)) + .pipe(gulp.dest(target.dest)); + } + + function watch(target) { + gulp.watch(target.src, function() { run(target); }); + } + + targets.forEach(run); + + if (options.shouldWatch) { + targets.forEach(watch); + } +} + +module.exports = { + build: function() { return copy({ shouldWatch: false }); }, + watch: function() { return copy({ shouldWatch: true }); } +}; diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/gulp/tests.js b/examples/fork-ts-checker-react-babel-karma-gulp/gulp/tests.js new file mode 100644 index 000000000..05af0a551 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/gulp/tests.js @@ -0,0 +1,45 @@ +'use strict'; + +var Server = require('karma').Server; +var path = require('path'); +var gutil = require('gulp-util'); + +function runTests(options) { + // Documentation: https://karma-runner.github.io/0.13/dev/public-api.html + var karmaConfig = { + configFile: path.join(__dirname, '../karma.conf.js'), + singleRun: !options.shouldWatch, + + plugins: ['karma-webpack', 'karma-jasmine', 'karma-mocha-reporter', 'karma-sourcemap-loader', 'karma-phantomjs-launcher'], + reporters: ['mocha'] + }; + + if (options.done) { + karmaConfig.plugins.push('karma-junit-reporter'); + karmaConfig.reporters.push('junit'); + } else { + karmaConfig.plugins.push('karma-notify-reporter'); + karmaConfig.reporters.push('notify'); + } + + new Server(karmaConfig, karmaCompleted).start(); + + function karmaCompleted(exitCode) { + if (options.done) { + if (exitCode === 1) { + gutil.log('Karma: tests failed with code ' + exitCode); + } else { + gutil.log('Karma completed!'); + } + options.done(); + } + else { + process.exit(exitCode); + } + } +} + +module.exports = { + run: function(done) { return runTests({ shouldWatch: false, done: done }); }, + watch: function() { return runTests({ shouldWatch: true }); } +}; diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/gulp/webpack.js b/examples/fork-ts-checker-react-babel-karma-gulp/gulp/webpack.js new file mode 100644 index 000000000..7607c122b --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/gulp/webpack.js @@ -0,0 +1,112 @@ +'use strict'; + +var gulp = require('gulp'); +var gutil = require('gulp-util'); +var webpack = require('webpack'); +var ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); +var ForkTsCheckerNotifierWebpackPlugin = require('fork-ts-checker-notifier-webpack-plugin'); +var webpackFailPlugin = require('webpack-fail-plugin'); + +var webpackConfig = require('../webpack.config.js'); +var packageJson = require('../package.json'); + +function buildProduction(done) { + // modify some webpack config options + var myProdConfig = webpackConfig; + myProdConfig.output.filename = '[name].[hash].js'; + + myProdConfig.plugins = myProdConfig.plugins.concat( + new webpack.DefinePlugin({ + 'process.env': { + 'NODE_ENV': JSON.stringify('production') + } + }), + new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: 'vendor.[hash].js' }), + new webpack.optimize.UglifyJsPlugin({ + compress: { + warnings: true + } + }), + new ForkTsCheckerWebpackPlugin({ + blockEmit: true, + // tslint: true, + watch: ['./src', './test'] // optional but improves performance (less stat calls) + }), + webpackFailPlugin + ); + + // run webpack + webpack(myProdConfig, function (err, stats) { + if (err) { throw new gutil.PluginError('webpack:build', err); } + gutil.log('[webpack:build]', stats.toString({ + colors: true + })); + + if (done) { done(); } + }); +} + +function createDevCompiler() { + // modify some webpack config options + var myDevConfig = webpackConfig; + myDevConfig.devtool = 'inline-source-map'; + + myDevConfig.plugins = myDevConfig.plugins.concat( + new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: 'vendor.js' }), + new ForkTsCheckerNotifierWebpackPlugin ({ title: 'Build', excludeWarnings: false }), + new ForkTsCheckerWebpackPlugin({ + blockEmit: false, + // tslint: true, + watch: ['./src'] // optional but improves performance (less stat calls) + }) + ); + + // create a single instance of the compiler to allow caching + return webpack(myDevConfig); +} + +function build() { + return new Promise(function (resolve, reject) { + buildProduction(function (err) { + if (err) { + reject(err); + } else { + resolve('webpack built'); + } + }); + }); +} + +function watch() { + var firstBuildDone = false; + + return new Promise(function (resolve, reject) { + var devCompiler = createDevCompiler(); + devCompiler.watch({ // watch options: + aggregateTimeout: 300 // wait so long for more changes + }, function (err, stats) { + if (err) { + if (!firstBuildDone) { + firstBuildDone = true; + reject(err); + } + throw new gutil.PluginError('webpack:build-dev', err); + } else { + if (!firstBuildDone) { + firstBuildDone = true; + resolve('webpack built'); + } + } + + gutil.log('[webpack:build-dev]', stats.toString({ + chunks: false, + colors: true + })); + }); + }); +} + +module.exports = { + build: function () { return build(); }, + watch: function () { return watch(); } +}; diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/gulpFile.js b/examples/fork-ts-checker-react-babel-karma-gulp/gulpFile.js new file mode 100644 index 000000000..02fcbc0d5 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/gulpFile.js @@ -0,0 +1,54 @@ +/* eslint-disable no-var, strict, prefer-arrow-callback */ +'use strict'; + +var gulp = require('gulp'); +var gutil = require('gulp-util'); +var webpack = require('./gulp/webpack'); +var staticFiles = require('./gulp/staticFiles'); +var tests = require('./gulp/tests'); +var clean = require('./gulp/clean'); +var inject = require('./gulp/inject'); + +var lintSrcs = ['./gulp/**/*.js']; + +gulp.task('delete-dist', function (done) { + clean.run(done); +}); + +gulp.task('build-js', ['delete-dist'], function(done) { + webpack.build().then(function() { done(); }); +}); + +gulp.task('build-other', ['delete-dist'], function() { + staticFiles.build(); +}); + +gulp.task('build', ['build-js', 'build-other'], function () { + inject.build(); +}); + +gulp.task('watch', ['delete-dist'], function(done) { + process.env.NODE_ENV = 'development'; + Promise.all([ + webpack.watch()//, + //less.watch() + ]).then(function() { + gutil.log('Now that initial assets (js and css) are generated inject will start...'); + inject.watch(); + done(); + }).catch(function(error) { + gutil.log('Problem generating initial assets (js and css)', error); + }); + + staticFiles.watch(); + tests.watch(); +}); + +gulp.task('watch-and-serve', ['watch'], function() { + // local as not required for build + var express = require('express') + var app = express() + + app.use(express.static('dist', {'index': 'index.html'})) + app.listen(8080); +}); diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/karma.conf.js b/examples/fork-ts-checker-react-babel-karma-gulp/karma.conf.js new file mode 100644 index 000000000..1f61cbeac --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/karma.conf.js @@ -0,0 +1,70 @@ +/* eslint-disable no-var, strict */ +'use strict'; + +var webpackConfig = require('./webpack.config.js'); +var ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); +var ForkTsCheckerNotifierWebpackPlugin = require('fork-ts-checker-notifier-webpack-plugin'); + +module.exports = function (config) { + var forkTsCheckerOptions = { + blockEmit: true, + // tslint: true, + watch: ['./test'] // optional but improves performance (less stat calls) + }; + var plugins = config.singleRun + ? [ + new ForkTsCheckerWebpackPlugin(forkTsCheckerOptions) + ] + : [ + new ForkTsCheckerNotifierWebpackPlugin({ title: 'Tests Build', excludeWarnings: false }), + new ForkTsCheckerWebpackPlugin(Object.assign({}, forkTsCheckerOptions, { blockEmit: false })) + ]; + + // Documentation: https://karma-runner.github.io/0.13/config/configuration-file.html + config.set({ + browsers: ['PhantomJS'], + + files: [ + // This ensures we have the es6 shims in place and then loads all the tests + 'test/main.js' + ], + + port: 9876, + + frameworks: ['jasmine'], + + logLevel: config.LOG_INFO, //config.LOG_DEBUG + + preprocessors: { + 'test/main.js': ['webpack', 'sourcemap'] + }, + + webpack: { + devtool: 'inline-source-map', + module: webpackConfig.module, + resolve: webpackConfig.resolve, + plugins: plugins + }, + + webpackMiddleware: { + quiet: true, + stats: { + colors: true + } + }, + + // reporter options + mochaReporter: { + colors: { + success: 'bgGreen', + info: 'cyan', + warning: 'bgBlue', + error: 'bgRed' + } + }, + + notifyReporter: { + reportSuccess: false // Default: true, Will notify when a suite was successful + } + }); +}; diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/package.json b/examples/fork-ts-checker-react-babel-karma-gulp/package.json new file mode 100644 index 000000000..0dc3042c5 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/package.json @@ -0,0 +1,87 @@ +{ + "name": "es6-babel-react-flux-karma", + "version": "1.0.0", + "description": "ES6 + TypeScript + Babel + React + Karma: The Secret Recipe", + "main": "index.js", + "scripts": { + "test": "karma start --reporters mocha,junit --single-run --browsers PhantomJS", + "serve": "gulp watch-and-serve", + "watch": "gulp watch", + "build": "gulp build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/microsoft/typescriptsamples.git" + }, + "keywords": [ + "React", + "Flux", + "ES2016", + "typescript" + ], + "author": "John Reilly", + "license": "MIT", + "bugs": { + "url": "https://github.com/microsoft/typescriptsamples/issues" + }, + "homepage": "https://github.com/Microsoft/TypeScriptSamples/tree/master/es6-babel-react-flux-karma#readme", + "devDependencies": { + "@types/fbemitter": "^2.0.32", + "@types/flux": "0.0.32", + "@types/jasmine": "^2.5.35", + "@types/react": "^0.14.41", + "@types/react-addons-test-utils": "^0.14.15", + "@types/react-bootstrap": "0.0.33", + "@types/react-dom": "^0.14.18", + "@types/react-test-renderer": "^15.5.0", + "babel": "^6.0.0", + "babel-core": "^6.0.0", + "babel-loader": "^7.0.0", + "babel-preset-es2015": "^6.0.0", + "babel-preset-es2016": "^6.16.0", + "babel-preset-react": "^6.0.0", + "del": "^2.0.2", + "express": "^4.13.3", + "fork-ts-checker-notifier-webpack-plugin": "^0.1.3", + "fork-ts-checker-webpack-plugin": "^0.1.1", + "glob": "^7.0.0", + "gulp": "^3.9.0", + "gulp-autoprefixer": "^3.1.0", + "gulp-cached": "^1.1.0", + "gulp-cssmin": "^0.1.7", + "gulp-eslint": "^2.0.0", + "gulp-if": "^2.0.0", + "gulp-inject": "^3.0.0", + "gulp-notify": "^2.2.0", + "gulp-sourcemaps": "^1.5.2", + "gulp-streamify": "1.0.2", + "gulp-uglify": "^1.2.0", + "gulp-util": "^3.0.6", + "jasmine-core": "^2.3.4", + "karma": "^1.2.0", + "karma-jasmine": "^1.0.0", + "karma-junit-reporter": "^1.0.0", + "karma-mocha-reporter": "^2.0.0", + "karma-notify-reporter": "^1.0.0", + "karma-phantomjs-launcher": "^1.0.0", + "karma-sourcemap-loader": "^0.3.6", + "karma-webpack": "^2.0.1", + "phantomjs-prebuilt": "^2.1.4", + "react-addons-test-utils": "^15.3.1", + "react-test-renderer": "^15.5.4", + "ts-loader": "^2.0.0", + "tslint": "^5.1.0", + "tslint-react": "^3.0.0", + "typescript": "^2.1.4", + "webpack": "^2.2.0", + "webpack-fail-plugin": "^1.0.4", + "webpack-notifier": "^1.2.1" + }, + "dependencies": { + "babel-polyfill": "^6.0.0", + "flux": "^2.0.3", + "fbemitter": "^2.0.2", + "react": "^15.4.1", + "react-dom": "^15.4.1" + } +} diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/src/actions/GreetingActions.ts b/examples/fork-ts-checker-react-babel-karma-gulp/src/actions/GreetingActions.ts new file mode 100644 index 000000000..147db5a50 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/src/actions/GreetingActions.ts @@ -0,0 +1,17 @@ +import {TypedEvent, AppDispatcher} from '../dispatcher/AppDispatcher'; + +export class AddGreetingEvent extends TypedEvent {} +export class NewGreetingChanged extends TypedEvent {} +export class RemoveGreeting extends TypedEvent {} + +export function addGreeting(newGreeting: string) { + AppDispatcher.dispatch(new AddGreetingEvent(newGreeting)); +} + +export function newGreetingChanged(newGreeting: string) { + AppDispatcher.dispatch(new NewGreetingChanged(newGreeting)); +} + +export function removeGreeting(greetingToRemove: string) { + AppDispatcher.dispatch(new RemoveGreeting(greetingToRemove)); +} diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/src/components/App.tsx b/examples/fork-ts-checker-react-babel-karma-gulp/src/components/App.tsx new file mode 100644 index 000000000..bf77c500f --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/src/components/App.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import FBEmitter from 'fbemitter'; + +import GreetingStore from '../stores/GreetingStore'; +import GreetingState from '../types/GreetingState'; +import WhoToGreet from './WhoToGreet'; +import Greeting from './Greeting'; + +class App extends React.Component<{}, GreetingState> { + eventSubscription: FBEmitter.EventSubscription; + constructor(props: {}) { + super(props); + this.state = this.getStateFromStores(); + } + + public componentWillMount() { + this.eventSubscription = GreetingStore.addChangeListener(this.onChange); + } + + public componentWillUnmount() { + this.eventSubscription.remove(); + } + + render() { + const { greetings, newGreeting } = this.state; + return ( +
+

Hello People!

+ + + + { greetings.map((g, index) => ) } +
+ ); + } + + private getStateFromStores() { + return GreetingStore.getState(); + } + private onChange = () => { + this.setState(this.getStateFromStores()); + } +} + +export default App; diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/src/components/Greeting.tsx b/examples/fork-ts-checker-react-babel-karma-gulp/src/components/Greeting.tsx new file mode 100644 index 000000000..a7e6931f8 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/src/components/Greeting.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import * as GreetingActions from '../actions/GreetingActions'; + +interface Props { + key: number; + targetOfGreeting: string; +} + +class Greeting extends React.Component { + constructor(props: Props) { + super(props); + } + + render() { + return ( +

+ Hello { this.props.targetOfGreeting }! + + +

+ ); + } + + _onClick = (_event: React.MouseEvent) => { + GreetingActions.removeGreeting(this.props.targetOfGreeting); + } +} + +export default Greeting; diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/src/components/WhoToGreet.tsx b/examples/fork-ts-checker-react-babel-karma-gulp/src/components/WhoToGreet.tsx new file mode 100644 index 000000000..f88d1b17d --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/src/components/WhoToGreet.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import * as GreetingActions from '../actions/GreetingActions'; + +interface Props { + newGreeting: string; +} + +class WhoToGreet extends React.Component { + constructor(props: Props) { + super(props); + } + + render() { + return ( +
+
+ + +
+
+ ); + } + + get _preventSubmission() { + return !this.props.newGreeting; + } + + _handleNewGreetingChange = (event: React.FormEvent) => { + const newGreeting = (event.target as HTMLInputElement).value; + GreetingActions.newGreetingChanged(newGreeting); + } + + _onSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + if (!this._preventSubmission) { + GreetingActions.addGreeting(this.props.newGreeting); + } + } +} + +export default WhoToGreet; diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/src/dispatcher/AppDispatcher.ts b/examples/fork-ts-checker-react-babel-karma-gulp/src/dispatcher/AppDispatcher.ts new file mode 100644 index 000000000..be9360f6d --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/src/dispatcher/AppDispatcher.ts @@ -0,0 +1,11 @@ +import { Dispatcher } from 'flux'; + +export class TypedEvent

{ + constructor(public payload: P) {} +} + +export type Event = TypedEvent; + +const dispatcherInstance: Dispatcher = new Dispatcher(); + +export { dispatcherInstance as AppDispatcher }; diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/src/index.html b/examples/fork-ts-checker-react-babel-karma-gulp/src/index.html new file mode 100644 index 000000000..16013fa36 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/src/index.html @@ -0,0 +1,19 @@ + + + + + + + + TypeScript, Babel, React, Flux, and Karma + + + + + + +

+ + + + diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/src/main.tsx b/examples/fork-ts-checker-react-babel-karma-gulp/src/main.tsx new file mode 100644 index 000000000..65b3f94b2 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/src/main.tsx @@ -0,0 +1,7 @@ +import 'babel-polyfill'; +import React from 'react'; +import ReactDOM from 'react-dom'; + +import App from './components/App'; + +ReactDOM.render(, document.getElementById('content')); diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/src/stores/FluxStore.ts b/examples/fork-ts-checker-react-babel-karma-gulp/src/stores/FluxStore.ts new file mode 100644 index 000000000..7737a298e --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/src/stores/FluxStore.ts @@ -0,0 +1,54 @@ +import { EventEmitter } from 'fbemitter'; +import { Event } from '../dispatcher/AppDispatcher'; +import * as Flux from 'flux'; + +const CHANGE_EVENT = 'change'; + +class FluxStore { + protected state: TState; + private changed: boolean; + private emitter: EventEmitter; + private dispatchToken: string; + private dispatcher: Flux.Dispatcher; + private cleanStateFn: () => TState; + + constructor(dispatcher: Flux.Dispatcher, public onDispatch: (action: Event) => void, cleanStateFn: () => TState) { + this.emitter = new EventEmitter(); + this.changed = false; + this.dispatcher = dispatcher; + this.dispatchToken = dispatcher.register(payload => { + this.invokeOnDispatch(payload); + }); + + this.cleanStateFn = cleanStateFn; + this.state = this.cleanStateFn(); + } + + /** + * Is idempotent per dispatched event + */ + emitChange() { + this.changed = true; + } + + hasChanged() { return this.changed; } + + addChangeListener(callback: () => void) { + return this.emitter.addListener(CHANGE_EVENT, callback); + } + + public cleanState() { + this.changed = false; + this.state = this.cleanStateFn(); + } + + private invokeOnDispatch(payload: Event) { + this.changed = false; + this.onDispatch(payload); + if (this.changed) { + this.emitter.emit(CHANGE_EVENT); + } + } +} + +export default FluxStore; diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/src/stores/GreetingStore.ts b/examples/fork-ts-checker-react-babel-karma-gulp/src/stores/GreetingStore.ts new file mode 100644 index 000000000..602845ea0 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/src/stores/GreetingStore.ts @@ -0,0 +1,38 @@ +import FluxStore from './FluxStore'; +import {Event, AppDispatcher} from '../dispatcher/AppDispatcher'; +import GreetingState from '../types/GreetingState'; +import { AddGreetingEvent, RemoveGreeting, NewGreetingChanged } from '../actions/GreetingActions'; + +class GreeterStore extends FluxStore { + constructor(dispatcher: typeof AppDispatcher) { + const onDispatch = (action: Event) => { + if (action instanceof AddGreetingEvent) { + const {payload} = action; + this.state.newGreeting = ''; + this.state.greetings = this.state.greetings.concat(payload); + this.emitChange(); + } + else if (action instanceof RemoveGreeting) { + const {payload} = action; + this.state.greetings = this.state.greetings.filter(g => g !== payload); + this.emitChange(); + } + else if (action instanceof NewGreetingChanged) { + const {payload} = action; + this.state.newGreeting = payload; + this.emitChange(); + } + }; + super(dispatcher, onDispatch, () => ({ + greetings: [], + newGreeting: '' + })); + } + + getState() { + return this.state; + } +} + +const greeterStoreInstance = new GreeterStore(AppDispatcher); +export default greeterStoreInstance; diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/src/types/GreetingState.ts b/examples/fork-ts-checker-react-babel-karma-gulp/src/types/GreetingState.ts new file mode 100644 index 000000000..656b20143 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/src/types/GreetingState.ts @@ -0,0 +1,6 @@ +interface GreetingState { + greetings: string[]; + newGreeting: string; +} + +export default GreetingState; diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/test/components/App.tests.tsx b/examples/fork-ts-checker-react-babel-karma-gulp/test/components/App.tests.tsx new file mode 100644 index 000000000..ab48327ee --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/test/components/App.tests.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { createRenderer } from 'react-test-renderer/shallow'; + +import App from '../../src/components/App'; +import WhoToGreet from '../../src/components/WhoToGreet'; +import Greeting from '../../src/components/Greeting'; +import GreetingStore from '../../src/stores/GreetingStore'; + +describe('App', () => { + it('renders expected HTML', () => { + const app = render({ greetings: ['James'], newGreeting: 'Benjamin' }); + expect(app).toEqual( +
+

Hello People!

+ + + + { [ + + ] } +
+ ); + }); + + function render(state: any) { + const shallowRenderer = createRenderer(); + spyOn(GreetingStore, 'getState').and.returnValue(state); + + shallowRenderer.render(); + return shallowRenderer.getRenderOutput(); + } +}); diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/test/components/Greeting.tests.tsx b/examples/fork-ts-checker-react-babel-karma-gulp/test/components/Greeting.tests.tsx new file mode 100644 index 000000000..aa26cadf8 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/test/components/Greeting.tests.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { createRenderer } from 'react-test-renderer/shallow'; + +import Greeting from '../../src/components/Greeting'; +import * as GreetingActions from '../../src/actions/GreetingActions'; + +describe('Greeting', () => { + let handleSelectionChangeSpy: jasmine.Spy; + beforeEach(() => { + handleSelectionChangeSpy = jasmine.createSpy('handleSelectionChange'); + }); + + it('given a targetOfGreeting of \'James\' it renders a p containing a greeting and a remove button', () => { + const targetOfGreeting = 'James'; + + const p = render({ targetOfGreeting }); + expect(p.type).toBe('p'); + expect(p.props.children[0]).toBe('Hello '); + expect(p.props.children[1]).toBe('James'); + expect(p.props.children[2]).toBe('!'); + + const [ , , , button ] = p.props.children; + + expect(button.type).toBe('button'); + expect(button.props.className).toBe('btn btn-default btn-danger'); + expect(button.props.children).toBe('Remove'); + }); + + it('button onClick triggers an removeGreeting action', () => { + const targetOfGreeting = 'Benjamin'; + const p = render({ targetOfGreeting }); + const [ , , , button ] = p.props.children; + spyOn(GreetingActions, 'removeGreeting'); + + button.props.onClick(); + + expect(GreetingActions.removeGreeting).toHaveBeenCalledWith(targetOfGreeting); + }); + + function render({ targetOfGreeting }: { targetOfGreeting: string; }) { + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + return shallowRenderer.getRenderOutput(); + } +}); diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/test/components/WhoToGreet.tests.tsx b/examples/fork-ts-checker-react-babel-karma-gulp/test/components/WhoToGreet.tests.tsx new file mode 100644 index 000000000..606ed735a --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/test/components/WhoToGreet.tests.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { createRenderer } from 'react-test-renderer/shallow'; + +import WhoToGreet from '../../src/components/WhoToGreet'; +import * as GreetingActions from '../../src/actions/GreetingActions'; + +describe('WhoToGreet', () => { + let handleSelectionChangeSpy: jasmine.Spy; + beforeEach(() => { + handleSelectionChangeSpy = jasmine.createSpy('handleSelectionChange'); + }); + + it('given a newGreeting then it renders a form containing an input containing that text and an add button', () => { + const newGreeting = 'James'; + + const form = render({ newGreeting }); + expect(form.type).toBe('form'); + expect(form.props.role).toBe('form'); + + const formGroup = form.props.children; + expect(formGroup.type).toBe('div'); + expect(formGroup.props.className).toBe('form-group'); + + const [input, button] = formGroup.props.children; + + expect(input.type).toBe('input'); + expect(input.props.type).toBe('text'); + expect(input.props.className).toBe('form-control'); + expect(input.props.placeholder).toBe('Who would you like to greet?'); + expect(input.props.value).toBe(newGreeting); + + expect(button.type).toBe('button'); + expect(button.props.type).toBe('submit'); + expect(button.props.className).toBe('btn btn-default btn-primary'); + expect(button.props.disabled).toBe(false); + expect(button.props.children).toBe('Add greeting'); + }); + + it('input onChange triggers a newGreetingChanged action', () => { + const newGreeting = 'Benjamin'; + const form = render({ newGreeting }); + const formGroup = form.props.children; + const [input] = formGroup.props.children; + spyOn(GreetingActions, 'newGreetingChanged'); + + input.props.onChange({ target: { value: newGreeting } }); + + expect(GreetingActions.newGreetingChanged).toHaveBeenCalledWith(newGreeting); + }); + + it('button onClick triggers an addGreeting action', () => { + const newGreeting = 'Benjamin'; + const form = render({ newGreeting }); + const formGroup = form.props.children; + const [, button] = formGroup.props.children; + spyOn(GreetingActions, 'addGreeting'); + + button.props.onClick({ preventDefault: () => { } }); + + expect(GreetingActions.addGreeting).toHaveBeenCalledWith(newGreeting); + }); + + function render({ newGreeting }: { newGreeting: string }) { + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + return shallowRenderer.getRenderOutput(); + } +}); diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/test/main.js b/examples/fork-ts-checker-react-babel-karma-gulp/test/main.js new file mode 100644 index 000000000..1b332a7c0 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/test/main.js @@ -0,0 +1,5 @@ +/* eslint-disable */ +import 'babel-polyfill'; + +const testsContext = require.context('./', true, /\.tests\.ts(x?)$/); +testsContext.keys().forEach(testsContext); \ No newline at end of file diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/test/stores/GreetingStore.tests.ts b/examples/fork-ts-checker-react-babel-karma-gulp/test/stores/GreetingStore.tests.ts new file mode 100644 index 000000000..b06071007 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/test/stores/GreetingStore.tests.ts @@ -0,0 +1,44 @@ +import GreetingStore from '../../src/stores/GreetingStore'; +import { AddGreetingEvent, RemoveGreeting, NewGreetingChanged } from '../../src/actions/GreetingActions'; + +const registeredCallback = GreetingStore.onDispatch.bind(GreetingStore); + +describe('GreetingStore', () => { + beforeEach(() => { + GreetingStore.cleanState(); + }); + + it('given no actions, newGreeting should be an empty string and greetings should be an empty array', () => { + const { greetings, newGreeting } = GreetingStore.getState(); + + expect(greetings).toEqual([]); + expect(newGreeting).toBe(''); + }); + + it('given an ADD_GREETING action with a newGreeting of \'Benjamin\', the newGreeting should be an empty string and greetings should contain \'Benjamin\'', () => { + [new AddGreetingEvent('Benjamin')].forEach(registeredCallback); + + const { greetings, newGreeting } = GreetingStore.getState(); + + expect(greetings.find(g => g === 'Benjamin')).toBeTruthy(); + expect(newGreeting).toBe(''); + }); + + it('given an REMOVE_GREETING action with a greetingToRemove of \'Benjamin\', the state greetings should be an empty array', () => { + [new AddGreetingEvent('Benjamin'), new RemoveGreeting('Benjamin')].forEach(registeredCallback); + + const { greetings } = GreetingStore.getState(); + + expect(greetings.length).toBe(0); + expect(greetings.find(g => g === 'Benjamin')).toBeFalsy(); + }); + + it('given a NEW_GREETING_CHANGED action with a newGreeting of \'Benjamin\', the state newGreeting should be \'Benjamin\'', () => { + [new NewGreetingChanged('Benjamin')].forEach(registeredCallback); + + const { newGreeting } = GreetingStore.getState(); + + expect(newGreeting).toEqual('Benjamin'); + }); + +}); diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/tsconfig.json b/examples/fork-ts-checker-react-babel-karma-gulp/tsconfig.json new file mode 100644 index 000000000..cb3163395 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "lib": [ + "dom", + "es2015", + "es2016" + ], + "jsx": "preserve", + "target": "es2016", + "module": "es2015", + "moduleResolution": "node", + "noImplicitAny": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "removeComments": false, + "preserveConstEnums": true, + "sourceMap": true, + "skipLibCheck": true + } +} \ No newline at end of file diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/tslint.json b/examples/fork-ts-checker-react-babel-karma-gulp/tslint.json new file mode 100644 index 000000000..0935562c8 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/tslint.json @@ -0,0 +1,91 @@ +{ + "extends": [ + "tslint:recommended", + "tslint-react" + ], + "rules": { + "arrow-parens": false, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "indent": [ + true, + "spaces" + ], + "interface-name": [ + false + ], + "jsx-alignment": false, + "jsx-no-lambda": true, + "jsx-wrap-multiline": false, + "jsx-no-multiline-js": false, + "jsx-no-string-ref": true, + "jsx-self-close": true, + "jsx-curly-spacing": "always", + "jsx-boolean-value": false, + "max-classes-per-file": { + "severity": "warning", + "options": [true, 1] + }, + "max-line-length": [ + false + ], + "member-access": false, + "no-duplicate-variable": true, + "no-empty": false, + "no-eval": true, + "no-internal-module": true, + "no-trailing-whitespace": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-whitespace" + ], + "ordered-imports": false, + "radix": false, + "quotemark": [ + true, + "single", + "avoid-escape", + "jsx-double" + ], + "semicolon": [ + true, + "always" + ], + "switch-default": false, + "trailing-comma": [ + false + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "variable-name": [ + true, + "ban-keywords" + ], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } +} \ No newline at end of file diff --git a/examples/fork-ts-checker-react-babel-karma-gulp/webpack.config.js b/examples/fork-ts-checker-react-babel-karma-gulp/webpack.config.js new file mode 100644 index 000000000..4c2922969 --- /dev/null +++ b/examples/fork-ts-checker-react-babel-karma-gulp/webpack.config.js @@ -0,0 +1,65 @@ +'use strict'; + +var path = require('path'); + +var babelOptions = { + "presets": [ + "react", + [ + "es2015", + { + "modules": false + } + ], + "es2016" + ] +}; + +module.exports = { + cache: true, + entry: { + main: './src/main.tsx', + vendor: [ + 'babel-polyfill', + 'fbemitter', + 'flux', + 'react', + 'react-dom' + ] + }, + output: { + path: path.resolve(__dirname, './dist/scripts'), + filename: '[name].js', + chunkFilename: '[chunkhash].js' + }, + module: { + rules: [{ + test: /\.ts(x?)$/, + exclude: /node_modules/, + use: [ + { + loader: 'babel-loader', + options: babelOptions + }, + { + loader: 'ts-loader', + options: { transpileOnly: true } + } + ] + }, { + test: /\.js$/, + exclude: /node_modules/, + use: [ + { + loader: 'babel-loader', + options: babelOptions + } + ] + }] + }, + plugins: [ + ], + resolve: { + extensions: ['.ts', '.tsx', '.js'] + }, +}; diff --git a/examples/react-babel-karma-gulp/README.md b/examples/react-babel-karma-gulp/README.md new file mode 100644 index 000000000..2683ed8ff --- /dev/null +++ b/examples/react-babel-karma-gulp/README.md @@ -0,0 +1,16 @@ +# TypeScript, Babel, React, and Karma Sample + +## Getting started + +You'll need [node / npm](https://nodejs.org/) installed. To get up and running just enter: + +``` +npm install +npm run serve +``` + +This will: + +1. Download the npm packages you need (including the type definitions from DefinitelyTyped) +2. Compile the code and serve it up at [http://localhost:8080](http://localhost:8080) +