Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 3ebdd25
Showing
13 changed files
with
549 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,3 @@ | |||
*.js | |||
!/bin/*.js | |||
node_modules |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,2 @@ | |||
#!/usr/bin/env node | |||
require('../lib/index').cli(process.argv.slice(2)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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.