├── .gitignore ├── lib ├── templates │ └── templates │ │ ├── client │ │ ├── main.js │ │ └── head.html │ │ ├── settings.json │ │ ├── imports │ │ ├── startup │ │ │ ├── client │ │ │ │ ├── index.js │ │ │ │ └── routes.js │ │ │ └── server │ │ │ │ ├── index.js │ │ │ │ └── fixtures.js │ │ └── ui │ │ │ ├── pages │ │ │ └── HomePage │ │ │ │ ├── index.js │ │ │ │ └── HomePage.jsx │ │ │ └── layouts │ │ │ └── AppLayout.jsx │ │ ├── template.html │ │ ├── admin-config.json │ │ ├── route.js │ │ ├── env.sh │ │ ├── template.js │ │ ├── package.js │ │ ├── page.jsx │ │ └── config.json ├── io.js ├── commands │ ├── generate.js │ ├── run.js │ ├── setup.js │ └── create.js ├── utilities │ ├── meteor-commands.js │ ├── logger.js │ └── file-system.js ├── generators │ ├── page.js │ ├── cmd.js │ ├── generator.js │ ├── template.js │ ├── route.js │ └── package.js ├── utils.js └── command.js ├── bin └── io ├── config ├── command.js └── generator.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | temp/ 2 | node_modules/ 3 | config/config.json 4 | .jscsrc 5 | -------------------------------------------------------------------------------- /lib/templates/templates/client/main.js: -------------------------------------------------------------------------------- 1 | import '/imports/startup/client'; 2 | -------------------------------------------------------------------------------- /lib/templates/templates/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "public": { 3 | } 4 | } 5 | -------------------------------------------------------------------------------- /lib/templates/templates/imports/startup/client/index.js: -------------------------------------------------------------------------------- 1 | import './routes.js'; 2 | -------------------------------------------------------------------------------- /lib/templates/templates/template.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/templates/templates/admin-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": { 3 | }, 4 | "generators": { 5 | } 6 | } -------------------------------------------------------------------------------- /lib/templates/templates/imports/ui/pages/HomePage/index.js: -------------------------------------------------------------------------------- 1 | export default HomePage from './HomePage.jsx'; 2 | -------------------------------------------------------------------------------- /lib/templates/templates/imports/startup/server/index.js: -------------------------------------------------------------------------------- 1 | // This defines a starting set of data to be loaded if the app is loaded with an empty db. 2 | import './fixtures.js'; 3 | -------------------------------------------------------------------------------- /bin/io: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | const args = argv._; 5 | 6 | const Io = require('../lib/io'); 7 | const io = Io.run(args, argv); 8 | -------------------------------------------------------------------------------- /lib/templates/templates/imports/startup/server/fixtures.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | 3 | // If the database is empty on server start, create some sample data here. 4 | Meteor.startup(() => { 5 | }); 6 | -------------------------------------------------------------------------------- /lib/templates/templates/route.js: -------------------------------------------------------------------------------- 1 | 2 | FlowRouter.route('/{{name}}', { 3 | name: '{{camelName}}', 4 | action() { 5 | mount(Layout, { 6 | content: (<{{pageName}} />) 7 | }); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /lib/io.js: -------------------------------------------------------------------------------- 1 | var Command = require('./command'); 2 | var requireDir = require('require-dir'); 3 | var commands = requireDir('./commands'); 4 | var Ion = new Command({ name: 'Template', 5 | commands: commands }); 6 | 7 | module.exports = Ion; 8 | -------------------------------------------------------------------------------- /lib/templates/templates/env.sh: -------------------------------------------------------------------------------- 1 | # If you use the io command to run your app 2 | # these environment variables will automatically be 3 | # imported into process.evn in your local environment. 4 | 5 | # Example: 6 | # MAIL_URL="smtp://username:password@smtp-url:port" 7 | -------------------------------------------------------------------------------- /lib/templates/templates/template.js: -------------------------------------------------------------------------------- 1 | Template.{{name}}.events({ 2 | }); 3 | 4 | Template.{{name}}.helpers({ 5 | }); 6 | 7 | Template.{{name}}.onCreated(function() { 8 | }); 9 | 10 | Template.{{name}}.onRendered(function() { 11 | }); 12 | 13 | Template.{{name}}.onDestroyed(function() { 14 | }); 15 | -------------------------------------------------------------------------------- /lib/templates/templates/imports/ui/layouts/AppLayout.jsx: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import React, { Component } from 'react'; 3 | import { createContainer } from 'meteor/react-meteor-data'; 4 | 5 | export const AppLayout = ({content}) => ( 6 |
7 |
8 | This is our header 9 |
10 |
11 | {content} 12 |
13 |
14 | ); 15 | -------------------------------------------------------------------------------- /lib/templates/templates/client/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/commands/generate.js: -------------------------------------------------------------------------------- 1 | var Command = require('../command'); 2 | 3 | class Generate extends Command { 4 | constructor() { 5 | super(); 6 | this.alias = 'g'; 7 | this.name = 'generate'; 8 | this.description = 'io {g, generate}:generator '; 9 | } 10 | run(args, argv) { 11 | var generator = this.findGenerator(args[0]); 12 | this.runGenerator(generator, args, argv); 13 | } 14 | } 15 | 16 | module.exports = new Generate; 17 | -------------------------------------------------------------------------------- /lib/templates/templates/imports/startup/client/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlowRouter } from 'meteor/kadira:flow-router'; 3 | import { mount } from 'react-mounter'; 4 | import { AppLayout } from '../../ui/layouts/AppLayout'; 5 | import HomePage from '../../ui/pages/HomePage'; 6 | 7 | FlowRouter.route('/', { 8 | name: 'home', 9 | action() { 10 | mount(AppLayout, { 11 | content: () 12 | }); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /lib/utilities/meteor-commands.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'debug', 3 | 'update', 4 | 'add', 5 | 'remove', 6 | 'list', 7 | 'add-platform', 8 | 'install-sdk', 9 | 'remove-platform', 10 | 'list-platforms', 11 | 'configure-android', 12 | 'build', 13 | 'shell', 14 | 'mongo', 15 | 'reset', 16 | 'deploy', 17 | 'logs', 18 | 'authorized', 19 | 'claim', 20 | 'login', 21 | 'logout', 22 | 'whoami', 23 | 'test-packages', 24 | 'admin', 25 | 'list-sites', 26 | 'publish-release', 27 | 'publish', 28 | 'publish-for-arch', 29 | 'search', 30 | 'show', 31 | 'npm' 32 | ]; 33 | -------------------------------------------------------------------------------- /config/command.js: -------------------------------------------------------------------------------- 1 | const Command = require('{{commandPath}}'); 2 | const path = require('path'); 3 | 4 | class {{className}} extends Command { 5 | constructor () { 6 | super(); 7 | // A shorter name to call your generator with 8 | this.alias = '{{alias}}'; 9 | // This name should match the file name without the extension 10 | this.name = '{{name}}'; 11 | // A brief description on how to use 12 | this.description = 'io {{name}} [args], io {{alias}} [args]'; 13 | } 14 | run(args, argv) { 15 | // Command code here 16 | } 17 | } 18 | 19 | module.exports = new {{className}}; 20 | -------------------------------------------------------------------------------- /config/generator.js: -------------------------------------------------------------------------------- 1 | const Command = require('{{commandPath}}'); 2 | const path = require('path'); 3 | 4 | class {{className}} extends Command { 5 | constructor () { 6 | super(); 7 | // A shorter name to call your generator with 8 | this.alias = '{{alias}}'; 9 | // This name should match the file name without the extension 10 | this.name = '{{name}}'; 11 | // A brief description on how to use 12 | this.description = 'io g:{{name}} , io g:{{alias}} '; 13 | } 14 | run(args, argv) { 15 | // Generator code here 16 | } 17 | } 18 | 19 | module.exports = new {{className}}; 20 | -------------------------------------------------------------------------------- /lib/templates/templates/imports/ui/pages/HomePage/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import React, { Component } from 'react'; 3 | import { createContainer } from 'meteor/react-meteor-data'; 4 | 5 | class Home extends Component { 6 | render() { 7 | return ( 8 |
9 |

Home

10 |
11 | ) 12 | } 13 | } 14 | 15 | Home.propTypes = {}; 16 | 17 | export default HomePage = createContainer(props => { 18 | // Props here will have `main`, passed from the router 19 | // anything we return from this function will be *added* to it 20 | return { 21 | // user: Meteor.user(), 22 | }; 23 | }, Home); 24 | -------------------------------------------------------------------------------- /lib/commands/run.js: -------------------------------------------------------------------------------- 1 | var Command = require('../command'); 2 | var path = require('path'); 3 | 4 | class Run extends Command { 5 | constructor() { 6 | super(); 7 | this.name = 'run'; 8 | this.description = 'io'; 9 | } 10 | run(args, argv) { 11 | var isRun = false; 12 | 13 | if (!args.length || (args.length && args[0] === 'run')) { 14 | isRun = true; 15 | } 16 | 17 | var args = args || []; 18 | 19 | if (argv.port) { 20 | args.push('--port', argv.port); 21 | } 22 | 23 | if (this.findProjectDir()) { 24 | if (isRun) { 25 | this.runMeteor(args); 26 | } else { 27 | this.runMeteorCommand(args); 28 | } 29 | 30 | } 31 | } 32 | } 33 | 34 | module.exports = new Run; 35 | -------------------------------------------------------------------------------- /lib/templates/templates/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: '{{packageName}}', 3 | version: '0.0.1', 4 | // Brief, one-line summary of the package. 5 | summary: '', 6 | // URL to the Git repository containing the source code for this package. 7 | git: '', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.versionsFrom('{{meteorVersion}}'); 15 | api.use('ecmascript'); 16 | 17 | api.addFiles('lib/client/templates/{{htmlDest}}', 'client'); 18 | api.addFiles('lib/client/templates/{{jsDest}}', 'client'); 19 | }); 20 | 21 | Package.onTest(function(api) { 22 | }); 23 | -------------------------------------------------------------------------------- /lib/templates/templates/page.jsx: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import React, { Component } from 'react'; 3 | import { createContainer } from 'meteor/react-meteor-data'; 4 | 5 | class {{shortName}} extends Component { 6 | render() { 7 | return ( 8 |
9 |

{{shortName}}

10 |
11 | ) 12 | } 13 | } 14 | 15 | {{shortName}}.propTypes = {}; 16 | 17 | export default {{longName}} = createContainer(props => { 18 | // Props here will have `main`, passed from the router 19 | // anything we return from this function will be *added* to it 20 | 21 | // const handle = Meteor.subscribe(); 22 | 23 | return { 24 | // user: Meteor.user(), 25 | // isLoading: ! handle.ready(), 26 | }; 27 | }, {{shortName}}); 28 | -------------------------------------------------------------------------------- /lib/generators/page.js: -------------------------------------------------------------------------------- 1 | const Command = require('../command'); 2 | 3 | class Page extends Command { 4 | constructor() { 5 | super(); 6 | // A shorter name to call your generator with. 7 | this.alias = 'pg'; 8 | // This name should match the file name without the extension. 9 | this.name = 'page'; 10 | // A brief description on how to use this. 11 | this.description = 'io g:{page, pg} [path/]'; 12 | } 13 | run(args, argv) { 14 | if (args.length < 2) { 15 | throw new Error('Requires two args'); 16 | } 17 | 18 | const fileBaseName = this.getBaseName(args[1]); 19 | const className = this.classify(fileBaseName); 20 | 21 | this.writeTemplatesWithData({ 22 | src: 'page.jsx', 23 | dest: [this.config.dest, className + 'Page.jsx'], 24 | data: { shortName: className, longName: className + 'Page' } 25 | }); 26 | } 27 | } 28 | 29 | module.exports = new Page; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meteor-io", 3 | "version": "1.0.4", 4 | "description": "A tool for Meteor to facilitate development", 5 | "homepage": "https://github.com/scottmcpherson/meteor-io", 6 | "bugs": { 7 | "url": "https://github.com/scottmcpherson/meteor-io/issues", 8 | "email": "scott_fl@me.com" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "author": "Scott McPherson", 14 | "license": "ISC", 15 | "bin": { 16 | "io": "./bin/io" 17 | }, 18 | "dependencies": { 19 | "cli-color": "1.0.0", 20 | "cli-table": "0.3.1", 21 | "dotenv": "1.2.0", 22 | "fs-extra": "0.22.1", 23 | "handlebars": "3.0.3", 24 | "lodash": "^3.10.1", 25 | "minimist": "1.1.2", 26 | "readline-sync": "1.2.20", 27 | "require-dir": "0.3.0", 28 | "single-line-log": "1.0.0", 29 | "underscore": "1.8.3", 30 | "data-store": "0.11.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/templates/templates/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": { 3 | "create": { 4 | "packages": { 5 | "add": [ 6 | "kadira:flow-router", 7 | "react-meteor-data" 8 | ], 9 | "remove": [ 10 | "autopublish", 11 | "insecure" 12 | ] 13 | }, 14 | "npm": { 15 | "install": [ 16 | "meteor-node-stubs", 17 | "react-mounter", 18 | "react", 19 | "react-dom", 20 | "react-addons-pure-render-mixin" 21 | ] 22 | } 23 | } 24 | }, 25 | "generators": { 26 | "template": { 27 | "dest": "$APP_PATH/client/templates/$DEST_PATH" 28 | }, 29 | "route": { 30 | "dest": "$APP_PATH/imports/startup/client/routes.js" 31 | }, 32 | "component": { 33 | "dest": "$APP_PATH/imports/ui/components/$DEST_PATH" 34 | }, 35 | "page": { 36 | "dest": "$APP_PATH/imports/ui/pages" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/generators/cmd.js: -------------------------------------------------------------------------------- 1 | const Command = require('../command'); 2 | 3 | class Cmd extends Command { 4 | constructor() { 5 | super(); 6 | this.alias = 'cmd'; 7 | this.name = 'cmd'; 8 | this.description = 'io g:cmd '; 9 | } 10 | run(args, argv) { 11 | var self = this; 12 | var commandName = args[1]; 13 | var commandFileName = commandName + '.js'; 14 | var className = this.classify(commandName); 15 | var userIoCommandPath = this.join(self.ioPath, 'commands', commandFileName); 16 | 17 | this.writeTemplateWithData({ 18 | src: [self._adminConfigPath, 'command.js'], 19 | dest: userIoCommandPath, 20 | data: { 21 | commandPath: self._adminCommandPath, 22 | name: commandName, 23 | className: className, 24 | alias: self.makeAliasFromFileName(commandName) 25 | } 26 | }); 27 | 28 | console.log(); 29 | self.logSuccess('Command created: ', userIoCommandPath); 30 | console.log(); 31 | 32 | return process.exit(1); 33 | } 34 | } 35 | 36 | module.exports = new Cmd; 37 | -------------------------------------------------------------------------------- /lib/generators/generator.js: -------------------------------------------------------------------------------- 1 | const Command = require('../command'); 2 | 3 | class Generator extends Command { 4 | constructor() { 5 | super(); 6 | this.alias = 'gen'; 7 | this.name = 'generator'; 8 | this.description = 'io g:{generator, gen} '; 9 | } 10 | run(args, argv) { 11 | var self = this; 12 | var generatorName = args[1]; 13 | var generatorFileName = generatorName + '.js'; 14 | var className = this.classify(generatorName); 15 | var userIoGeneratorPath = this.join(self.ioPath, 'generators', generatorFileName); 16 | 17 | this.writeTemplateWithData({ 18 | src: [self._adminConfigPath, 'generator.js'], 19 | dest: userIoGeneratorPath, 20 | data: { 21 | commandPath: self._adminCommandPath, 22 | name: generatorName, 23 | className: className, 24 | alias: self.makeAliasFromFileName(generatorName) 25 | } 26 | }); 27 | 28 | console.log(); 29 | self.logSuccess('Generator created: ', userIoGeneratorPath); 30 | console.log(); 31 | 32 | return process.exit(1); 33 | } 34 | } 35 | 36 | module.exports = new Generator; 37 | -------------------------------------------------------------------------------- /lib/generators/template.js: -------------------------------------------------------------------------------- 1 | const Command = require('../command'); 2 | 3 | class Template extends Command { 4 | constructor() { 5 | super(); 6 | // A shorter name to call your generator with. 7 | this.alias = 't'; 8 | // This name should match the file name without the extension. 9 | this.name = 'template'; 10 | // A brief description on how to use this. 11 | this.description = 'io g:{template, t} [path/]'; 12 | } 13 | run(args, argv) { 14 | if (args.length < 2) { 15 | throw new Error('Requires two args'); 16 | } 17 | 18 | const fileBaseName = this.getBaseName(args[1]); 19 | const camelName = this.camelize(fileBaseName); 20 | 21 | console.log(); 22 | this.writeTemplatesWithData({ 23 | src: [this.ioTemplatesPath, 'template.html'], 24 | dest: [this.config.dest, fileBaseName + '.html'], 25 | data: { name: camelName } 26 | }, { 27 | src: [this.ioTemplatesPath, 'template.js'], 28 | dest: [this.config.dest, fileBaseName + '.js'], 29 | data: { name: camelName } 30 | }); 31 | console.log(); 32 | } 33 | } 34 | 35 | module.exports = new Template; 36 | -------------------------------------------------------------------------------- /lib/generators/route.js: -------------------------------------------------------------------------------- 1 | const Command = require('../command'); 2 | 3 | class Route extends Command { 4 | constructor() { 5 | super(); 6 | // A shorter name to call your generator with. 7 | this.alias = 'r'; 8 | // This name should match the file name without the extension. 9 | this.name = 'route'; 10 | // A brief description on how to use this. 11 | this.description = 'io g:{route, r} [path/]'; 12 | } 13 | run(args, argv) { 14 | if (args.length < 2) { 15 | throw new Error('Requires two args'); 16 | } 17 | 18 | const fileBaseName = this.getBaseName(args[1]); 19 | const pageName = this.classify(fileBaseName) + 'Page'; 20 | const camelName = this.camelize(fileBaseName); 21 | 22 | const importPath = this.join(this.config.dest, pageName); 23 | const importStatement = `import ${pageName} from '../../ui/pages/${pageName}'\n`; 24 | this.writeImportToFile(this.config.dest, importStatement); 25 | 26 | 27 | // Create the route 28 | this.writeTemplateWithDataToEndOfFile({ 29 | src: 'route.js', 30 | dest: this.config.dest, 31 | data: { name: fileBaseName, pageName, camelName } 32 | }); 33 | 34 | // Create page for this route 35 | this.runGenerator('page', args, argv); 36 | } 37 | } 38 | 39 | module.exports = new Route; 40 | -------------------------------------------------------------------------------- /lib/generators/package.js: -------------------------------------------------------------------------------- 1 | const Command = require('../command'); 2 | 3 | class RaisalPackage extends Command { 4 | constructor() { 5 | super(); 6 | // A shorter name to call your generator with. 7 | this.alias = 'p'; 8 | // This name should match the file name without the extension. 9 | this.name = 'package'; 10 | // A brief description on how to use this. 11 | this.description = 'io g:package , io g:p '; 12 | } 13 | 14 | run(args, argv) { 15 | // Generator code here 16 | if (args.length < 2) { 17 | throw new Error('Specify a package name.'); 18 | return process.exit(1); 19 | } 20 | 21 | var self = this; 22 | var packageName = args[1]; 23 | var fileBaseName = self.getBaseName(args[1]); 24 | var cammelCasePackageName = self.camelize(packageName); 25 | var packagesPath = self.packagesPath; 26 | var libPath = self.join(packagesPath, packageName, 'lib'); 27 | var readme = self.join(packagesPath, packageName, 'README.md'); 28 | 29 | var jsFileName = packageName + '.js'; 30 | var htmlFileName = packageName + '.html'; 31 | 32 | var jsDest = self.join(libPath, 'client', 'templates', jsFileName); 33 | var htmlDest = self.join(libPath, 'client', 'templates', htmlFileName); 34 | 35 | self.mkdirs([libPath, 'client', 'templates'], 36 | [libPath, 'client', 'stylesheets'], 37 | [libPath, 'server']); 38 | 39 | console.log(); 40 | 41 | var packageData = { 42 | packageName: packageName, 43 | jsDest: jsFileName, 44 | htmlDest: htmlFileName, 45 | meteorVersion: self.getMeteorVersion() 46 | }; 47 | 48 | self.writeTemplatesWithData({ 49 | src: [self.ioTemplatesPath, 'package.js'], 50 | dest: [packagesPath, packageName, 'package.js'], 51 | data: packageData, 52 | }, { 53 | src: [self.ioTemplatesPath, 'template.html'], 54 | dest: htmlDest, 55 | data: { name: cammelCasePackageName }, 56 | }, { 57 | src: [self.ioTemplatesPath, 'template.js'], 58 | dest: jsDest, 59 | data: { name: cammelCasePackageName }, 60 | }); 61 | 62 | self.writeFile(readme); 63 | 64 | console.log(); 65 | } 66 | } 67 | 68 | module.exports = new RaisalPackage; 69 | -------------------------------------------------------------------------------- /lib/utilities/logger.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var cli = require('cli-color'); 3 | var Table = require('cli-table'); 4 | 5 | class Logger { 6 | logSuccess(...args) { 7 | console.log(cli.green.apply(this, args)); 8 | } 9 | logWarning(...args) { 10 | console.log(cli.yellow.apply(this, args)); 11 | } 12 | logError(...args) { 13 | console.log(cli.red.apply(this, args)); 14 | } 15 | logComplete(...args) { 16 | console.log(cli.green.apply(this, args)); 17 | } 18 | logNoProjectFound(commands, generator) { 19 | console.log(); 20 | this.logError('No io project found in the directory: ', process.cwd()); 21 | console.log(); 22 | this.logSuccess('To create a new io project, run the command:'); 23 | this.logSuccess('$ io create '); 24 | console.log(); 25 | console.log('Or run: io --help, to see more options'); 26 | console.log(); 27 | } 28 | logHelp(commands, generators, ioPath) { 29 | var commandTable = borderlessTable('Command', 'Description'); 30 | 31 | _.each(commands, function(command) { 32 | let name = command.name || ''; 33 | let description = command.description || ''; 34 | commandTable.push([ 35 | name, description 36 | ]); 37 | }); 38 | 39 | console.log(); 40 | console.log(commandTable.toString()); 41 | console.log(); 42 | 43 | var generatorTable = borderlessTable('Generator', 'Description'); 44 | _.each(generators, function(gen) { 45 | let name = gen.name || ''; 46 | let description = gen.description || ''; 47 | generatorTable.push([ 48 | name, description 49 | ]); 50 | }); 51 | 52 | console.log(generatorTable.toString()); 53 | console.log(); 54 | console.log('Your io is installed at: ', ioPath); 55 | console.log(); 56 | } 57 | } 58 | 59 | function borderlessTable(...head) { 60 | return new Table({ 61 | head: head, 62 | chars: { 'top': '', 'top-mid': '', 'top-left': '', 'top-right': '', 'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '', 'left': '', 'left-mid': '', 'mid': '', 'mid-mid': '', 'right': '', 'right-mid': '', 'middle': ' ' }, 63 | style: { 'padding-left': 0, 'padding-right': 5 }, 64 | }); 65 | } 66 | 67 | module.exports = Logger; 68 | -------------------------------------------------------------------------------- /lib/commands/setup.js: -------------------------------------------------------------------------------- 1 | var Command = require('../command'); 2 | var fs = require('fs-extra'); 3 | var path = require('path'); 4 | var readlineSync = require('readline-sync'); 5 | var store = require('data-store')('io'); 6 | 7 | class Setup extends Command { 8 | constructor() { 9 | super(); 10 | this.name = 'setup'; 11 | this.description = 'io setup'; 12 | } 13 | run(args, argv) { 14 | var self = this; 15 | 16 | console.log(); 17 | console.log('First we need to set up an io directory.'); 18 | console.log(); 19 | console.log('Specify an existing directory where you would like to store your generators, templates, and config.'); 20 | console.log(); 21 | 22 | var ioDest = readlineSync.question('Directory: '); 23 | ioDest = ioDest.trim(); 24 | console.log('ioDest: ', ioDest); 25 | 26 | if (self.isDir(ioDest)) { 27 | try { 28 | let ioInstallPath = path.join(ioDest, 'io'); 29 | 30 | console.log(); 31 | 32 | // Create user's io directory 33 | self.mkdir(ioInstallPath); 34 | 35 | // Save the user's io path to disk 36 | store.set('usersIoPath', ioInstallPath); 37 | 38 | // Set up dirs for user's io 39 | self.mkdirs([ioInstallPath, 'templates'], 40 | [ioInstallPath, 'generators'], 41 | [ioInstallPath, '.io']); 42 | 43 | console.log(); 44 | 45 | // Set up templates and generators 46 | let generatorsPath = path.join(self._adminTemplatesDir, 'generators'); 47 | let generatorsDestPath = path.join(ioInstallPath, 'generators'); 48 | 49 | let templatesPath = path.join(self._adminTemplatesDir, 'templates'); 50 | let templatesDestPath = path.join(ioInstallPath, 'templates'); 51 | 52 | // Set up global config file 53 | self.copy([templatesPath, 'config.json'], 54 | [ioInstallPath, '.io', 'config.json']); 55 | 56 | console.log(); 57 | self.logSuccess('io successfully installed at: ' + ioInstallPath); 58 | console.log(); 59 | 60 | } catch (e) { 61 | console.log(e); 62 | } 63 | 64 | } else { 65 | throw new Error('Not a valid directory. Make sure the directory already exists.'); 66 | } 67 | 68 | } 69 | } 70 | 71 | module.exports = new Setup; 72 | -------------------------------------------------------------------------------- /lib/commands/create.js: -------------------------------------------------------------------------------- 1 | var Command = require('../command'); 2 | var path = require('path'); 3 | var _ = require('underscore'); 4 | 5 | class Create extends Command { 6 | constructor() { 7 | super(); 8 | this.name = 'create'; 9 | this.description = 'io create '; 10 | } 11 | run(args, argv) { 12 | var self = this; 13 | 14 | if (args.length < 2 && !argv.generator) 15 | throw new Error('Need to give your project a name'); 16 | 17 | console.log(); 18 | 19 | var projectName = args[1]; 20 | var projectPath = path.join(process.cwd(), projectName); 21 | var appPath = path.join(projectPath, 'app'); 22 | 23 | this.mkdirs(projectPath, 24 | [projectPath, 'config', 'development']); 25 | 26 | var adminTemplatesPath = path.join(this._adminTemplatesDir, 'templates'); 27 | var ioDirPath = path.join(projectPath, '.io'); 28 | var ioFilePath = path.join(ioDirPath, 'config.json'); 29 | var ioSrcPath = path.join(adminTemplatesPath, 'admin-config.json'); 30 | 31 | self.mkdir(ioDirPath); 32 | self.copy(ioSrcPath, ioFilePath); 33 | self.copy([adminTemplatesPath, 'env.sh'], 34 | [projectPath, 'config', 'development', 'env.sh']); 35 | 36 | self.copy([adminTemplatesPath, 'settings.json'], 37 | [projectPath, 'config', 'development', 'settings.json']); 38 | 39 | this.createMeteorApp(projectPath); 40 | 41 | if (isPackagesToRemove.call(self)) { 42 | let packagesToRemove = self.config.packages.remove; 43 | _.each(packagesToRemove, function(name) { 44 | self.removePackage(name, path.join(projectPath, 'app')); 45 | }); 46 | } 47 | 48 | if (isPackagesToAdd.call(self)) { 49 | let packagesToAdd = self.config.packages.add; 50 | _.each(packagesToAdd, function(name) { 51 | self.addPackage(name, path.join(projectPath, 'app')); 52 | }); 53 | } 54 | 55 | if (isNPMPackagesToAdd.call(self)) { 56 | let packagesToAdd = self.config.npm.install; 57 | _.each(packagesToAdd, function(name) { 58 | self.installNPMPackage(name, path.join(projectPath, 'app')); 59 | }); 60 | } 61 | 62 | 63 | this.removeAll([appPath, 'client', 'main.css'], 64 | [appPath, 'client', 'main.html'], 65 | [appPath, 'client', 'main.js']); 66 | 67 | const importsPath = path.join(appPath, 'imports'); 68 | 69 | this.mkdirs([appPath, 'client'], 70 | [appPath, 'server']); 71 | 72 | this.copy([adminTemplatesPath, 'imports'], [appPath, 'imports']); 73 | this.copy([adminTemplatesPath, 'client'], [appPath, 'client']); 74 | 75 | console.log(); 76 | } 77 | } 78 | 79 | module.exports = new Create; 80 | 81 | function isPackagesToRemove() { 82 | var self = this; 83 | return self.config && 84 | self.config.packages && 85 | self.config.packages.remove && 86 | self.config.packages.remove.length; 87 | }; 88 | 89 | function isPackagesToAdd() { 90 | var self = this; 91 | return self.config && 92 | self.config.packages && 93 | self.config.packages.add && 94 | self.config.packages.add.length; 95 | }; 96 | 97 | function isNPMPackagesToAdd() { 98 | var self = this; 99 | return self.config && 100 | self.config.npm && 101 | self.config.npm.install && 102 | self.config.npm.install.length; 103 | }; 104 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const _ = require('underscore'); 2 | const FileSystem = require('./utilities/file-system'); 3 | const path = require('path'); 4 | const fs = require('fs-extra'); 5 | const exec = require('child_process').execSync; 6 | const spawn = require('child_process').spawn; 7 | const store = require('data-store')('io'); 8 | 9 | class Utils extends FileSystem { 10 | constructor() { 11 | super(); 12 | } 13 | classify(fileName) { 14 | fileName = this.removeFileNameCharacters(fileName); 15 | fileName = fileName.replace(/\w\S*/g, function(txt) { 16 | return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); 17 | }); 18 | return fileName.replace(' ', ''); 19 | } 20 | capitalize(s) { 21 | return s && s[0].toUpperCase() + s.slice(1); 22 | } 23 | camelizeFileName(fileName) { 24 | this.logWarning('camelizeFileName is depreciated. Use camelize() instead'); 25 | return this.camelize(fileName); 26 | } 27 | camelize(fileName) { 28 | return fileName.replace(/-([a-z])/g, function(g) { return g[1].toUpperCase(); }); 29 | } 30 | removeFileNameCharacters(fileName) { 31 | fileName = fileName.replace('-', ' '); 32 | fileName = fileName.replace('_', ' '); 33 | return fileName; 34 | } 35 | convertConstantToVariable(constant) { 36 | const lowerCase = constant.toLowerCase(); 37 | const underscoreToDash = lowerCase.replace('_', '-'); 38 | return this.camelize(underscoreToDash); 39 | } 40 | converFileToClassName(fileName) { 41 | this.logWarning('converFileToClassName is depreciated. Use classify() instead'); 42 | return this.classify(fileName); 43 | } 44 | makeAliasFromFileName(fileName) { 45 | fileName = this.removeFileNameCharacters(fileName); 46 | const matches = fileName.match(/\b(\w)/g); 47 | const alias = matches.join(''); 48 | return alias; 49 | } 50 | isIoUserConfigPath() { 51 | if (!store.has('usersIoPath')) { 52 | return false; 53 | } 54 | const configFile = path.join(store.get('usersIoPath'), '.io', 'config.json'); 55 | return this.isFile(configFile); 56 | } 57 | createMeteorApp(projectPath) { 58 | const command = 'meteor create app'; 59 | this.execMeteorCommandSync(command, projectPath); 60 | } 61 | createPackage(packageName) { 62 | const command = `meteor create ${packageName} --package`; 63 | const cwd = this.packagesPath; 64 | this.execMeteorCommandSync(command, cwd); 65 | } 66 | installNPMPackage(packageName, appPath) { 67 | const command = `meteor npm install --save ${packageName}`; 68 | const cwd = appPath || this.appPath; 69 | this.execMeteorCommandSync(command, cwd); 70 | } 71 | addPackage(packageName, appPath) { 72 | const command = `meteor add ${packageName}`; 73 | const cwd = appPath || this.appPath; 74 | this.execMeteorCommandSync(command, cwd); 75 | } 76 | removePackage(packageName, appPath) { 77 | const command = `meteor remove ${packageName}`; 78 | const cwd = appPath || this.appPath; 79 | this.execMeteorCommandSync(command, cwd); 80 | } 81 | execCommandSync(command, cwd) { 82 | try { 83 | const results = exec(command, { cwd: cwd }); 84 | return results; 85 | } catch (e) { 86 | this.logError(e); 87 | } 88 | } 89 | execMeteorCommandSync(command, cwd) { 90 | const results = this.execCommandSync(command, cwd); 91 | this.logSuccess(results); 92 | } 93 | runMeteor(args = []) { 94 | const envPath = this.envPath; 95 | const settingsPath = this.settingsPath; 96 | 97 | if (this.isFile(settingsPath)) { 98 | args.push('--settings', settingsPath); 99 | } 100 | 101 | if (this.isFile(envPath)) { 102 | require('dotenv').config({ path: envPath }); 103 | } 104 | 105 | this.runMeteorCommand(args); 106 | } 107 | runMeteorCommand(args = []) { 108 | 109 | spawn('meteor', args, { 110 | cwd: this.appPath, 111 | stdio: 'inherit' 112 | }); 113 | } 114 | getMeteorVersion() { 115 | const command = 'meteor --version'; 116 | const results = this.execCommandSync(command, this.appPath); 117 | const resultStr = results.toString('utf-8'); 118 | return resultStr.replace(/[^\d.-]/g, ''); 119 | } 120 | }; 121 | 122 | module.exports = Utils; 123 | -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | const _ = require('underscore'); 2 | const Utils = require('./utils'); 3 | const path = require('path'); 4 | const fs = require('fs-extra'); 5 | const requireDir = require('require-dir'); 6 | const FileSystem = require('./utilities/file-system'); 7 | const meteorCommands = require('./utilities/meteor-commands'); 8 | const store = require('data-store')('io'); 9 | 10 | class Command extends Utils { 11 | constructor(opts = {}) { 12 | super(); 13 | this.name = opts.name; 14 | this.commands = opts.commands || []; 15 | this.generators = opts.generators || []; 16 | this.constantPaths = ['$PROJECT_PATH', '$APP_PATH', '$PACKAGES_PATH', 17 | 'PROJECT_PATH', 'APP_PATH', 'PACKAGES_PATH']; 18 | } 19 | get commandKeys() { 20 | return _.keys(this.commands); 21 | } 22 | get generatorKeys() { 23 | return _.keys(this.generators); 24 | } 25 | run(args, argv) { 26 | 27 | if (! this.isIoUserConfigPath()) { 28 | return this.commands.setup.run(args, argv); 29 | } 30 | 31 | this.loadUserCommands(); 32 | this.loadUserGenerators(); 33 | 34 | if (argv.help) { 35 | this.logHelp(this.commands, this.generators, this.ioPath); 36 | process.exit(1); 37 | } 38 | 39 | // If the command maps to a meteor command. 40 | if (this.isMeteorCommand(args) && this.findProjectDir()) { 41 | return this.commands.run.run(args, argv); 42 | } 43 | 44 | const whole = args[0]; 45 | const command = this.findCommand(whole); 46 | let generator; 47 | 48 | if (command === 'generate') { 49 | generator = this.findGenerator(args[0]); 50 | } 51 | 52 | if (command === 'create' || this.isProjectDir()) { 53 | this.commands[command].bindCommandConfig(args); 54 | this.commands[command].run(args, argv); 55 | } else if (command === 'generate' && 56 | generator && 57 | (generator === 'generator' || 58 | generator === 'cmd')) 59 | { 60 | this.commands[command].run(args, argv); 61 | } else if (!this.isProjectDir()) { 62 | this.logError('No io project found. Are you in an io project directory?'); 63 | } else { 64 | this.logError('Command not found.'); 65 | } 66 | } 67 | loadUserCommands() { 68 | const usersCommandsPath = path.join(this.ioPath, 'commands'); 69 | if (this.isDir(usersCommandsPath)) { 70 | const usersCommands = requireDir(usersCommandsPath); 71 | const adminCommands = requireDir('./commands'); 72 | this.commands = _.extend(adminCommands, usersCommands); 73 | } 74 | } 75 | loadUserGenerators() { 76 | const usersGeneratorsPath = path.join(this.ioPath, 'generators'); 77 | if (this.isDir(usersGeneratorsPath)) { 78 | const usersGenerators = requireDir(usersGeneratorsPath); 79 | const adminGenerators = requireDir(this._adminGeneratorsDir); 80 | this.generators = _.extend(adminGenerators, usersGenerators); 81 | } 82 | } 83 | findCommand(command) { 84 | const splitCommand = command.split(':'); 85 | let result; 86 | 87 | if (this.commands.length === 0) { 88 | this.loadUserCommands(); 89 | } 90 | 91 | var commands = this.commands; 92 | 93 | // TODO: Need to set up aliases 94 | if (splitCommand.length > 0) { 95 | _.each(commands, (cmd) => { 96 | let command = splitCommand[0]; 97 | if ((_.has(cmd, 'name') && cmd.name === command) || 98 | (_.has(cmd, 'alias') && cmd.alias === command)) 99 | { 100 | result = cmd.name; 101 | } 102 | }); 103 | } 104 | 105 | if (result) return result; 106 | 107 | this.logHelp(this.commands, this.generators); 108 | process.exit(1); 109 | } 110 | findGenerator(command) { 111 | this.loadUserGenerators(); 112 | const splitCommand = command.split(':'); 113 | const generators = this.generators; 114 | let result; 115 | 116 | // XXX: Both files and generator's name have 117 | // to be unique. Need to find a way to return 118 | // the generator by it's property "name", and 119 | // not the file name. 120 | if (generators && splitCommand.length > 1) { 121 | _.each(generators, (gen) => { 122 | let splitGen = splitCommand[1]; 123 | if ((_.has(gen, 'name') && gen.name === splitGen) || 124 | (_.has(gen, 'alias') && gen.alias === splitGen)) 125 | { 126 | result = gen.name; 127 | } 128 | }); 129 | } 130 | 131 | if (result) return result; 132 | 133 | throw new Error('Can\'t find generator'); 134 | process.exit(1); 135 | } 136 | runGenerator(generator, args, argv) { 137 | this.loadUserGenerators(); 138 | 139 | // bindGeneratorConfig uses findDirectory, which will 140 | // throw an exception if not in an io dir. When generating 141 | // a new command or generator, we shouldn't need to be 142 | // in an io project. For now, when generating a command 143 | // or generator, don't bind the config to the generator. 144 | if (generator !== 'cmd' && generator !== 'generator') { 145 | this.generators[generator].bindGeneratorConfig(args); 146 | } 147 | this.generators[generator].run(args, argv); 148 | } 149 | runCommand(command, args, argv) { 150 | this.loadUserCommands(); 151 | 152 | this.commands[command].run(args, argv); 153 | } 154 | isMeteorCommand(args) { 155 | return args.length === 0 || args.length > 0 && 156 | _.contains(meteorCommands, args[0]); 157 | } 158 | bindCommandConfig(args) { 159 | const commandName = this.name; 160 | const replaceDirs = this.constantPaths; 161 | let configFile; 162 | 163 | if (commandName === 'create') { 164 | configFile = this.globalConfig; 165 | } else { 166 | configFile = this.configFile; 167 | } 168 | 169 | if (configFile && configFile.commands && configFile.commands[commandName]) { 170 | this.config = configFile.commands[commandName] || {}; 171 | if (_.has(this.config, 'dest')) { 172 | _.each(replaceDirs, (dir) => { 173 | const str = dir.replace('$', ''); 174 | const camelCase = this.convertConstantToVariable(str); 175 | this.config.dest = this.config.dest.replace(dir, this[camelCase]); 176 | }); 177 | 178 | // XXX: Need a better way to determine if the second arg is in fact a path 179 | if (args.length > 1) { 180 | const destPath = args[1]; 181 | this.config.dest = this.config.dest.replace('$DEST_PATH', destPath); 182 | } 183 | } 184 | } 185 | } 186 | bindGeneratorConfig(args) { 187 | const generatorName = this.name; 188 | const configFile = this.configFile; 189 | const replaceDirs = this.constantPaths; 190 | 191 | if (configFile && configFile.generators && configFile.generators[generatorName]) { 192 | this.config = configFile.generators[generatorName] || {}; 193 | if (_.has(this.config, 'dest')) { 194 | _.each(replaceDirs, (dir) => { 195 | const str = dir.replace('$', ''); 196 | const camelCase = this.convertConstantToVariable(str); 197 | this.config.dest = this.config.dest.replace(dir, this[camelCase]); 198 | }); 199 | 200 | // XXX: Need a better way to determine if the second arg is in fact a path 201 | if (args.length > 1) { 202 | const destPath = args[1]; 203 | this.config.dest = this.config.dest.replace('$DEST_PATH', destPath); 204 | } 205 | } 206 | } 207 | } 208 | }; 209 | 210 | module.exports = Command; 211 | -------------------------------------------------------------------------------- /lib/utilities/file-system.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const _ = require('lodash'); 4 | const Handlebars = require('handlebars'); 5 | const appDir = path.dirname(require.main.filename); 6 | const Logger = require('./logger'); 7 | const readlineSync = require('readline-sync'); 8 | const store = require('data-store')('io'); 9 | 10 | class FileSystem extends Logger { 11 | // For the user. 12 | get ioTemplatesPath() { 13 | var templatesPath = path.join(this.ioPath, 'templates'); 14 | return templatesPath; 15 | } 16 | // Useful generator and command variables 17 | get projectPath() { 18 | return this.findProjectDir(); 19 | } 20 | get appPath() { 21 | return path.join(this.projectPath, 'app'); 22 | } 23 | get packagesPath() { 24 | return path.join(this.projectPath, 'app', 'packages'); 25 | } 26 | get settingsPath() { 27 | return path.join(this.projectPath, 'config', 'development', 'settings.json'); 28 | } 29 | get envPath() { 30 | return path.join(this.projectPath, 'config', 'development', 'env.sh'); 31 | } 32 | get globalConfig() { 33 | const globalConfigPath = this.join(this.ioPath, '.io', 'config.json'); 34 | const globalJson = fs.readJsonSync(globalConfigPath); 35 | return globalJson; 36 | } 37 | get localConfig() { 38 | const localConfigPath = this.join(this.projectPath, '.io', 'config.json'); 39 | const localJson = fs.readJsonSync(localConfigPath); 40 | return localJson; 41 | } 42 | get configFile() { 43 | try { 44 | const globalConfig = this.globalConfig; 45 | const localConfig = this.localConfig; 46 | const json = _.merge(globalConfig, localConfig); 47 | return json; 48 | } catch (e) { 49 | this.logError(e); 50 | return false; 51 | } 52 | } 53 | 54 | // Internal Variables 55 | get _adminLibPath() { 56 | return path.join(this._adminRootDir, 'lib'); 57 | } 58 | get _adminCommandPath() { 59 | return path.join(this._adminLibPath, 'command.js'); 60 | } 61 | get _adminRootDir() { 62 | return appDir.replace(path.sep + 'bin', ''); 63 | } 64 | get _adminConfigPath() { 65 | return path.join(this._adminRootDir, 'config'); 66 | } 67 | get _adminTemplatesDir() { 68 | return path.join(this._adminLibPath, 'templates'); 69 | } 70 | get _adminGeneratorsDir() { 71 | return path.join(this._adminLibPath, 'generators'); 72 | } 73 | 74 | get ioPath() { 75 | return store.get('usersIoPath'); 76 | } 77 | 78 | // Files, directories, and templates 79 | isDir(dir) { 80 | try { 81 | const stat = fs.statSync(dir); 82 | return stat.isDirectory(); 83 | } catch (e) { 84 | return false; 85 | } 86 | } 87 | isFile(file) { 88 | try { 89 | return fs.statSync(file).isFile(); 90 | } catch (e) { 91 | return false; 92 | } 93 | } 94 | mkdir(dir) { 95 | if (_.isArray(dir)) { 96 | dir = this.joinArray(dir); 97 | } 98 | fs.ensureDirSync(dir); 99 | this.logSuccess('Created dir: ', dir); 100 | } 101 | mkdirs(...dirs) { 102 | _.each(dirs, (dir) => { 103 | this.mkdir(dir); 104 | }); 105 | } 106 | outputJson(file, data) { 107 | fs.outputJsonSync(file, data); 108 | } 109 | copy(src, dest) { 110 | if (_.isArray(src)) { 111 | src = this.joinArray(src); 112 | } 113 | if (_.isArray(dest)) { 114 | dest = this.joinArray(dest); 115 | } 116 | this.confirmFileOverwriteIfExists(dest); 117 | fs.copySync(src, dest); 118 | } 119 | copyAll(...dirGroups) { 120 | _.each(dirGroups, (dirGroup) => { 121 | if (dirGroup.hasOwnProperty('src') && dirGroup.hasOwnProperty('dest')) { 122 | return this.copy(dirGroup.src, dirGroup.dest); 123 | } 124 | return this.logError('copyAll needs a src and a dest'); 125 | }); 126 | } 127 | remove(dir) { 128 | if (_.isArray(dir)) { 129 | dir = this.joinArray(dir); 130 | } 131 | fs.removeSync(dir); 132 | } 133 | removeAll(...dirs) { 134 | _.each(dirs, (dir) => { 135 | this.remove(dir); 136 | }); 137 | } 138 | join(...paths) { 139 | return path.join.apply(null, paths); 140 | } 141 | joinArray(arr) { 142 | return path.join.apply(null, arr); 143 | } 144 | getDirName(fullPath) { 145 | return path.dirname(fullPath); 146 | } 147 | getBaseName(fullPath) { 148 | return path.basename(fullPath); 149 | } 150 | 151 | // XXX: Smarter defaults and checking needed. 152 | writeTemplateWithData(opts = {}, isAddToEndOfExistingFile = false) { 153 | let src = opts.src || ''; 154 | let dest = opts.dest || ''; 155 | 156 | if (_.isArray(src)) { 157 | src = this.joinArray(src); 158 | } 159 | 160 | if (_.isArray(dest)) { 161 | dest = this.joinArray(dest); 162 | } 163 | 164 | var source = this.getTemplateSource(src); 165 | var compiledTemplate = this.compile(source); 166 | var data = opts.data || {}; 167 | var result = compiledTemplate(data); 168 | 169 | if (!isAddToEndOfExistingFile) { 170 | this.confirmFileOverwriteIfExists(dest); 171 | fs.ensureFileSync(dest); 172 | fs.writeFileSync(dest, result); 173 | } else { 174 | fs.ensureFileSync(dest); 175 | fs.appendFileSync(dest, result); 176 | } 177 | 178 | this.logSuccess('Created template: ', dest); 179 | } 180 | writeTemplatesWithData(...templates) { 181 | var self = this; 182 | _.each(templates, function(template) { 183 | self.writeTemplateWithData(template); 184 | }); 185 | } 186 | writeTemplateWithDataToEndOfFile(opts) { 187 | this.writeTemplateWithData(opts, true); 188 | } 189 | writeFile(file) { 190 | if (_.isArray(file)) { 191 | file = this.joinArray(file); 192 | } 193 | this.confirmFileOverwriteIfExists(file); 194 | fs.ensureFileSync(file); 195 | this.logSuccess('Created file: ', file); 196 | } 197 | writeImportToFile(file, importStatement) { 198 | if (_.isArray(file)) { 199 | file = this.joinArray(file); 200 | } 201 | const fileContents = fs.readFileSync(file, 'utf8'); 202 | 203 | let strt = fileContents.lastIndexOf('\nimport '); 204 | strt = fileContents.indexOf('\n', strt + 1); 205 | const result = fileContents.splice(strt + 1, 0, importStatement) 206 | 207 | fs.writeFileSync(file, result, 'utf8'); 208 | } 209 | getSource(filePath) { 210 | var source = fs.readFileSync(filePath, 'utf8'); 211 | return source; 212 | } 213 | getTemplateSource(fileName) { 214 | let srcPath = this.join(this.ioTemplatesPath, fileName); 215 | 216 | if (this.isFile(srcPath)) { 217 | return this.getSource(srcPath); 218 | } else { 219 | 220 | srcPath = this.join(this._adminTemplatesDir, 'templates', fileName); 221 | if (this.isFile(srcPath)) { 222 | return this.getSource(srcPath); 223 | } else { 224 | this.logError(`Template does not exist: ${srcPath}`); 225 | } 226 | } 227 | } 228 | compile(source) { 229 | return Handlebars.compile(source); 230 | } 231 | confirmFileOverwriteIfExists(file) { 232 | if (this.isFile(file)) { 233 | this.logError(`File already exists: ${file}`); 234 | if (!readlineSync.keyInYNStrict('Do you want to overwrite it?')) { 235 | process.exit(); 236 | } 237 | } 238 | } 239 | 240 | // For now, we'll just check three levels. 241 | findProjectDir() { 242 | try { 243 | const base = process.cwd(); 244 | const firstLevel = path.join(base, '.io'); 245 | const secondBase = path.dirname(base); 246 | const secondLevel = path.join(secondBase, '.io'); 247 | const thirdBase = path.dirname(secondBase); 248 | const thirdLevel = path.join(thirdBase, '.io'); 249 | 250 | if (this.isDir(firstLevel)) { 251 | return base; 252 | } else if (this.isDir(secondLevel)) { 253 | return secondBase; 254 | } else if (this.isDir(thirdLevel)) { 255 | return thirdBase; 256 | } else { 257 | this.logNoProjectFound(); 258 | process.exit(1); 259 | } 260 | } catch (e) { 261 | // XXX: Need to inform the user that 262 | // they are not in a project, and possibly 263 | // log a usage or helper error. 264 | this.logNoProjectFound(); 265 | process.exit(1); 266 | } 267 | 268 | } 269 | 270 | // For now, we'll just check three levels. 271 | isProjectDir() { 272 | try { 273 | const base = process.cwd(); 274 | const firstLevel = path.join(base, '.io'); 275 | const secondBase = path.dirname(base); 276 | const secondLevel = path.join(secondBase, '.io'); 277 | const thirdBase = path.dirname(secondBase); 278 | const thirdLevel = path.join(thirdBase, '.io'); 279 | 280 | if (this.isDir(firstLevel) || 281 | this.isDir(secondLevel) || 282 | this.isDir(thirdLevel)) 283 | { 284 | return true; 285 | } 286 | } catch (e) { 287 | return false; 288 | } 289 | 290 | } 291 | } 292 | 293 | String.prototype.splice = function(idx, rem, str) { 294 | return this.slice(0, idx) + str + this.slice(idx + Math.abs(rem)); 295 | }; 296 | 297 | module.exports = FileSystem; 298 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # io (for Meteor) 2 | io is a pluggable and extendable, command-line scaffolding tool written entirely in es6. 3 | 4 | It gives you the ability to create custom commands, generators, and templates to facilitate your Meteor workflow. 5 | 6 | Windows has not yet been thoroughly tested. 7 | ## Table of Contents 8 | * [Getting Started](#getting-started) 9 | * [Config Files](#config-files) 10 | * [Generators](#generators) 11 | * [Commands](#commands) 12 | * [Templating](#templating) 13 | * [API](#api) 14 | 15 | ## Getting Started 16 | io leverage es6 and required node v4.2.1 or later. 17 | 18 | To install the meteor-io npm package: 19 | ```shell 20 | npm install -g meteor-io 21 | ``` 22 | And then run io from anywhere: 23 | ```shell 24 | io 25 | ``` 26 | io will ask you to specify a directory to set up a couple sample generators, templates, and a config file. 27 | 28 | After you specify a directory, io will create a director that looks similar to this: 29 | ``` 30 | io/ 31 | .io/ 32 | config.json 33 | generators/ 34 | route.js 35 | template.js 36 | templates/ 37 | package.js 38 | route.js 39 | template.html 40 | template.js 41 | ``` 42 | To create a new io project: 43 | ```shell 44 | io create project-name 45 | ``` 46 | This will set up a boiler plate io project for you. 47 | 48 | If you're not happy with the structure of the project that io creates, you can always override the create command by generating one of your own: 49 | ```sh 50 | io g:cmd create 51 | ``` 52 | This will create a command for at: `path/to/your/io/commands/create.js` 53 | 54 | (If you forget where io is installed, run `io --help`) 55 | 56 | Once your project is created, you can then cd into your project and run the io command to start the server: 57 | ```sh 58 | io 59 | ``` 60 | 61 | ## Config Files 62 | The first time you run the io command, it will create a global config for you: `path/to/your/io/.io/config.json` 63 | You can modify this file to control which packages will be installed and removed when you run `io create project-name`. 64 | By default, it will install: `kadira:flow-router` and `kadira:blaze-layout`, and remove: `autopublish` and `insecure`. 65 | 66 | io will also create a local config file for you in your project when you run `io create project-name`. Local config settings will trump globals. 67 | 68 | ## Generators 69 | io automatically creates a couple of sample generators for you. One to generate routes—io uses [FlowRouter](https://github.com/kadirahq/flow-router) by default, but you can set it up to use any router, and one to generate templates. 70 | You'll be able to see and modify these generators in your local io/generators folder, and run them with the following commands: 71 | ```sh 72 | io g:route route-name 73 | io g:template template-name 74 | ``` 75 | 76 | To create a new pluggable generator: 77 | ```sh 78 | io g:generator generator-name 79 | ``` 80 | 81 | ## Commands 82 | Command and generators both extend the Command class, and both have access to the same functionality. They're just two separate means to organize actions. 83 | And command are called without the `g:` prefix. 84 | 85 | To run a command: 86 | ```sh 87 | io command-name args 88 | ``` 89 | 90 | To generate a new pluggable command: 91 | ```sh 92 | io g:cmd command-name 93 | ``` 94 | 95 | ## Templating 96 | io uses handlebars to generate templates. Templates can be used to dynamically create files and resources for you app. Take for example the template that's used to generate helpers, events, and life-cycle callbacks when you run `io g:template template-name`. 97 | The template that's used to generate those resources can be found at `path/to/your/io/templates/template.js`, and looks like this: 98 | ```js 99 | Template.{{name}}.events({ 100 | }); 101 | 102 | Template.{{name}}.helpers({ 103 | }); 104 | 105 | Template.{{name}}.onCreated(function () { 106 | }); 107 | 108 | Template.{{name}}.onRendered(function () { 109 | }); 110 | 111 | Template.{{name}}.onDestroyed(function () { 112 | }); 113 | ``` 114 | And we can use this template and interpolate the data by calling [writeTemplateWithData](https://github.com/scottmcpherson/meteor-io#thiswritetemplatewithdata-src-path-dest-path-data-data-) or [writeTemplatesWithData](https://github.com/scottmcpherson/meteor-io#thiswritetemplateswithdataargs-args). 115 | For example, given the command `io g:template template-name`: 116 | ``` 117 | run(args, argv) { 118 | 119 | var fileBaseName = self.getBaseName(args[1]); 120 | // template-name 121 | 122 | var camelCaseName = self.camelize(fileBaseName); 123 | // templateName 124 | 125 | this.writeTemplateWithData({ 126 | src: [this.ioTemplatesPath, 'template.js'], 127 | dest: [this.config.dest, fileBaseName + '.js'], 128 | data: { name: camelCaseName } 129 | }); 130 | 131 | ``` 132 | 133 | 134 | ## API 135 | io provides you with a high-level API for creating custom commands and generators. All of which can be accessed and called from within the `run()` methods of your generators and commands. 136 | 137 | #### *this*.ioTemplatesPath; 138 | Gets the path to your io templates. 139 | 140 | #### *this*.projectPath; 141 | Gets the path to your io project. 142 | 143 | #### *this*.appPath; 144 | Gets the path to your meteor app inside the io project. 145 | 146 | #### *this*.settingsPath; 147 | Gets the path to your app's local setting.json file. 148 | 149 | #### *this*.envPath; 150 | Gets the path to your app's local env.sh file. 151 | 152 | #### *this*.globalConfig; 153 | Gets the global config in a JSON format. 154 | 155 | #### *this*.localConfig; 156 | Gets the project's local config in a JSON format. 157 | 158 | #### *this*.configFile; 159 | Gets the global config in a JSON format. 160 | 161 | #### *this*.configFile; 162 | Gets both the local and global config, merges them, and returns the JSON. 163 | 164 | #### *this*.mkdir([path]); 165 | Creates a directory with a given path String, or an Array of path parts. 166 | 167 | Examples: 168 | ```js 169 | run(args, argv) { 170 | this.mkdir([this.appPath, 'client', templates]); 171 | // Creates a directory at: path/to/project/app/client/templates 172 | ``` 173 | ```js 174 | run(args, argv) { 175 | this.mkdir('path/to/project/app/client/templates'); 176 | // creates a directory at: path/to/project/app/client/templates 177 | ``` 178 | 179 | #### *this*.mkdirs([path], [path]); 180 | Creates x number of directories with given path Strings, or Arrays of path parts. 181 | 182 | Example: 183 | ```js 184 | run(args, argv) { 185 | var templatesPath = this.join(this.appPath, 'client', templates); 186 | this.mkdirs([templatesPath, 'public'], 187 | [templatesPath, 'admin']); 188 | // Creates directories at: 189 | // path/to/project/app/client/templates/public 190 | // path/to/project/app/client/templates/admin 191 | ``` 192 | 193 | #### *this*.copy([srcPath], [destPath]); 194 | Copies a file from the given source path to the destination path. 195 | 196 | Both paths can be either a String, or an array of path parts. 197 | 198 | Examples: 199 | ```js 200 | run(args, argv) { 201 | var src = this.join(this.ioTemplatesPath, 'sample.js'); 202 | var dest = this.join(this.appPath, 'client', 'templates', 'sample.js'); 203 | this.copy(src, dest); 204 | // Copies the file from: path/to/your/io/templates/sample.js 205 | // to: path/to/your/project/app/client/templates/sample.js 206 | ``` 207 | 208 | #### *this*.copyAll({ src: src, dest: dest }, { src: src, dest: dest }); 209 | Copies x number of files from given source paths to destination paths. 210 | 211 | Example: 212 | ```js 213 | run(args, argv) { 214 | var appTemplatesPath = this.join(this.appPath, 'client', 'templates'); 215 | this.copy({ 216 | src: [this.ioTemplatesPath, 'sample-one.js'], 217 | dest: [appTemplatesPath, 'sample-one.js'] 218 | }, { 219 | src: [this.ioTemplatesPath, 'sample-two.js'], 220 | dest: [appTemplatesPath, 'sample-two.js'] 221 | }); 222 | // from: path/to/your/io/templates/sample-one.js 223 | // to: path/to/your/project/app/client/templates/sample-one.js 224 | // from: path/to/your/io/templates/sample-two.js 225 | // to: path/to/your/project/app/client/templates/sample-two.js 226 | ``` 227 | 228 | #### *this*.remove([srcPath]); 229 | This will remove the file at the given source path. 230 | 231 | #### *this*.removeAll([srcPath], [srcPath]); 232 | This will remove x number of files at the given source paths. 233 | 234 | #### *this*.writeTemplateWithData({ src: [path], dest: [path], data: data }); 235 | Grabs a template, interpolates the data, and writes the template to the specified dest path. 236 | 237 | Example: 238 | ```js 239 | run(args, argv) { 240 | var appTemplatesPath = this.join(this.appPath, 'client', 'templates'); 241 | this.copy({ 242 | src: [this.ioTemplatesPath, 'sample.js'], 243 | dest: [appTemplatesPath, 'sample.js'], 244 | data: { name: 'demo' } 245 | }); 246 | // This will get the template from `path/to/your/io/templates/sample.js`, 247 | // interpolate the data, and write the file to `path/to/your/project/app/client/templates/sample.js` 248 | ``` 249 | 250 | #### *this*.writeTemplatesWithData({args}, {args}); 251 | Same as writeTemplateWithData, but allows you to write multiple templates with data. 252 | 253 | Example: 254 | ```js 255 | run(args, argv) { 256 | var appTemplatesPath = this.join(this.appPath, 'client', 'templates'); 257 | this.copy({ 258 | src: [this.ioTemplatesPath, 'sample.js'], 259 | dest: [appTemplatesPath, 'sample.js'], 260 | data: { name: 'demo' } 261 | }, { 262 | src: [this.ioTemplatesPath, 'sample-two.js'], 263 | dest: [appTemplatesPath, 'sample-two.js'], 264 | data: { name: 'demo' } 265 | }); 266 | // This will get the template from `path/to/your/io/templates/sample.js`, 267 | // interpolate the data, and write the file to `path/to/your/project/app/client/templates/sample.js` 268 | // And the template from `path/to/your/io/templates/sample-two.js`, 269 | // interpolate the data, and write the file to `path/to/your/project/app/client/templates/sample-two.js` 270 | ``` 271 | 272 | #### *this*.writeFile(filePath); 273 | Write a blank file to the specified filePath. filePath must be a **String**. 274 | 275 | Example: 276 | ```js 277 | run(args, argv) { 278 | this.writeFile('path/to/your/project/app/client/templates/sample.js'); 279 | // Creates a blank file at path/to/your/project/app/client/templates/sample.js 280 | ``` 281 | --------------------------------------------------------------------------------