diff --git a/README.md b/README.md index 1b57a33..eb13813 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# reactive +# Reactive Biased generator for react apps. Supports sass and jade. diff --git a/generators/app/index.js b/generators/app/index.js new file mode 100644 index 0000000..c4effbf --- /dev/null +++ b/generators/app/index.js @@ -0,0 +1,96 @@ +'use strict'; + +var yo = require('yeoman-generator'), + yosay = require('yosay'); + +module.exports = yo.generators.Base.extend({ + constructor: function() { + yo.generators.Base.apply(this, arguments); + }, + + initializing: function() { + this.pkg = require('../../package.json'); + }, + + prompting: function() { + var done = this.async(); + + this.log(yosay('\'Allo! Devs! Reactive does lot of stuff.')); + + this.prompt([{ + type: 'input', + name: 'rname', + message: 'Your project name', + default: this.appname // Default to current folder name + }, { + type: 'input', + name: 'rauthor', + message: 'Author name', + default: 'Reactive' // Defaults to Reactive + }], function(answers) { + this.rname = answers.rname; + this.rauthor = answers.rauthor; + done(); + }.bind(this)); + }, + + writing: { + appJSON: function() { + this.template('app.json', 'app.json'); + }, + git: function() { + this.copy('gitignore', '.gitignore'); + this.copy('gitattributes', '.gitattributes'); + }, + bower: function() { + // this.copy('bower.json', 'bower.json'); + this.copy('bowerrc', '.bowerrc'); + }, + packageJSON: function() { + this.template('package.json', 'package.json'); + }, + reactive: function() { + this.mkdir('.reactive'); + this.mkdir('.reactive/tpl'); + this.copy('gulpfile.js', 'gulpfile.js'); + this.copy('component.js', '.reactive/component.js'); + this.copy('compile.js', '.reactive/compile.js'); + this.copy('captain.js', '.reactive/captain.js'); + this.copy('util.js', '.reactive/util.js'); + this.copy('component.json.tpl', '.reactive/tpl/component.json.tpl'); + this.copy('component.jsx.tpl', '.reactive/tpl/component.jsx.tpl'); + this.copy('trigger.jsx.tpl', '.reactive/tpl/trigger.jsx.tpl'); + this.copy('component.jade.tpl', '.reactive/tpl/component.jade.tpl'); + this.copy('component.scss.tpl', '.reactive/tpl/component.scss.tpl'); + }, + components: function() { + this.mkdir('src/components'); + this.mkdir('src/components/Yo'); + this.mkdir('src/components/Yo/src'); + this.mkdir('src/components/Yo/test'); + this.mkdir('src/components/Yo/src/jsx'); + // this.copy('component_index.jade', 'src/components/yo/src/index.jade') + }, + src: function() { + this.mkdir('src/actions'); + this.mkdir('src/constants'); + this.mkdir('src/dispatchers'); + this.mkdir('src/layouts'); + this.mkdir('src/media'); + this.mkdir('src/media/styles'); + this.mkdir('src/media/images'); + this.mkdir('src/media/fonts'); + this.mkdir('src/mixins'); + this.mkdir('src/stores'); + this.mkdir('src/utility'); + }, + app: function() { + // add app directories + this.mkdir('src'); + this.mkdir('.tmp'); + this.mkdir('.tmp/app'); + this.mkdir('.tmp/components'); + this.mkdir('test'); + } + } +}); diff --git a/generators/app/templates/app.json b/generators/app/templates/app.json new file mode 100644 index 0000000..9310df5 --- /dev/null +++ b/generators/app/templates/app.json @@ -0,0 +1,46 @@ +{ + "name": "<%= rname %>", + "author": "<%= rauthor %>", + "dependencies": { + "js-x": ["actions", "constants", "dispatchers", "mixins", "stores", "utility", "layouts"], + "s-css": ["media/styles/base", "media/styles"], + "components": ["Yo"] + }, + "components":{ + "basePath": "src/components", + "tempDir": ".tmp/components", + "tpl" : { + "folders" : ["src", "src/jsx", "test"], + "files" : [ + { + "name": "component", + "type": "json", + "path": ".", + "content": "tpl/component.json.tpl" + }, + { + "type" : "jsx", + "path": "src/jsx", + "content": "tpl/component.jsx.tpl" + }, + { + "name" : "trigger", + "type" : "jsx", + "path": "src/jsx", + "content": "tpl/trigger.jsx.tpl" + }, + { + "name" : "index", + "type" : "jade", + "path": "src", + "content": "tpl/component.jade.tpl" + }, + { + "type" : "scss", + "path": "src", + "content": "tpl/component.scss.tpl" + } + ] + } + } +} diff --git a/generators/app/templates/bower.json b/generators/app/templates/bower.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/generators/app/templates/bower.json @@ -0,0 +1 @@ +{} diff --git a/generators/app/templates/bowerrc b/generators/app/templates/bowerrc new file mode 100644 index 0000000..04f7ec3 --- /dev/null +++ b/generators/app/templates/bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "src/media/bower_components" +} diff --git a/generators/app/templates/captain.js b/generators/app/templates/captain.js new file mode 100644 index 0000000..dde23ee --- /dev/null +++ b/generators/app/templates/captain.js @@ -0,0 +1,112 @@ +'use strict'; + +var path = require('path'), + fs = require('fs'), + gulp = require('gulp'), + compile = require('./compile.js'), + util = require('./util.js'), + connect = require('gulp-connect'), + gutil = require('gulp-util'), + lodash = require('lodash'), + config = require('../app.json'); + +var baseDir = path.resolve(__dirname, '../'), + appDir = path.join(baseDir, 'src'), + tempDir = path.join(baseDir, '/.tmp', 'app'); + +var Captain = function() {}; + +Captain.prototype.watch = function() { + try { + fs.mkdirSync(path.join(baseDir + '/.tmp')); + } catch (e) { + if (e.code != 'EEXIST') { + gutil.log(gutil.colors.red('App watch failed.', e)); + return; + } + } + + try { + util.rmdir(tempDir); + } catch (e) { + if (e.code != 'ENOENT') { + gutil.log(gutil.colors.red('App watch failed.', e)); + return; + } + } + + try { + fs.mkdirSync(tempDir); + fs.mkdirSync(path.join(tempDir, 'css')); + fs.mkdirSync(path.join(tempDir, 'scripts')); + } catch (e) { + if (e.code != 'EEXIST') { + gutil.log(gutil.colors.red('App watch failed.', e)); + return; + } + } + + var _appCss = lodash.map(config.dependencies['s-css'], function(dep) { + return path.join(appDir, dep, '/*.scss'); + }); + + var _componentCss = lodash.map(config.dependencies.components, function(dep) { + return path.join(appDir, 'components', dep, 'src/*.scss'); + }); + + var _cssDeps = _appCss.concat(_componentCss); + + gulp.task('compile-scss', function() { + return compile.scss({ + sources: _cssDeps, + destination: path.join(tempDir, 'css'), + }); + }); + + /* Note: Here we compile only trigger.jsx because trigger is only used for + * development phase which actually includes the component */ + gulp.task('compile-jsx', function() { + return compile.jsx({ + source: path.join(appDir, 'boot.jsx'), + destination: path.join(tempDir, 'scripts') + }, 'app'); + }); + + gulp.task('compile-jade', ['compile-scss'], function() { + return compile.jade({ + ignore: '.tmp/app', + inject: path.join(tempDir, 'css', '/*.css'), + source: path.join(appDir, 'index.jade'), + destination: tempDir + }); + }); + + gulp.task('connect', function() { + util.getPort(9000, function(port) { + connect.server({ + root: tempDir, + port: port, + livereload: true + }); + gutil.log(gutil.colors.green('Watching app for changes.')); + }); + }); + + gulp.task('watch', function() { + // Watch current component jade + gulp.watch([path.join(appDir, '/*.jade')], ['compile-jade']); + + // Watch app & component scss + gulp.watch(_cssDeps, ['compile-scss', 'compile-jade']); + + // Watch app & component jsx + }); + + gulp.start('compile-scss', 'compile-jade', 'compile-jsx', 'watch', 'connect'); +}; + +Captain.prototype.build = function() {}; + +Captain.prototype.test = function() {}; + +module.exports = Captain; diff --git a/generators/app/templates/compile.js b/generators/app/templates/compile.js new file mode 100644 index 0000000..6aab12f --- /dev/null +++ b/generators/app/templates/compile.js @@ -0,0 +1,47 @@ +'use strict'; + +var gulp = require('gulp'), + jade = require('gulp-jade'), + concat = require('gulp-concat'), + connect = require('gulp-connect'), + browserify = require('gulp-browserify'), + gutil = require('gulp-util'), + sass = require('gulp-sass'), + inject = require('gulp-inject'), + util = require('./util.js'); + +module.exports = { + scss: function(paths) { + return gulp.src(paths.sources) + .pipe(sass()) + .pipe(connect.reload()) + .pipe(gulp.dest(paths.destination)); + }, + + jade: function(paths) { + return gulp.src(paths.source) + .pipe(inject( + gulp.src(paths.inject, {read: false}, {relative: true}), + { + ignorePath: paths.ignore, + addRootSlash: false + } + )) + .pipe(jade({ + locals: {}, + pretty: true + })) + .pipe(connect.reload()) + .pipe(gulp.dest(paths.destination)); + }, + + jsx: function(paths, _component) { + return gulp.src(paths.source) + .pipe(browserify({ + transform: 'reactify' + })) + .pipe(concat(_component + '.js')) + .pipe(connect.reload()) + .pipe(gulp.dest(paths.destination)); + } +}; diff --git a/generators/app/templates/component.jade.tpl b/generators/app/templates/component.jade.tpl new file mode 100644 index 0000000..291a691 --- /dev/null +++ b/generators/app/templates/component.jade.tpl @@ -0,0 +1,10 @@ +html + head + // Can't touch this! Or it will go BOOOM. + //- inject:css + //- endinject + // Aye! Captain. + + body + #main-wrapper + script(src='scripts/trigger.js') diff --git a/generators/app/templates/component.js b/generators/app/templates/component.js new file mode 100644 index 0000000..7255e16 --- /dev/null +++ b/generators/app/templates/component.js @@ -0,0 +1,235 @@ +'use strict'; + +var fs = require('fs'), + path = require('path'), + lodash = require('lodash'), + gutil = require('gulp-util'), + gulp = require('gulp'), + net = require('net'), + connect = require('gulp-connect'), + compile = require('./compile.js'), + util = require('./util.js'), + config = require('../app.json').components; + +var baseDir = path.resolve(__dirname, '../'); + +var Component = function() {}; + +Component.prototype.add = function(_component) { + var _componentPath = path.join(path.join(baseDir, config.basePath), _component); + + try { + fs.mkdirSync(_componentPath); + } catch (e) { + if (e.code == 'EEXIST') { + gutil.log(gutil.colors.red(_component + ' exits in ' + config.basePath)); + } else { + gutil.log(gutil.colors.red(_component + ' couldn\'t be added ', e)); + } + return; + } + + config.tpl.folders.forEach(function(foldername) { + fs.mkdirSync(path.join(_componentPath, foldername)); + }); + + config.tpl.files.forEach(function(file) { + var _path = path.join(_componentPath, file.path); + + var filename = (file.name ? file.name : _component) + '.' + file.type; + + var template = fs.readFileSync(path.join(__dirname, file.content), 'utf8'); + fs.writeFile( + path.join(_path, filename), + lodash.template(template)({ + name: _component.charAt(0).toUpperCase() + _component.slice(1) + }), + function(e) { + if (e) { + gutil.log(e); + } + } + ); + }); +}; + +Component.prototype.remove = function(_component) { + var _componentPath = path.join(path.join(baseDir, config.basePath), _component); + try { + util.rmdir(_componentPath); + } catch (e) { + if (e.code == 'ENOENT') { + gutil.log(gutil.colors.red(_component + ' doesn\'t exist at ' + config.basePath)); + } else { + gutil.log(gutil.colors.red(_component + ' remove failed.', e)); + } + return; + } + + gutil.log(_component + ' removed.'); +}; + +Component.prototype.info = function(_component) { + var _componentPath = path.join(path.join(baseDir, config.basePath), _component); + try { + var _jsonFile = fs.readFileSync(path.join(_componentPath, 'component.json'), 'utf8'); + } catch (e) { + gutil.log(gutil.colors.red(_component + ' info failed.', e)); + return; + } + + gutil.log(_jsonFile); +}; + +Component.prototype.watch = function(_component) { + var _baseDir = path.join(baseDir, config.basePath), + _componentDir = path.join(_baseDir, _component), + _tempDir = path.join(baseDir, config.tempDir), + _tempComponentDir = path.join(_tempDir, _component); + + try { + var _depMap = {}; + getDep(_baseDir, _component, _depMap); + } catch (e) { + gutil.log(gutil.colors.red(_component + ' watch failed.', e)); + return; + } + + try { + fs.mkdirSync(path.join(baseDir + '/.tmp')); + } catch (e) { + if (e.code != 'EEXIST') { + gutil.log(gutil.colors.red(_component + ' watch failed.', e)); + return; + } + } + + try { + fs.mkdirSync(_tempDir); + } catch (e) { + if (e.code != 'EEXIST') { + gutil.log(gutil.colors.red(_component + ' watch failed.', e)); + return; + } + } + + try { + util.rmdir(_tempComponentDir); + } catch (e) { + if (e.code != 'ENOENT') { + gutil.log(gutil.colors.red(_component + ' watch failed.', e)); + return; + } + } + + try { + fs.mkdirSync(_tempComponentDir); + fs.mkdirSync(path.join(_tempComponentDir, 'css')); + fs.mkdirSync(path.join(_tempComponentDir, 'scripts')); + } catch (e) { + gutil.log(gutil.colors.red(_component + ' watch failed.', e)); + return; + } + + // Get components + var _deps = Object.keys(_depMap); + + // extract dependencies + var _jsxDeps = [], + _importSass = [], + _importJsx = [], + _sassDeps = []; + + var _tempSass = [], + _tempJsx = []; + + _deps.forEach(function(_dep) { + _jsxDeps.push(path.join(_baseDir, _dep, '/src/jsx/*.jsx'), path.join(_baseDir, _dep, '/src/jsx/*.js')); + _sassDeps.push(path.join(_baseDir, _dep, '/src/*.scss'), path.join(_baseDir, _dep, '/src/*.css')); + _tempSass = lodash.uniq(_tempSass.concat(_depMap[_dep]['s-css'])); + _tempJsx = lodash.uniq(_tempJsx.concat(_depMap[_dep]['js-x'])); + }); + + _tempSass.forEach(function(_path) { + _path = path.join(_componentDir, _path); + _importSass.push(path.join(_path, '/*.scss'), path.join(_path, '/*.css')); + }); + + _tempJsx.forEach(function(_path) { + _path = path.join(_componentDir, _path); + _importJsx.push(path.join(_path, '/*.jsx'), path.join(_path, '/*.js')); + }); + + gulp.task('compile-scss', function() { + return compile.scss({ + sources: _sassDeps.concat(_importSass), + destination: path.join(_tempComponentDir, 'css') + }); + }); + + /* Note: Here we compile only trigger.jsx because trigger is only used for + * development phase which actually includes the component */ + gulp.task('compile-jsx', function() { + return compile.jsx({ + source: path.join(_componentDir, 'src/jsx', 'trigger.jsx'), + destination: path.join(_tempComponentDir, 'scripts') + }, 'trigger'); + }); + + gulp.task('compile-jade', ['compile-scss'], function() { + return compile.jade({ + ignore: '.tmp/components/' + _component, + inject: path.join(_tempComponentDir, 'css', '/*.css'), + source: path.join(_componentDir, 'src/*.jade'), + destination: _tempComponentDir + }); + }); + + gulp.task('watch', function() { + // Watch current component jade + gulp.watch([path.join(_componentDir, '/src/*.jade')], ['compile-jade']); + + // Watch current and dependency component jsx + gulp.watch(_jsxDeps, ['compile-jsx']); + + // Watch current and dependency component sass + gulp.watch(_sassDeps, ['compile-scss']); + + // Watch external js & jsx + gulp.watch(_importJsx, ['compile-jsx']); + + // Watch external sass + gulp.watch(_importSass, ['compile-scss']); + }); + + gulp.task('connect', function() { + util.getPort(9000, function(port) { + connect.server({ + root: _tempComponentDir, + port: port, + livereload: true + }); + gutil.log(gutil.colors.green('Watching component - ' + _component)); + }); + }); + + gulp.start('compile-scss', 'compile-jade', 'compile-jsx', 'watch', 'connect'); +}; + +Component.prototype.test = function(_component) {}; + +Component.prototype.coverage = function(_component) {}; + +// Recursive dependency check +var getDep = function(_baseDir, _component, _depMap) { + var _jsonFile = fs.readFileSync(path.join(_baseDir, _component, 'component.json'), 'utf8'); + var _componentInfo = JSON.parse(_jsonFile); + _depMap[_component] = _componentInfo.dependencies; + for (var i = 0; i < _componentInfo.dependencies.components.length; i++) { + if (!_depMap[_componentInfo.dependencies.components[i]]) { + getDep(_baseDir, _componentInfo.dependencies.components[i], _depMap); + } + } +}; + +module.exports = Component; diff --git a/generators/app/templates/component.json.tpl b/generators/app/templates/component.json.tpl new file mode 100644 index 0000000..b631e98 --- /dev/null +++ b/generators/app/templates/component.json.tpl @@ -0,0 +1,11 @@ +{ + "name" : "<%= name %>", + "author" : "<%= author %>", + "keywords" : [], + "description" : "", + "dependencies" : { + "s-css" : [], + "js-x": [], + "components" : [] + } +} diff --git a/generators/app/templates/component.jsx.tpl b/generators/app/templates/component.jsx.tpl new file mode 100644 index 0000000..12aa39f --- /dev/null +++ b/generators/app/templates/component.jsx.tpl @@ -0,0 +1,15 @@ +'use strict'; + +var React = require('react/addons'); + +var <%= name %> = React.createClass({ + getInitialState: function() { + return {}; + }, + + render: function() { + return (
); + } +}); + +module.exports = <%= name %>; diff --git a/generators/app/templates/component.scss.tpl b/generators/app/templates/component.scss.tpl new file mode 100644 index 0000000..e69de29 diff --git a/generators/app/templates/gitattributes b/generators/app/templates/gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/generators/app/templates/gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/generators/app/templates/gitignore b/generators/app/templates/gitignore new file mode 100644 index 0000000..129aeea --- /dev/null +++ b/generators/app/templates/gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.tmp +.sass-cache +src/media/bower_components +test/bower_components diff --git a/generators/app/templates/gulpfile.js b/generators/app/templates/gulpfile.js new file mode 100644 index 0000000..4b06906 --- /dev/null +++ b/generators/app/templates/gulpfile.js @@ -0,0 +1,41 @@ +'use strict'; + +var gulp = require('gulp'), + gulpCommand = require('gulp-command')(gulp); + +var Component = new(require('./.reactive/component.js')), + Captain = new(require('./.reactive/captain.js')); + +gulp + .option('component', '-a, --add ', 'New Component') + .option('component', '-r, --remove', 'Delete Component') + .option('component', '-i, --info', 'Component Information') + .option('component', '-w, --watch', 'Watch Component Development') + .option('component', '-t, --test', 'Run tests for the Component') + .option('component', '-c, --coverage', 'Run code coverage') + .task('component', function() { + if (this.flags.add) { + return Component.add(this.flags.add); + } else if (this.flags.remove) { + return Component.remove(this.flags.remove); + } else if (this.flags.info) { + return Component.info(this.flags.info); + } else if (this.flags.watch) { + return Component.watch(this.flags.watch); + } else if (this.flags.test) { + return Component.test(this.flags.test); + } else if (this.flags.coverage) { + return Component.coverage(this.flags.test); + } + }); + +gulp + .task('watch', function() { + Captain.watch(); + }) + .task('build', function() { + Captain.build(); + }) + .task('test', function() { + Captain.test(); + }); diff --git a/generators/app/templates/package.json b/generators/app/templates/package.json new file mode 100644 index 0000000..75a2f57 --- /dev/null +++ b/generators/app/templates/package.json @@ -0,0 +1,21 @@ +{ + "name": "<%= rname %>", + "author": "<%= rauthor %>", + "version": "0.0.1", + "private": true, + "engines": { + "node": ">=0.10.0" + }, + "devDependencies": { + "gulp": "^3.8.10", + "gulp-connect": "^2.2.0", + "gulp-sass": "^1.3.3", + "gulp-command": "^0.1.1", + "gulp-inject": "^1.2.0", + "gulp-concat": "^2.5.0", + "gulp-util": "^3.0.3", + "gulp-jade": "^0.11.0", + "lodash": "^3.2.0", + "map-stream": "0.0.5" + } +} diff --git a/generators/app/templates/trigger.jsx.tpl b/generators/app/templates/trigger.jsx.tpl new file mode 100644 index 0000000..265df8c --- /dev/null +++ b/generators/app/templates/trigger.jsx.tpl @@ -0,0 +1,5 @@ +var React = require('react'); + +var <%= name %> = require('./<%= name %>.jsx'); + +React.renderComponent(<<%= name %>/>, document.getElementById('main-wrapper')); diff --git a/generators/app/templates/util.js b/generators/app/templates/util.js new file mode 100644 index 0000000..3c9e172 --- /dev/null +++ b/generators/app/templates/util.js @@ -0,0 +1,48 @@ +'use strict'; + +var net = require('net'), + path = require('path'), + gutil = require('gulp-util'), + fs = require('fs'), + map = require('map-stream'); + +// Get free port +var getPort = exports.getPort = function(port, cb) { + var server = net.createServer(); + server.listen(port, function(err) { + server.once('close', function() { + cb(port); + }); + server.close(); + }); + server.on('error', function(err) { + getPort(port + 1, cb); + }); +}; + +// Recursive directory remove +var rmdir = exports.rmdir = function(dir) { + var list = fs.readdirSync(dir); + for (var i = 0; i < list.length; i++) { + var filename = path.join(dir, list[i]); + var stat = fs.statSync(filename); + + if (filename == '.' || filename == '..') { + // pass these files + } else if (stat.isDirectory()) { + // rmdir recursively + rmdir(filename); + } else { + // rm fiilename + fs.unlinkSync(filename); + } + } + fs.rmdirSync(dir); +}; + +var using = exports.using = function() { + return map(function(file, cb) { + gutil.log('Watching ' + gutil.colors.green(file.path.replace(file.cwd + '/', ''))); + cb(null, file); + }); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..266d2ce --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "generator-reactive", + "version": "0.0.1", + "description": "All simple generator for react apps. Supports sass and jade.", + "main": "index.js", + "keywords": [ + "yeoman-generator", + "react", + "reactjs", + "sass", + "jade" + ], + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Babaji", + "license": "unlicense", + "dependencies": { + "yeoman-generator": "^0.18.8", + "yosay": "^1.0.2" + } +}