Permalink
Browse files

Woot 0.1!

  • Loading branch information...
0 parents commit 3ebdd2572129f8a55485c51a1904c45f2e99d871 @andreyvit committed May 25, 2012
Showing with 549 additions and 0 deletions.
  1. +3 −0 .gitignore
  2. 0 .npmignore
  3. +98 −0 README.md
  4. +2 −0 bin/woot.js
  5. +41 −0 lib/builder.iced
  6. +151 −0 lib/cli.iced
  7. +28 −0 lib/file.iced
  8. +69 −0 lib/formats.coffee
  9. +3 −0 lib/index.coffee
  10. +24 −0 lib/ref.coffee
  11. +15 −0 lib/repository.coffee
  12. +86 −0 lib/template.iced
  13. +29 −0 package.json
@@ -0,0 +1,3 @@
+*.js
+!/bin/*.js
+node_modules
No changes.
@@ -0,0 +1,98 @@
+# Woot! — Instant project creation
+
+Create a template once, use everywhere:
+
+ $ woot npm-package foo-bar
+
+ You will now be prompted for the following values:
+
+ --description VALUE
+
+ You can also provide them on the command line if you want.
+
+ description: Amazing new package
+
+ Argument values:
+ name (underscored) = foo_bar
+ name (dashed) = foo-bar
+ name (camelCase) = fooBar
+ name (CamelCase) = FooBar
+ description (human readable) = Amazing new package
+ github_user (raw) = andreyvit
+
+
+ Is this correct (yes/no) [yes]:
+
+ create /private/tmp/wutest/foo-bar
+ add .npmignore
+ add .gitignore
+ add package.json
+ add README.md
+ add lib/index.coffee
+ add lib/index.js
+ add test/foo_bar_test.coffee
+ add test/foo_bar_test.js
+ run git init
+ run npm install
+
+ Finished.
+
+The second argument defaults to the current folder. Woot never overwrites files, so if you run it again, it will only add the missing ones.
+
+Any subfolder under `~/.woot` is a template. In the future, I might add an option to distribute templates as npm modules (woot-something).
+
+Variables like `__something__` are substituted in file names and data. `__name__` is set to the folder name, other values come from `~/.woot.json`, command-line arguments or interactive answers.
+
+You can save variables to `~/.woot.json` using `--save`:
+
+ woot --github-user andreyvit --save
+
+Add `woot.json` to your template to run some custom commands as the last step:
+
+ {
+ "after": [
+ "git init",
+ "npm install"
+ ]
+ }
+
+
+## Variable substitution details
+
+Variables are automatically transformed by example. If you provide `CoolModel` as a value for model_name, the following substitutions will be made:
+
+ __model_name_raw__ CoolModel # untransformed input
+ __model_name__ cool_model
+ __ModelName__ CoolModel
+ __modelName__ coolModel
+ __model-name__ cool-model
+
+You can append `woot` to any name, which is both cool and helps to provide examples for single-word names:
+
+ __name__ foo_bar
+ __name_woot__ foo_bar
+ __name-woot__ foo-bar
+ __NameWoot__ FooBar
+ __nameWoot__ fooBar
+ __name woot__ foo bar
+ __Name Woot__ Foo Bar
+
+Note that the last two ones (with spaces) are only available via woot (to avoid runaway name lookups). The corresponding substitutions for model_name variable will be:
+
+ __model_name woot__ cool model
+ __Model_Name Woot__ Cool Model
+
+Woot!
+
+
+## Installation
+
+ npm install woot
+
+
+## License
+
+© 2012, Andrey Tarantsov, distributed under the MIT license.
+
+
+## Make woot, not wat!
@@ -0,0 +1,2 @@
+#!/usr/bin/env node
+require('../lib/index').cli(process.argv.slice(2))
@@ -0,0 +1,41 @@
+Path = require 'path'
+fs = require 'fs'
+mkdirp = require 'mkdirp'
+
+{ EventEmitter } = require 'events'
+{ exec } = require 'child_process'
+
+
+module.exports = class WootBuilder extends EventEmitter
+
+ constructor: (@root) ->
+
+ addFile: (relPath, content, autocb) ->
+ absPath = Path.join(@root, relPath)
+
+ await Path.exists absPath, defer(exists)
+ if exists
+ @emit 'exists', relPath
+ return
+
+ await mkdirp Path.dirname(absPath), defer(err)
+ if err
+ @emit 'error', err, relPath
+ return
+
+ await fs.writeFile absPath, content, defer(err)
+ if err
+ @emit 'error', err, relPath
+ return
+
+ @emit 'file', relPath
+
+ execute: (command, autocb) ->
+ @emit 'execute', command
+ await exec command, { cwd: @root }, defer(err, stdout, stderr)
+
+ if err
+ @emit 'error', err
+ return
+
+ @emit 'output', (stdout.trim() + "\n" + stderr.trim()).trim()
@@ -0,0 +1,151 @@
+Path = require 'path'
+fs = require 'fs'
+debug = require('debug')('woot:cli')
+readline = require 'readline'
+dreamopt = require 'dreamopt'
+
+WootRepository = require './repository'
+WootBuilder = require './builder'
+Formats = require './formats'
+
+
+module.exports = (args) ->
+ userSettingsFile = Path.join(process.env.HOME, '.woot.json')
+ userSettings = {}
+ if Path.existsSync(userSettingsFile)
+ userSettings = JSON.parse(fs.readFileSync(userSettingsFile, 'utf-8'))
+
+ # option processing stage 1: find repository and template (if applicable) to get the final option list
+
+ bogusOptions = []
+
+ options = dreamopt [
+ " template"
+ " dir"
+ " -y, --dont-ask"
+ " --save"
+ " --help Disable built-in --help handler by providing a dummy option #bool"
+ ], {
+ # allow any long options at this stage
+ resolveLongOption: (name, options, syntax) ->
+ bogusOptions.push name
+ syntax.add " --#{name} VALUE"
+ }, args
+
+
+ # process --save here and now; can't run stage 2 because we allow saving arbitrary options
+ if options.save
+ for name in bogusOptions
+ userSettings[name] = options[name]
+ fs.writeFileSync userSettingsFile, JSON.stringify(userSettings, null, 2)
+ process.stderr.write "#{userSettingsFile} updated.\n"
+ process.exit 0
+
+
+ # lookup the chosen template and add its options to our list
+ repository = new WootRepository()
+
+ syntax = [
+ "Usage: woot subdir #{options.template || 'template'} [--option VALUE]..."
+
+ "Arguments:"
+ " template Template name to apply"
+ " subdir A folder to operate in; will be created if necessary; defaults to '.' #default(.)", (path) ->
+ return Path.resolve(path)
+
+ "Operation modes:"
+ " --save Save the given options in ~/.woot.json to be reused later"
+
+ "Template generation options:"
+ " -y, --dont-ask Don't ask to confirm the argument values #var(dont_ask)"
+ ]
+
+ if options.template
+ template = repository.find(options.template)
+ unless template
+ process.stderr.write "Template not found: #{options.template}\n"
+ process.exit 1
+
+ await template.scan defer()
+
+ syntax.push "Template arguments:"
+ for param in template.params
+ syntax.push " --#{param.in('dashed')} VALUE #var(#{param.name})"
+
+ syntax.push "Other options:"
+
+
+ # option processing stage 2: this time for real
+ options = dreamopt(syntax, args)
+
+ debug "Options: " + JSON.stringify(options)
+
+ unless Path.existsSync(Path.dirname(options.subdir))
+ process.stderr.write "Parent folder of the target subfolder must exist: #{Path.dirname(options.subdir)}\n"
+ process.exit 1
+
+ values = { name: Path.basename(options.subdir) }
+ missing = []
+ for param in template.params
+ if options.hasOwnProperty(param.name)
+ values[param.name] = options[param.name] # override even if already defined
+ else if values.hasOwnProperty(param.name)
+ # keep the predefined value
+ else if userSettings.hasOwnProperty(param.in('dashed'))
+ values[param.name] = userSettings[param.in('dashed')]
+ else
+ missing.push param
+
+ ri = readline.createInterface(process.stdin, process.stdout, null)
+ ri.on 'close', ->
+ process.stderr.write '\n'
+ process.exit 0
+
+ if missing.length > 0
+ process.stderr.write "\nYou will now be prompted for the following values:\n\n"
+ for param in missing
+ process.stderr.write " --#{param.in('dashed')} VALUE\n"
+
+ process.stderr.write "\nYou can also provide them on the command line if you want.\n"
+
+ for param in missing
+ await ri.question "#{param.name}: ", defer(answer)
+ values[param.name] = answer.trim()
+
+ process.stderr.write "\nArgument values:\n"
+ for param in template.params
+ raw = values[param.name]
+ for format in param.formats()
+ process.stderr.write " #{param.name} (#{format}) = #{Formats[format].build(raw)}\n"
+ process.stderr.write "\n"
+
+ unless options.dont_ask
+ loop
+ await ri.question "Is this correct (yes/no) [yes]: ", defer(answer)
+ break if answer in ['', 'y', 'yes', 'n', 'no']
+ unless answer in ['', 'y', 'yes']
+ process.stderr.write "Cancelled.\n"
+ process.exit 1
+
+ process.stderr.write "\n"
+
+ builder = new WootBuilder(options.subdir)
+ builder.on 'exists', (path) ->
+ process.stderr.write " existing #{path}\n"
+ builder.on 'file', (path) ->
+ process.stderr.write " add #{path}\n"
+ builder.on 'execute', (command) ->
+ process.stderr.write " run #{command}\n"
+ # builder.on 'output', (output) ->
+ # process.stderr.write output + "\n"
+
+ unless Path.existsSync(options.subdir)
+ process.stderr.write " create #{options.subdir}\n"
+ fs.mkdirSync(options.subdir)
+
+ await template.apply builder, values, defer()
+
+ process.stderr.write "\nFinished.\n"
+
+ ri.close()
+ process.stdin.destroy()
@@ -0,0 +1,28 @@
+fs = require 'fs'
+
+WootRef = require './ref'
+
+
+module.exports = class WootFileTemplate
+
+ constructor: (@relPath, @absPath) ->
+
+ scan: (params, callback) ->
+ WootRef.process @relPath, params
+
+ await fs.readFile @absPath, 'utf-8', defer(err, content)
+ return callback(err) if err
+
+ WootRef.process content, params
+
+ callback()
+
+ apply: (builder, values, callback) ->
+ destPath = WootRef.process @relPath, null, values
+
+ await fs.readFile @absPath, 'utf-8', defer(err, content)
+ return callback(err) if err
+
+ content = WootRef.process content, null, values
+
+ builder.addFile destPath, content, callback
@@ -0,0 +1,69 @@
+
+camelCaseToUnderscores = (camelCase) -> camelCase.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/[- ]/g, '_').toLowerCase()
+
+Formats =
+ 'raw':
+ build: (raw) -> raw
+
+ 'underscored':
+ build: (raw) -> camelCaseToUnderscores(raw)
+
+ 'dashed':
+ build: (raw) -> camelCaseToUnderscores(raw).replace(/_/g, '-')
+
+ 'camelCase':
+ build: (raw) -> camelCaseToUnderscores(raw).replace(/_([a-z])/g, (_, x) -> x.toUpperCase())
+
+ 'CamelCase':
+ build: (raw) -> camelCaseToUnderscores(raw).replace(/(?:^|_)([a-z])/g, (_, x) -> x.toUpperCase())
+
+ 'human readable':
+ build: (raw) ->
+ if raw.indexOf(' ') >= 0
+ raw
+ else
+ camelCaseToUnderscores(raw).replace(/_/g, ' ').trim()
+
+ 'Human Readable Title':
+ build: (raw) ->
+ if raw.indexOf(' ') >= 0
+ raw
+ else
+ camelCaseToUnderscores(raw).replace(/_/g, ' ').replace(/(^| )([a-z])/g, (_, p, x) -> p + x.toUpperCase()).trim()
+
+ parse: (name) ->
+ if name.match /_raw$/
+ format = 'raw'
+ name = name.replace /_raw$/, ''
+
+ else if name.indexOf(' ') >= 0
+ if name.search(/[A-Z]/) >= 0
+ format = 'Human Readable Title'
+ else
+ format = 'human readable'
+ name = name.replace(/[ ]/g, '_').toLowerCase()
+
+ else if name.search(/[A-Z]/) >= 0 && name.indexOf('_') < 0
+ if name.search(/^[A-Z]/) >= 0
+ format = 'CamelCase'
+ else
+ format = 'camelCase'
+ name = name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase()
+
+ else if name.indexOf('_') >= 0 && name.search(/[A-Z]/) < 0
+ format = 'underscored'
+ name = name
+
+ else if name.indexOf('-') >= 0 && name.search(/[A-Z]/) < 0
+ format = 'dashed'
+ name = name
+
+ else
+ format = 'underscored'
+ name = name
+
+ name = name.replace /[_ -]woot$/, ''
+
+ return { name, format }
+
+module.exports = Formats
Oops, something went wrong.

0 comments on commit 3ebdd25

Please sign in to comment.