Skip to content

Commit

Permalink
Woot 0.1!
Browse files Browse the repository at this point in the history
  • Loading branch information
andreyvit committed May 25, 2012
0 parents commit 3ebdd25
Show file tree
Hide file tree
Showing 13 changed files with 549 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
@@ -0,0 +1,3 @@
*.js
!/bin/*.js
node_modules
Empty file added .npmignore
Empty file.
98 changes: 98 additions & 0 deletions README.md
@@ -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!
2 changes: 2 additions & 0 deletions bin/woot.js
@@ -0,0 +1,2 @@
#!/usr/bin/env node
require('../lib/index').cli(process.argv.slice(2))
41 changes: 41 additions & 0 deletions lib/builder.iced
@@ -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()
151 changes: 151 additions & 0 deletions lib/cli.iced
@@ -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()
28 changes: 28 additions & 0 deletions lib/file.iced
@@ -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
69 changes: 69 additions & 0 deletions lib/formats.coffee
@@ -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

0 comments on commit 3ebdd25

Please sign in to comment.