UNPKG

muffin.io

Version:

A full stack development tool for creating modern webapps

387 lines (321 loc) 13.4 kB
# The `muffin` command line tool. fs = require 'fs-extra' sysPath = require 'path' _ = require 'underscore' async = require 'async' {spawn} = require 'child_process' logging = require './utils/logging' optparse = require './utils/optparse' project = require './project' watcher = require './watcher' server = require './server' pkgmgr = require './pkgmgr' optimizer = require './optimizer' # Underscore template settings _.templateSettings = evaluate : /<\?([\s\S]+?)\?>/g, interpolate : /<\?=([\s\S]+?)\?>/g, escape : /<\?-([\s\S]+?)\?>/g # The help banner BANNER = ''' Usage: Create a new project: * muffin new <project-name> (the frontend stack only) * muffin new <project-name> -s nodejs (frontend and Node.js server stack) * muffin new <project-name> -s gae (frontend and Google App Engine server stack) Code generators: * muffin generate model user (use --app option to generate inside a specific app) * muffin generate view UserListView * muffin generate scaffold user name:string email:string age:number --app auth Remove generated code: * muffin destroy model user * muffin destroy view UserListView * muffin destroy scaffold user --app auth Package management: * muffin install <package-name> (install a Muffin package and save in config.json) * muffin install (install all packages listed in config.json) Watch mode: * muffin watch (watch the client files and recompile as needed) * muffin watch -s (watch the client files and start a web server/app server) * muffin watch -s -p 4001 (use --port or -p to specify the server port) Build: * muffin build (env is set to 'development', files are compiled but not minified) * muffin minify (env is set to 'production', files are minified and concatenated) * muffin server (run the server without watching client files, useful for testing the production build, use --port or -p to specify the server port) * muffin clean (remove the build directory) Deploy: * muffin deploy [heroku|jitsu|gae|gh-pages] -h, --help display this help message -v, --version display the version number ''' # The list of all the valid option flags that `muffin` supports. SWITCHES = [ ['-h', '--help', 'display this help message'] ['-v', '--version', 'display the version number'] ['-s', '--server', 'choose the server stack or start the server'] ['-p', '--port', 'specify the server port'] ['-a', '--app', 'set the app (default to main)'] ] # Top-level objects shared by all the functions. tasks = {} opts = {} # Define a task with a short name, a description, and the function to run. task = (name, description, action) -> tasks[name] = {name, description, action} # Invoke a task invoke = (name) -> tasks[name].action opts # Run `muffin` exports.run = -> # Remove the first two arguments because they are generic. # `process.argv = ['node', '/usr/local/lib/node_modules/muffin.io/bin/muffin', ...]` opts = optparse.parse(process.argv[2..], SWITCHES) return version() if opts.version return usage() if opts.help or opts.arguments.length is 0 # The task name may consist of one or two words. for len in [1..2] name = opts.arguments[...len].join(' ') if tasks[name] then return invoke name # The task name is not recognized. logging.fatal "Muffin can't run the task '#{opts.arguments.join(' ')}'." # Task - create a new project task 'new', 'create a new project', -> projectName = opts.arguments[1] logging.fatal "You must provide a name for the project." unless projectName projectDir = sysPath.join(process.cwd(), projectName) logging.fatal "The application '#{projectName}' already exists." if fs.existsSync(projectDir) # Create the project directory createProjectDir = (done) -> fs.mkdir projectDir, done # Copy client boilerplate copyClientSkeleton = (done) -> from = sysPath.join(__dirname, '../skeletons/client') to = sysPath.join(projectDir, 'client') fs.copy from, to, done # Copy Node.js/MongoDB boilerplate copyNodeJSSkeleton = (done) -> from = sysPath.join(__dirname, '../skeletons/nodejs') to = sysPath.join(projectDir, 'server') fs.copy from, to, done # Copy Google App Engine boilerplate copyGAESkeleton = (done) -> from = sysPath.join(__dirname, '../skeletons/gae') to = sysPath.join(projectDir, 'server') fs.copy from, to, done # Write `config.json` in the project directory writeJSONConfig = (done) -> from = sysPath.join(__dirname, '../skeletons/config.json') json = JSON.parse(fs.readFileSync(from)) switch opts.server when 'nodejs' json.serverDir = 'server' json.serverType = 'nodejs' json.buildDir = 'server/public' json.plugins.push 'muffin-generator-nodejs' when 'gae' json.serverDir = 'server' json.serverType = 'gae' json.buildDir = 'server/public' json.plugins.push 'muffin-generator-gae' to = sysPath.join(projectDir, 'config.json') fs.writeFileSync(to, JSON.stringify(json, null, 2)) done(null) # Write .gitignore files writeGitIgnore = (done) -> switch opts.server when 'none' from = sysPath.join(__dirname, '../skeletons/_gitignore') to = sysPath.join(projectDir, '.gitignore') fs.copy from, to, done when 'nodejs', 'gae' from = sysPath.join(projectDir, 'server/_gitignore') to = sysPath.join(projectDir, 'server/.gitignore') fs.rename from, to, done # Print the completion message printMessage = (done) -> logging.info "The application '#{projectName}' has been created." logging.info "You need to run `muffin install` inside the project directory to install dependencies." # Use `async` to run these subtasks in series. opts.server ?= 'none' switch opts.server when 'none' async.series [createProjectDir, copyClientSkeleton, writeJSONConfig, writeGitIgnore, printMessage] when 'nodejs' async.series [createProjectDir, copyClientSkeleton, copyNodeJSSkeleton, writeJSONConfig, writeGitIgnore, printMessage] when 'gae' async.series [createProjectDir, copyClientSkeleton, copyGAESkeleton, writeJSONConfig, writeGitIgnore, printMessage] # Task - create a new model task 'generate model', 'create a new model', -> model = opts.arguments[2] logging.fatal "You must provide a name for the model." unless model app = opts.app ? 'main' # Invoke each generator to do its job. project.setEnv 'development' for generator in project.plugins.generators generator.generateModel?(model, app, opts.arguments[3..]) # Task - remove a generated model task 'destroy model', 'remove a generated model', -> model = opts.arguments[2] logging.fatal "You must provide a name for the model." unless model app = opts.app ? 'main' # Invoke each generator to do its job. project.setEnv 'development' for generator in project.plugins.generators generator.destroyModel?(model, app) # Task - create a new view task 'generate view', 'create a new view', -> view = opts.arguments[2] logging.fatal "You must provide a name for the view." unless view app = opts.app ? 'main' # Invoke each generator to do its job. project.setEnv 'development' for generator in project.plugins.generators generator.generateView?(view, app) # Task - remove a generated view task 'destroy view', 'remove a generated view', -> view = opts.arguments[2] logging.fatal "You must provide a name for the view." unless view app = opts.app ? 'main' # Invoke each generator to do its job. project.setEnv 'development' for generator in project.plugins.generators generator.destroyView?(view, app) # Task - create scaffolding for a resource task 'generate scaffold', 'create scaffolding for a resource', -> model = opts.arguments[2] logging.fatal "You must provide a name for the model." unless model app = opts.app ? 'main' # Invoke each generator to do its job. project.setEnv 'development' for generator in project.plugins.generators generator.generateScaffold?(model, app, opts.arguments[3..]) # Task - remove generated scaffolding for a resource task 'destroy scaffold', 'remove generated scaffolding for a resource', -> model = opts.arguments[2] logging.fatal "You must provide a name for the model." unless model app = opts.app ? 'main' # Invoke each generator to do its job. project.setEnv 'development' for generator in project.plugins.generators generator.destroyScaffold?(model, app) # Task - install packages task 'install', 'install packages', -> project.setEnv 'development' pkgs = opts.arguments[1..] if pkgs.length > 0 # Install the packages for pkg in pkgs [repo, version] = pkg.split('@') pkgmgr.install repo, version # Save to `client/config.json` path = sysPath.join(project.clientDir, 'config.json') config = JSON.parse(fs.readFileSync(path)) config.dependencies ?= {} for pkg in pkgs [repo, version] = pkg.split('@') config.dependencies[repo] = version ? '*' fs.writeFileSync(path, JSON.stringify(config, null, 2)) else # Install all dependencies listed in `client/config.json` for repo, version of project.clientConfig.dependencies pkgmgr.install repo, version # Call `npm install` in the serverDir if using the Node.js stack if project.config.serverType is 'nodejs' spawn 'npm', ['install'], {cwd: project.serverDir, stdio: 'inherit'} # Common subtask: build build = (done) -> fs.removeSync(project.buildDir) project.loadHtmlHelpers() watcher.compileDir(project.clientDir, done) # Common subtask: startServer startServer = (done) -> if project.config.serverType in ['nodejs', 'gae'] and fs.existsSync(project.serverDir) server.startAppServer {port: opts.port} else server.startDummyWebServer {port: opts.port} # Task - watch files and compile as needed task 'watch', 'watch files and compile as needed', -> logging.info 'Watching project...' project.setEnv 'development' # Watch the client directory watch = (done) -> watcher.watchDir(project.clientDir) server.startLiveReloadServer() done(null) if opts.server async.series [server.testLiveReloadPort, build, watch, startServer] else async.series [server.testLiveReloadPort, build, watch] # Task - compile coffeescripts and copy assets into `public/` directory task 'build', 'compile coffeescripts and copy assets into public/ directory', -> logging.info 'Building project...' project.setEnv 'development' build -> {} # Task - minify and concatenate js/css files for production task 'minify', 'minify and concatenate js/css files for production', -> logging.info 'Preparing project files for production...' project.setEnv 'production' minify = (done) -> logging.info 'Minifying project files...' fs.removeSync(project.tempBuildDir) optimizer.optimizeDir(project.buildDir, project.tempBuildDir, done) # Remove temp directories removeTempDirs = (done) -> fs.removeSync(project.buildDir) fs.renameSync(project.tempBuildDir, project.buildDir) done(null) # Concatenate modules concat = (done) -> for path in project.clientConfig.concat logging.info "Concatenating module dependencies: #{path}" optimizer.concatDeps(path) done(null) async.series [build, minify, removeTempDirs, concat] # Task - start a server without watching client files task 'server', 'start a server without watching client files', -> startServer -> {} # Task - remove the build directory task 'clean', 'remove the build directory', -> fs.removeSync(project.buildDir) relativePath = sysPath.relative(process.cwd(), project.buildDir) logging.warn "Removed the build directory at #{relativePath}." # Task - deploy the app task 'deploy', 'deploy the app', -> dest = opts.arguments[1]?.toLowerCase() platforms = ['heroku', 'jitsu', 'gae', 'gh-pages'] deployScriptExists = fs.existsSync('deploy.sh') createDeployScript = -> buildDir = sysPath.relative(process.cwd(), project.buildDir) serverDir = sysPath.relative(process.cwd(), project.serverDir) from = sysPath.join(__dirname, "deploy/#{dest}.sh") to = sysPath.join(process.cwd(), 'deploy.sh') deployScript = _.template(fs.readFileSync(from).toString(), {buildDir, serverDir}) fs.writeFileSync(to, deployScript) invokeScript = -> deploy = spawn 'sh', ['deploy.sh'], {stdio: 'inherit'} deploy.on 'close', (code) -> if code is 0 logging.info "The application has been successfully deployed." else logging.error "Failed to deploy the application." fatal = -> logging.fatal "Must choose a platform from the following: heroku, jitsu, gae, gh-pages" if dest if dest in platforms createDeployScript() invokeScript() else fatal() else if deployScriptExists invokeScript() else fatal() # Print the `--help` usage message and exit. usage = -> console.log BANNER # Print the `--version` message and exit. version = -> path = sysPath.join(__dirname, '../package.json') json = JSON.parse(fs.readFileSync(path)) console.log "muffin.io - version #{json.version}"