ReactXP samples comes preconfigured to build your application with Webpack when you target the browser and Typescript compiler when you target React Native. This is a great way to get you started quickly. Just clone the GitHub repository, open one of the samples in your favorite editor, follow the read.me instructions and you’re up and running in minutes.
You may want to use ts-nameof for instance or allow resources aliasing as introduced in this post on the ReactXP blog. In both cases, the goal is to preprocess either your .ts(x) source files or to post process the generated .js files.
Simply put, out of the box you can’t do it. The solution is to build a development workflow or pipeline to chain actions. There are several options to do that but I will discuss the use of Gulp. I encourage you to read the docs before going further if you’re new to Gulp.
But first, let’s review the process of building a ReactXP application:
- Whatever the target, the very first step is to transpile .ts(x) files to .js files. This is done with the TypeScript compiler.
- If the browser is the target, then you’ll need to bundle your code alongside all required modules.
- If the target is React Native, the above step is not required. You compile and run an XCode and/or Android project which is then deployed to your device or simulator. The React Native packager is used to “bundle” your app. I will not talk about that specifically. Suffice to say for now that, by default, the packager will look for JS files recursively in your project directory. So, we want to make sure that step one process the final .js files.
In my use case, the build workflow will be:
- clean the solution (delete target and temporary .js files)
- ts-nameof then typescript
- aliasing
- clean .js temporary files files
- bundle (web target only)
In terms of a Gulp task that will run a sequence, it will become the following code:
var runSequence = require('run-sequence');<span data-mce-type="bookmark" id="mce_SELREST_start" data-mce-style="overflow:hidden;line-height:0" style="overflow:hidden;line-height:0" ></span> gulp.task('run', function(callback) { runSequence(['clean-dest', 'clean-src'], 'typescript', 'apply-aliases', 'clean-src', 'bundle', callback); });
The tasks in angle brackets will run in parallel.
I’ve borrowed the gulp file code from the ReactXP blog.
We’re using a configuration object to specify the aliasing tasks and directories:
var config = { aliasify: { src: './.temp/' + argv.platform, dest: getBuildPath() + 'js/', aliases: (argv.platform === 'web') ? // Web Aliases { 'AppAlertAndPrompt': './AlertAndPromptWeb' } : // Native Aliases { 'AppAlertAndPrompt': './AlertAndPromptNative' } } }
Now, let’s examine the code for each individual tasks:
var clean = require('gulp-clean'); gulp.task('clean-dest', function() { return gulp.src(config.aliasify.dest, {read: false}) .pipe(clean()); }); gulp.task('clean-src', function() { return gulp.src(config.aliasify.src, {read: false}) .pipe(clean()); });
Above are the “clean solution” tasks. Nothing very fancy, we’re using gulp-clean.
Next is the ts-nameof replacement and transpiling:
var ts = require("gulp-typescript"); var tsNameof = require("ts-nameof"); // To use tsconfig.json var tsProject = ts.createProject('tsconfig.json');<span data-mce-type="bookmark" id="mce_SELREST_start" data-mce-style="overflow:hidden;line-height:0" style="overflow:hidden;line-height:0" ></span> gulp.task('typescript', function() { return gulp.src("src/**/*.ts*") .pipe(tsNameof()) .pipe(tsProject()) .pipe(gulp.dest(config.aliasify.src)) .on('error', onError); });
We’ll work on TypeScript source files to apply the ts-nameof module, then transpile to javascript using the .tsconfig file that comes from the ReactXP sample and output the results in a temporary directory relative to the target platform.
Out next task is the aliasing step. Again, to learn more about that, the credits goes to Microsoft:
var joinPath = require('path.join'); gulp.task('apply-aliases', function() { return gulp.src(joinPath(config.aliasify.src, '**/*.js')) .pipe(aliasify(config.aliasify.aliases)) .pipe(gulp.dest(config.aliasify.dest)) .on('error', onError); });
We again make sure to clean the intermediate directories to make sure that the ReactNative Packager will work on the correct files (see above for the task code).
If we target the browser, the final task is to bundle the files:
var webpack = require('webpack-stream'); var argv = require('yargs').argv; var gutil = require('gulp-util');<span data-mce-type="bookmark" id="mce_SELREST_start" data-mce-style="overflow:hidden;line-height:0" style="overflow:hidden;line-height:0" ></span> gulp.task('bundle', function() { if (argv.platform === 'web') return gulp.src(joinPath(config.aliasify.dest, 'index.js')) .pipe(webpack( require('./_____webpack.config.js') )) .pipe(gulp.dest('dist/')); else return gutil.noop(); });
We’re using Webpack on JavaScript files instead of TypeScript ones. Make sure to use webpack-stream and not the now deprecated gulp-webpack. This is basically the only difference, the config file becomes simpler:
var debug = process.env.NODE_ENV !== "production"; var webpack = require('webpack'); module.exports = { stats: { // Configure the console output errorDetails: true, colors: true, modules: true, reasons: true }, entry: "./out/js/index.js", output: { filename: "bundle.js", path: __dirname + "/dist" } }
That’s it 😉
Following is my gulpfile.js:
var gulp = require('gulp'); var eventStream = require('event-stream'); var joinPath = require('path.join'); var clean = require('gulp-clean'); var webpack = require('webpack-stream'); var ts = require("gulp-typescript"); var tsNameof = require("ts-nameof"); // To use tsconfig.json var tsProject = ts.createProject('tsconfig.json'); var gutil = require('gulp-util'); var argv = require('yargs').argv; var runSequence = require('run-sequence'); function getBuildPath() { return './out/'; } var config = { aliasify: { src: './.temp/' + argv.platform, dest: getBuildPath() + 'js/', aliases: (argv.platform === 'web') ? // Web Aliases { 'AppAlertAndPrompt': './AlertAndPromptWeb' } : // Native Aliases { 'AppAlertAndPrompt': './AlertAndPromptNative' } } } // Command line option: // --fatal=[warning|error|off] var fatalLevel = require('yargs').argv.fatal; var ERROR_LEVELS = ['error', 'warning']; // Return true if the given level is equal to or more severe than // the configured fatality error level. // If the fatalLevel is 'off', then this will always return false. // Defaults the fatalLevel to 'error'. function isFatal(level) { return ERROR_LEVELS.indexOf(level) <= ERROR_LEVELS.indexOf(fatalLevel || 'error'); } // Handle an error based on its severity level. // Log all levels, and exit the process for fatal levels. function handleError(level, error) { gutil.log(error.message); if (isFatal(level)) { process.exit(1); } } // Convenience handler for error-level errors. function onError(error) { handleError.call(this, 'error', error);} // Convenience handler for warning-level errors. function onWarning(error) { handleError.call(this, 'warning', error);} function aliasify(aliases) { var reqPattern = new RegExp(/require\(['"]([^'"]+)['"]\)/g); // For all files in the stream, apply the replacement. return eventStream.map(function(file, done) { if (!file.isNull()) { var fileContent = file.contents.toString(); if (reqPattern.test(fileContent)) { file.contents = new Buffer(fileContent.replace(reqPattern, function(req, oldPath) { if (!aliases[oldPath]) { return req; } return "require('" + aliases[oldPath] + "')"; })); } } done(null, file); }); } gulp.task('typescript', function() { return gulp.src("src/**/*.ts*") .pipe(tsNameof()) .pipe(tsProject()) .pipe(gulp.dest(config.aliasify.src)) .on('error', onError); }); gulp.task('apply-aliases', function() { return gulp.src(joinPath(config.aliasify.src, '**/*.js')) .pipe(aliasify(config.aliasify.aliases)) .pipe(gulp.dest(config.aliasify.dest)) .on('error', onError); }); gulp.task('clean-dest', function() { return gulp.src(config.aliasify.dest, {read: false}) .pipe(clean()); }); gulp.task('clean-src', function() { return gulp.src(config.aliasify.src, {read: false}) .pipe(clean()); }); gulp.task('run', function(callback) { runSequence(['clean-dest', 'clean-src'], 'typescript', 'apply-aliases', 'clean-src', 'bundle', callback); }); gulp.task('bundle', function() { if (argv.platform === 'web') return gulp.src(joinPath(config.aliasify.dest, 'index.js')) .pipe(webpack( require('./_____webpack.config.js') )) .pipe(gulp.dest('dist/')); else return gutil.noop(); });
My package.json scripts and devDependencies:
"scripts": { "clean": "watchman watch-del-all && rm -rf node_modules && npm install && rm -fr $TMPDIR/react-* && git checkout -q -- ./node_modules/react-native-prompt-android/index.d.ts", "gulp-web": "cross-env NODE_ENV=development gulp run --platform=web", "gulp-ios": "cross-env NODE_ENV=development gulp run --platform=ios", "gulp-android": "cross-env NODE_ENV=development gulp run --platform=android", "web": "webpack --display-error-details --progress --colors", "web-watch": "webpack --display-error-details --progress --colors --watch", "rn-watch": "node ts-nameofCustomProcess.js && tsc --watch", "start": "node node_modules/react-native/local-cli/cli.js start", "android": "node node_modules/react-native/local-cli/cli.js run-android", "ios": "node node_modules/react-native/local-cli/cli.js run-ios" }, "devDependencies": { "@types/node": "^7.0.12", "@types/webpack": "^2.2.14", "awesome-typescript-loader": "3.1.2", "babel-loader": "^7.1.2", "cross-env": "^5.1.1", "event-stream": "^3.3.4", "fs-extra": "^4.0.2", "gulp": "^3.9.1", "gulp-clean": "^0.3.2", "gulp-concat": "^2.6.1", "gulp-jshint": "^2.0.4", "gulp-typescript": "^3.2.3", "gulp-uglify": "^3.0.0", "jshint": "^2.9.5", "path.join": "^1.0.0", "rn-nodeify": "^8.2.0", "run-sequence": "^2.2.0", "source-map-loader": "^0.1.6", "ts-nameof-loader": "^1.0.1", "ts-node": "^3.2.1", "typescript": "2.4.1", "webpack": "2.2.1", "webpack-stream": "^4.0.0" },
Now if you run the following command in a terminal window:
npm run gulp-web
you should get something similar to the following output:
> cross-env NODE_ENV=development gulp run --platform=web [10:00:21] Using gulpfile ~/Projects/trash/reactxp/samples/hello-world/gulpfile.js [10:00:21] Starting 'run'... [10:00:21] Starting 'clean-dest'... [10:00:21] Starting 'clean-src'... [10:00:21] Finished 'clean-src' after 13 ms [10:00:21] Finished 'clean-dest' after 46 ms [10:00:21] Starting 'typescript'... [10:00:27] Finished 'typescript' after 6.29 s [10:00:27] Starting 'apply-aliases'... [10:00:28] Finished 'apply-aliases' after 120 ms [10:00:28] Starting 'clean-src'... [10:00:28] Finished 'clean-src' after 14 ms [10:00:28] Starting 'bundle'... [10:00:31] Version: webpack 3.10.0 Asset Size Chunks Chunk Names bundle.js 3.07 MB 0 [emitted] [big] main [8] ./node_modules/reactxp/index.js 146 bytes {0} [built] ... + 491 hidden modules [10:00:31] Finished 'bundle' after 3.38 s [10:00:31] Finished 'run' after 9.87 s
I’ll let you play with the native target scripts.
Now, there is one missing part though. The ability to watch .ts(x) files for modifications to be able to recompile and reload the project.
This will be our next goal in part 2 of this series about Gulp and ReactXP. Stay tuned.
One thought on “Using Gulp with ReactXP (Part 1)”