├── LICENSE ├── README.md ├── bin └── generate ├── lib ├── add_contrib_paths.js ├── computed_property.js ├── find_template.js ├── fs.js ├── generic_generator.js ├── get_generator.js ├── get_templates.js ├── message.js ├── parser.js ├── require_engine.js ├── runner.js └── try_require.js ├── loom.js ├── package.json └── test ├── fixtures ├── one │ ├── engines │ │ ├── noop.js │ │ └── whack.js │ ├── generators │ │ ├── default.js │ │ └── model.js │ └── templates │ │ ├── app │ │ └── model.js.hbs │ │ ├── spec │ │ └── model.spec.js.hbs │ │ └── template_only.whack └── three │ ├── generators │ └── model.js │ └── templates │ └── app │ └── model.js.hbs ├── support ├── fixture.js └── tmp_dir.js └── unit ├── add_contrib_paths.spec.js ├── computed_property.spec.js ├── find_template.spec.js ├── fs.spec.js ├── generic_generator.spec.js ├── get_generator.spec.js ├── get_templates.spec.js ├── message.spec.js ├── parser.spec.js ├── require_engine.spec.js ├── runner.spec.js └── try_require.spec.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Ryan Florence 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Loom 2 | ==== 3 | 4 | Weave your wefts betwixt the warps of loom generators and scaffolds. 5 | 6 | ![wefts and warps](http://ryanflorence.com/gifs/warp-weft.gif) 7 | 8 | **Loom makes it easy to share best-practices and common patterns for app 9 | development.** 10 | 11 | - build a set of generators for public consumption based on some 12 | framework or library (like ember, angular, backbone, etc.) 13 | - consume those sets of generators 14 | - override those generators 15 | - build your own generators for your specific app 16 | 17 | Using Loom Generator Packages from npm 18 | -------------------------------------- 19 | 20 | Using generator packages from npm is easy: 21 | 22 | ```sh 23 | npm install loom-ember --save 24 | npm install loom-ember-qunit --save 25 | generate controller user name:string age:number 26 | ``` 27 | 28 | Then refer to the documentation for the generators you've installed. 29 | 30 | You must install with `--save` or add the module to your package.json 31 | instead (that's how loom knows how to use them). 32 | 33 | If two generator sets respond to the same commands, they will both be 34 | run, allowing authors and consumers to compose them. 35 | 36 | Create New Project Scaffolds with `originate` 37 | --------------------------------------------- 38 | 39 | In addition to generator sets, full project scaffolds are a simple 40 | command away when an author publishes a project to npm with a name 41 | matching `originate-*`. 42 | 43 | For example: 44 | 45 | ```sh 46 | npm install -g loom 47 | originate ember my-new-app 48 | ``` 49 | 50 | Read more about [originate][1]. 51 | 52 | Creating Your Own Generators 53 | ---------------------------- 54 | 55 | Also, see [the full generator API below](#generator-api) 56 | 57 | While using generators others have created for you is great, its awesome 58 | to have a simple way to make generators for our own apps. Even if you're 59 | using a set of generators from npm, defining your own generators will 60 | override them. 61 | 62 | ### Installation 63 | 64 | ```sh 65 | npm install loom -g 66 | generate --init 67 | ``` 68 | 69 | ### Templates 70 | 71 | Initializing loom simply creates some directories in your project. After 72 | that, all you need is a template in `./loom/templates/`: 73 | 74 | Lets say we have a lot of "meal" objects in our app, lets make a 75 | template for what one of these objects looks like: 76 | 77 | _loom/templates/app/meal.js.hbs_ 78 | 79 | ```mustache 80 | function {{objectName}}() { 81 | this.food = '{{params.food}}'; 82 | this.venue = '{{params.venue}}'; 83 | } 84 | ``` 85 | 86 | And then you can generate files based on the template: 87 | 88 | ```sh 89 | generate app/meal.js lunch food:taco venue:cart 90 | ``` 91 | 92 | This will create a file at `app/lunch.js` that looks like: 93 | 94 | ```js 95 | function lunch() { 96 | this.food = 'taco'; 97 | this.venue = 'cart'; 98 | } 99 | ``` 100 | 101 | Loom, by default, will save files to your app in the same relative 102 | location they were found in your templates directory. 103 | 104 | ### Generators 105 | 106 | We can define a generator to make everything a little nicer. First we'll 107 | create a `present` method that determines what data goes to the 108 | template. Then we'll tell it where to find the template so we can 109 | simplify the generator command. 110 | 111 | _loom/generators/meal.js_ 112 | 113 | ```js 114 | exports.present = function(next, env) { 115 | var locals = env.params; 116 | var name = env.args[0]; 117 | locals.constructorName = name.charAt(0).toUpperCase() + name.slice(1); 118 | return locals; 119 | }; 120 | 121 | exports.template = 'app/meal.js.hbs'; 122 | ``` 123 | 124 | Now our template is simpler, no more `{{params.food}}` and it 125 | capitalizes our constructor like a proper lady or gent. 126 | 127 | _loom/templates/meal.js.hbs_ 128 | 129 | ```mustache 130 | function {{constructorName}}() { 131 | this.food = '{{food}}'; 132 | this.venue = '{{venue}}'; 133 | } 134 | ``` 135 | 136 | And finally our command is simpler, it now just matches a generator 137 | named `meal` instead of a template found at `app/meal.js`. 138 | 139 | `generate meal lunch food:taco venue:cart` 140 | 141 | ### Engines 142 | 143 | The default generator uses handlebars, but we can swap it out for ejs by 144 | creating a very simple "engine": 145 | 146 | _loom/engines/ejs.js_ 147 | 148 | ```js 149 | var _ = require('underscore'); 150 | // module.exports = _.template 151 | // that works, but for clarity: 152 | 153 | module.exports = function(src, locals, callback) { 154 | callback(_.template(src, locals)); 155 | }; 156 | ``` 157 | 158 | Rename your template to `meal.js.ejs` and edit it: 159 | 160 | ```ejs 161 | function <%= constructorName %>() { 162 | this.food = '<%= food %>'; 163 | this.venue = '<%= venue %>'; 164 | } 165 | ``` 166 | 167 | Update your generator to point to the proper template: 168 | 169 | ```js 170 | exports.template = 'app/meal.js.ejs'; 171 | ``` 172 | 173 | Loom looks at the file extension of the template (in this case `ejs`) 174 | and then tries to find a template engine named `ejs.js`. 175 | 176 | Now generate your newly configured template: 177 | 178 | `generate meal lunch food:taco venue:cart` 179 | 180 | ### Multiple templates for one generator 181 | 182 | Its very common for a generator to create several files, like unit tests 183 | and scaffoling. Lets add a unit test template to our meal generator. 184 | 185 | _loom/templates/test/unit/meal.spec.js.ejs_ 186 | 187 | ```ejs 188 | describe('<%= constructorName %>', function() { 189 | it('sets food to "<%= food %>"', function() { 190 | var meal = new <%= constructorName %>(); 191 | expect(meal.food).to.equal('<%= food %>'); 192 | }); 193 | }); 194 | ``` 195 | 196 | And add the template path to your generator, note the rename from 197 | `exports.template` to `export.templates`. 198 | 199 | ```js 200 | exports.templates = [ 201 | 'app/meal.js.ejs', 202 | 'test/unit/meal.spec.js.ejs' 203 | ]; 204 | ``` 205 | 206 | Both templates will get the same data from `generator.present` and the 207 | files will be saved to the same relative path in your app as they are 208 | defined in your templates directory. 209 | 210 | ### Default Generators 211 | 212 | If you define `loom/generators/default.js`, loom will use it when a 213 | specific generator is not found. 214 | 215 | 216 | Publishing Generators to npm for Everybody 217 | ------------------------------------------ 218 | 219 | Name your module `loom-` (like 220 | `loom-ember`), place generators, templates, and engines in 221 | `./loom`, and then publish. That's it. People can simply `npm install 222 | loom- --save` and start using them. 223 | 224 | Publishing Template Engines to npm for Everybody 225 | ------------------------------------------------ 226 | 227 | To add support for your favorite templating engine you can either add a 228 | file to `loom/engines` or publish a module to npm named 229 | `loom-engine-`. Loom will attempt to require the engine if it 230 | doesn't find it in your project. 231 | 232 | Generator API 233 | ------------- 234 | 235 | Loom has a generic generator that can be overridden to meet your specific 236 | use case. Generators can export a few methods that loom will use. 237 | 238 | Your generator can implement as many methods as you need, loom will 239 | merge in the `generic_generator` methods that you don't provide. 240 | 241 | Here's a generator that does nothing: 242 | 243 | _loom/generators/noop.js_ 244 | 245 | ```js 246 | exports.before = function(){}; 247 | exports.present = function(){}; 248 | exports.savePath = function(){}; 249 | exports.write = function(){}; 250 | exports.render = function(){}; 251 | exports.template = ''; 252 | // exports.template = function(){}; 253 | // exports.templates = []; 254 | // exports.templates = function(){}; 255 | ``` 256 | 257 | Below is documentation on generator API, also, check out the [generic 258 | generator](lib/generic_generator). 259 | 260 | All methods share the first two arguments: `next`, and `env`. 261 | 262 | - `next` - all methods are asynchronous, so when you're done doing what 263 | you need to do, call `next(val)`. 264 | - `env` - the loom environment, it contains all sorts of information 265 | about the `generate` command the user ran. 266 | 267 | ### generator.before 268 | 269 | Executes before anything else happens. Useful if you need to set or 270 | change some things on `env` before it moves through the other methods of 271 | your generator. 272 | 273 | #### signature 274 | 275 | `function(next, env)` 276 | 277 | 278 | 1. next (Function) - the callback. 279 | 2. env (Object) - the loom environment object. 280 | 281 | 282 | ### generator.present 283 | 284 | You're probably going to want `env.args` and `env.params`. 285 | 286 | #### signature 287 | 288 | `function(next, env)` 289 | 290 | #### arguments 291 | 292 | 1. next (Function) - the callback. 293 | 2. env (Object) - the loom environment object. 294 | 295 | #### examples 296 | 297 | Lets make a generator that logs the arguments to explore how this works. 298 | 299 | _loom/generators/user.js_ 300 | 301 | ```js 302 | exports.present = function(next, env) { 303 | console.log(env); 304 | }; 305 | ``` 306 | 307 | The following are commands followed by what is logged for the arguments: 308 | 309 | ```sh 310 | generate model user name:string age:number 311 | { args: ['user'], params: { name: 'string', age: 'number' } } 312 | 313 | generate model foo bar baz qux:1 quux:2 314 | { args: ['foo', 'bar', 'baz' ] 315 | params: { qux: '1', quux: '2' } } 316 | ``` 317 | 318 | As you can see, the space separated values become the `args` and the 319 | key:value pairs are wrapped up into the `params` argument. 320 | 321 | 322 | ### generator.template 323 | 324 | Determines which template to render. 325 | 326 | `exports.template` can simply be a string, or a function if you need to 327 | compute it. 328 | 329 | Paths are relative to the `./loom/templates` directory. 330 | 331 | #### example 332 | 333 | To use a template found at 334 | `loom/templates/spec/models/model.spec.js.hbs`: 335 | 336 | ```js 337 | exports.template = 'spec/models/model.spec.js.hbs'; 338 | exports.template = function(next) { 339 | // some computation 340 | next('spec/models/model.spec.js.hbs'); 341 | }; 342 | ``` 343 | 344 | #### notes 345 | 346 | Unless you override `generator.write` the generated file will be saved 347 | in the mirrored location in `loom/templates`, so the example above will 348 | be saved to `spec/models/.spec.js`. 349 | 350 | ### generator.templates 351 | 352 | Same as `template` but is an array of template paths that take 353 | precendence over `template`. Each template will receive the same locals 354 | returned from `present`. Can also be a function that calls back an array. 355 | 356 | #### examples 357 | 358 | ```js 359 | exports.templates = [ 360 | 'app/models/model.js.ejs', 361 | 'spec/models/model.spec.js.ejs' 362 | ]; 363 | 364 | exports.templates = function(next) { 365 | next([ 366 | 'app/models/model.js.ejs', 367 | 'spec/models/model.spec.js.ejs' 368 | ]); 369 | }; 370 | ``` 371 | 372 | ### generator.savePath 373 | 374 | Determines the path in which to save a template. 375 | 376 | #### signature 377 | 378 | `function(next, env, template)` 379 | 380 | #### arguments 381 | 382 | 1. next (Function) - callback with the savePath you want 383 | 2. env (Object) - the loom environment object 384 | 3. template (String) - the path of the template being rendered 385 | 386 | ### generator.write 387 | 388 | Writes a rendered template to the file system, its unlikely you'll want 389 | to override this. 390 | 391 | #### signature 392 | 393 | `function(next, env, savePath, src)` 394 | 395 | ### generator.render 396 | 397 | Determines how to render the template, its unlinkely you'll want to 398 | override this. 399 | 400 | #### signature 401 | 402 | `function(next, env, engine, templatePath, locals)` 403 | 404 | TODO 405 | ---- 406 | 407 | - conflict resolution when two generators want to save to the same path 408 | - --force option to overwrite files (better for scripting so you don't 409 | get the prompt) 410 | 411 | License and Copyright 412 | --------------------- 413 | 414 | MIT Style license 415 | 416 | (c) 2013 Ryan Florence 417 | 418 | [1]:https://github.com/rpflorence/originate 419 | 420 | 421 | -------------------------------------------------------------------------------- /bin/generate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../loom')(process.argv); 3 | 4 | -------------------------------------------------------------------------------- /lib/add_contrib_paths.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | /* 4 | * finds any loom-* in package.json so an entire set of generators 5 | * is an npm install away :D 6 | */ 7 | 8 | module.exports = function (program) { 9 | var paths = program.loom.paths; 10 | var packagePath = process.cwd()+'/package.json'; 11 | if (!fs.existsSync(packagePath)) { 12 | return; 13 | } 14 | var json = JSON.parse(fs.readFileSync(packagePath).toString()); 15 | var deps = merge(json.dependencies || {}, json.devDependencies || {}); 16 | for (var dep in deps) { 17 | if (dep.match(/^loom-/) && !dep.match(/^loom-engine/)) { 18 | paths.push('node_modules/'+dep+'/loom'); 19 | } 20 | } 21 | if (fs.existsSync('./loom')) { 22 | // do this last so local templates win, pretty important until conflict 23 | // resolution is implemented 24 | paths.push('loom'); 25 | } 26 | }; 27 | 28 | function merge(target, ext) { 29 | for (var prop in ext) { 30 | target[prop] = ext[prop]; 31 | } 32 | return target; 33 | } 34 | 35 | -------------------------------------------------------------------------------- /lib/computed_property.js: -------------------------------------------------------------------------------- 1 | /* 2 | * gets a value from a value or function 3 | */ 4 | 5 | module.exports = function (/*prop, next, args...*/) { 6 | var args = [].slice.call(arguments, 0); 7 | var prop = args.shift(); 8 | if ('function' == typeof prop) { 9 | prop.apply(null, args); 10 | } else { 11 | args.shift()(prop); 12 | } 13 | }; 14 | 15 | -------------------------------------------------------------------------------- /lib/find_template.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | module.exports = function(template, envPath, callback) { 5 | var templatePath = path.relative( 6 | process.cwd(), 7 | envPath+'/templates/'+template 8 | ); 9 | fs.exists(templatePath, function(exists) { 10 | callback(exists ? templatePath : false); 11 | }); 12 | }; 13 | 14 | -------------------------------------------------------------------------------- /lib/fs.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | var msg = require('./message'); 3 | 4 | module.exports = fs; 5 | 6 | /* 7 | * writes a file unless it exists, then asks the user 8 | */ 9 | 10 | fs.confirmWriteFile = function(fileName, data, callback) { 11 | fs.exists(fileName, function(exists) { 12 | if (exists) { 13 | msg.confirm(fileName+' exists, overwrite?', function(confirmed) { 14 | if (!confirmed) { 15 | msg.fileSkipped(fileName); 16 | callback(null); 17 | } else { 18 | write(); 19 | } 20 | }); 21 | } else { 22 | write(); 23 | } 24 | }); 25 | 26 | function write() { 27 | fs.createFile(fileName, function(err) { 28 | if (err) { callback(err); } 29 | fs.writeFile(fileName, data, function(err) { 30 | if (err) { callback(err); } 31 | msg.fileCreated(fileName); 32 | callback(null); 33 | }); 34 | }); 35 | } 36 | }; 37 | 38 | fs.mkdirpUnlessExistsSync = function(path) { 39 | var result = fs.mkdirpSync(path); 40 | if (result) { 41 | msg.fileCreated(path); 42 | } else { 43 | msg.fileExists(path); 44 | } 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /lib/generic_generator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Generator used to fill in missing methods from custom generators, or serves 3 | * as the generator when none is provided otherwise. 4 | * 5 | * All methods are expected to be overridden by your own generators when the 6 | * defaults don't fit. 7 | */ 8 | 9 | var fs = require('./fs'); 10 | var path = require('path'); 11 | 12 | /* 13 | * Hook to modify `env` before anything else runs 14 | * 15 | * @param {Function} next - callback you must call to continue 16 | * @param {Object} env - loom environment 17 | */ 18 | 19 | exports.before = function(next, env) { 20 | next(); 21 | }; 22 | 23 | /* 24 | * The object returned becomes the object sent to the templating engine. 25 | * 26 | * This default method works for commands like: 27 | * 28 | * `generate model user name:string age:number` 29 | * `generate ` 30 | * 31 | * @param {Function} next - callback you must call to continue 32 | * @param {Object} env - loom environment 33 | */ 34 | 35 | exports.present = function(next, env) { 36 | next({ 37 | objectName: env.args[0], 38 | params: env.params 39 | }); 40 | }; 41 | 42 | /* 43 | * Returns template(s) for the the program to use. Can also define 44 | * `exports.templates` to be used instead, in which case return an array. 45 | * 46 | * @param {Function} next - callback you must call to continue 47 | * @param {Object} env - loom environment 48 | */ 49 | 50 | exports.template = function(next, env) { 51 | var ext = env.name.match(/(\.([^/.]+)){2}$/); 52 | var template = ext ? env.name : env.name+'.hbs'; 53 | next(template); 54 | }; 55 | 56 | /* 57 | * Renders a template with a loom template engine 58 | * 59 | * @param {Function} next - callback you must call to continue 60 | * @param {Object} env - loom environment 61 | * @param {Function} engine 62 | * @param {String} templatePath 63 | * @param {Object} locals 64 | */ 65 | 66 | exports.render = function(next, env, engine, templatePath, locals) { 67 | fs.readFile(templatePath, function(error, data) { 68 | if (error) throw new Error(error); 69 | engine(data.toString(), locals, function(src) { 70 | next(src); 71 | }); 72 | }) 73 | }; 74 | 75 | /* 76 | * Writes rendered templates to the files system. 77 | * 78 | * @param {Function} next - callback you must call to continue 79 | * @param {Object} env - loom environment 80 | * @param {String} savePath - where the file should be written to 81 | * @param {String} src - the template's source to write 82 | */ 83 | 84 | exports.write = function(next, env, savePath, src) { 85 | fs.confirmWriteFile(savePath, src, next); 86 | }; 87 | 88 | /* 89 | * Determines where to save the file. This default method assumes the 90 | * default `present` method along with commands like: 91 | * 92 | * generate model user 93 | * generate 94 | * 95 | * Templates will be saved to the matching directory as their location in your 96 | * app for example: 97 | * 98 | * Given this command: 99 | * 100 | * generate model user 101 | * 102 | * and a generator with a template configured at: 103 | * 104 | * loom/templates/app/model.js.hbs 105 | * 106 | * the file will be saved to: 107 | * 108 | * app/user.js 109 | * 110 | * @param {Function} next - callback you must call to continue 111 | * @param {Object} env - loom environment 112 | * @param {String} template - the template name 113 | * @param {Object} env - the loom environment 114 | */ 115 | 116 | exports.savePath = function(next, env, template) { 117 | var extRegex = /\.[^/.]+$/ 118 | var basename = path.basename(template).replace(extRegex, ''); 119 | var objectName = env.args[0]; 120 | var templateName = env.name.replace(extRegex, ''); 121 | var filename = basename.replace(templateName, objectName); 122 | next(path.dirname(template)+'/'+filename); 123 | }; 124 | 125 | -------------------------------------------------------------------------------- /lib/get_generator.js: -------------------------------------------------------------------------------- 1 | var fs = require('./fs'); 2 | var path = require('path'); 3 | var genericGenerator = require('./generic_generator'); 4 | 5 | module.exports = function(genPath, name, callback) { 6 | var relativePath = path.relative(process.cwd(), genPath+'/generators'); 7 | var basePath = path.resolve(relativePath); 8 | var generatorPath = basePath+'/'+name+'.js'; 9 | var defaultPath = basePath+'/default.js'; 10 | fs.exists(generatorPath, function(exists) { 11 | if (exists) { 12 | return callback(merge(require(generatorPath))); 13 | } 14 | fs.exists(defaultPath, function(exists) { 15 | if (exists) { 16 | return callback(merge(require(defaultPath))); 17 | } 18 | callback(genericGenerator); 19 | }); 20 | }); 21 | }; 22 | 23 | function merge(generator) { 24 | for (var method in genericGenerator) { 25 | if (generator[method]) continue; 26 | generator[method] = genericGenerator[method]; 27 | } 28 | return generator; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /lib/get_templates.js: -------------------------------------------------------------------------------- 1 | var computed = require('./computed_property'); 2 | 3 | /* 4 | * gets templates from the generator from generator.templates or 5 | * generator.template 6 | */ 7 | 8 | module.exports = function(generator, env, callback) { 9 | if (generator.templates) { 10 | computed(generator.templates, callback, env); 11 | } else { 12 | computed(generator.template, function(template) { 13 | callback([template]); 14 | }, env); 15 | } 16 | }; 17 | 18 | -------------------------------------------------------------------------------- /lib/message.js: -------------------------------------------------------------------------------- 1 | /* 2 | * utilities to print messages to the user 3 | */ 4 | 5 | var prompt = require('cli-prompt'); 6 | var color = require('cli-color'); 7 | 8 | var green = color.green; 9 | var yellow = color.yellow; 10 | var blue = color.blue; 11 | var red = color.red; 12 | var silence = false; 13 | 14 | exports.silence = function() { 15 | silence = true; 16 | }; 17 | 18 | exports.assert = function(message, test) { 19 | if (test) return; 20 | throw new Error(message); 21 | }; 22 | 23 | exports.error = function(message, test) { 24 | if (test) return; 25 | print(red('-> ')+message); 26 | process.exit(); 27 | }; 28 | 29 | exports.notify = function(message) { 30 | print(message); 31 | }; 32 | 33 | exports.fileCreated = function(path) { 34 | print(green(" created:\t") + path); 35 | }; 36 | 37 | exports.fileSkipped = function(path) { 38 | print(yellow(" skipped:\t") + path); 39 | }; 40 | 41 | exports.fileExists = function(path) { 42 | print(yellow(" exists:\t") + path); 43 | }; 44 | 45 | exports.confirm = function(question, callback) { 46 | exports.prompt(question+' (y/n)', function(userInput) { 47 | callback(userInput == 'y' || userInput == 'yes'); 48 | }); 49 | }; 50 | 51 | exports.prompt = function(question, callback) { 52 | prompt(yellow('-> ')+question+': ', function(input) { 53 | callback(input.toLowerCase()); 54 | }); 55 | }; 56 | 57 | function print(text) { 58 | if (!silence) console.log(text); 59 | } 60 | 61 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var msg = require('./message'); 3 | var fs = require('./fs'); 4 | var addContribPaths = require('./add_contrib_paths'); 5 | 6 | /* 7 | * Parses the user input into the loom env object, this object gets passed to 8 | * pretty much every other function used. Understand this and you'll understand 9 | * everything else. 10 | */ 11 | 12 | module.exports = function(program) { 13 | program.loom = { 14 | 15 | /* 16 | * args are the space separated values in the command, for example `loom 17 | * model user age=number` would produce `['user']` these are args are 18 | * sent to generator.render 19 | */ 20 | args: [], 21 | 22 | /* 23 | * params are the `foo=bar` values in the command, for example loom model 24 | * user name=string age=number` would produce `{name: 'string', age: 25 | * 'number'}` 26 | */ 27 | params: {}, 28 | 29 | /* 30 | * the name of the generator to use, for example `loom model user` would 31 | * produce `name: model` 32 | */ 33 | name: null, 34 | 35 | // the paths loom will look in to find generators, engines, and templates 36 | paths: program.path ? [program.path] : [] 37 | }; 38 | parseArgsAndParams(program); 39 | shiftGeneratorName(program); 40 | addContribPaths(program); 41 | }; 42 | 43 | function parseArgsAndParams(program) { 44 | var i, arg, split; 45 | for (i = 0; i < program.args.length; i += 1) { 46 | arg = program.args[i]; 47 | split = arg.split(':'); 48 | if (split.length == 2) { 49 | addParam(program, split); 50 | } else { 51 | addArg(program, arg); 52 | } 53 | } 54 | } 55 | 56 | function addParam(program, pair) { 57 | program.loom.params[pair[0]] = pair[1]; 58 | } 59 | 60 | function addArg(program, arg) { 61 | program.loom.args.push(arg); 62 | } 63 | 64 | function shiftGeneratorName(program) { 65 | msg.assert('you must provide a generator name as the first argument', program.loom.args.length); 66 | program.loom.name = program.loom.args.shift(); 67 | } 68 | 69 | -------------------------------------------------------------------------------- /lib/require_engine.js: -------------------------------------------------------------------------------- 1 | var msg = require('./message'); 2 | var tryRequire = require('./try_require'); 3 | 4 | /* 5 | * Finds the template engine from the loom env paths based on the extension of 6 | * the template. For example, foo.hbs would look for engines/hbs.js and if not 7 | * found in the paths will then look for it with `require('loom-engine-hbs')`. 8 | */ 9 | 10 | module.exports = function (template, env) { 11 | var ext = template.match(/\.([^/.]+)$/, ""); 12 | msg.assert("templates must have an extension, no extension found for "+template, ext); 13 | var type = ext[1]; 14 | var local, engine; 15 | for (var i = 0; i < env.paths.length; i += 1) { 16 | local = process.cwd()+'/'+env.paths[i]+'/engines/'+type; 17 | engine = tryRequire(local); 18 | if (engine) { 19 | return engine; 20 | } 21 | } 22 | return tryRequire('loom-engine-'+type); 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /lib/runner.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var msg = require('./message'); 3 | var findTemplate = require('./find_template'); 4 | var getTemplates = require('./get_templates'); 5 | var getGenerator = require('./get_generator'); 6 | var requireEngine = require('./require_engine'); 7 | var async = require('async'); 8 | 9 | /* 10 | * Runs the program, finds all the objects, calls methods on the generator and 11 | * eventually sets the `out` property on the loom env. 12 | */ 13 | 14 | module.exports = function(program, callback) { 15 | var env = program.loom; 16 | env.out = ''; 17 | 18 | msg.notify('generating from:\n - '+env.paths.join('\n - ')); 19 | 20 | async.eachSeries(env.paths, runPath, function() { 21 | callback(env); 22 | }); 23 | 24 | function runPath(envPath, next) { 25 | getGenerator(envPath, env.name, function(generator) { 26 | runGenerator(generator, envPath, next); 27 | }); 28 | } 29 | 30 | function runGenerator(generator, envPath, callback) { 31 | var locals; 32 | generator.before(afterBefore, env); 33 | 34 | function afterBefore() { 35 | generator.present(afterPresent, env); 36 | } 37 | 38 | function afterPresent(obj) { 39 | locals = obj; 40 | getTemplates(generator, env, afterGetTemplates); 41 | } 42 | 43 | function afterGetTemplates(templates) { 44 | async.eachSeries(templates, renderTemplate, callback); 45 | } 46 | 47 | function renderTemplate(template, next) { 48 | findTemplate(template, envPath, handleTemplate); 49 | 50 | function handleTemplate(path) { 51 | if (!path) { 52 | return msg.notify('no template found at '+envPath+'/'+template+'; skipping'); 53 | } 54 | var engine = requireEngine(template, env); 55 | generator.render(afterRender, env, engine, path, locals); 56 | } 57 | 58 | function afterRender(src) { 59 | env.out += src; 60 | if (program.stdout) { return next(); } 61 | generator.savePath(function(savePath) { 62 | generator.write(next, env, savePath, src); 63 | }, env, template); 64 | } 65 | } 66 | } 67 | }; 68 | 69 | -------------------------------------------------------------------------------- /lib/try_require.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Tries to require a module, catches errors. 3 | */ 4 | 5 | module.exports = function (path) { 6 | try { 7 | return require(path); 8 | } catch(e) { 9 | return false; 10 | } 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /loom.js: -------------------------------------------------------------------------------- 1 | var commander = require('commander'); 2 | var fs = require('./lib/fs'); 3 | var run = require('./lib/runner'); 4 | var parse = require('./lib/parser'); 5 | var version = JSON.parse(fs.readFileSync(__dirname+'/package.json')).version; 6 | var msg = require('./lib/message'); 7 | 8 | /* 9 | * It all starts with a string, get it? 10 | * 11 | * You can call loom directly by passing it the string you would have 12 | * used via command line. For example: 13 | * 14 | * loom('model user name:string age:number'); 15 | */ 16 | 17 | module.exports = function(argv, callback) { 18 | var program = new commander.Command(); 19 | 20 | program.option( 21 | '-p, --path [path]', 22 | 'path of directory containing generators' 23 | ); 24 | 25 | program.option( 26 | '-s, --stdout', 27 | 'print result to stdout only' 28 | ); 29 | 30 | program.option( 31 | '-q, --quiet', 32 | 'log nothing, even if --stdout is used' 33 | ); 34 | 35 | program.option( 36 | '--init', 37 | 'creates loom directories to hold your generators' 38 | ); 39 | 40 | if (argv !== process.argv) { 41 | argv = argv.split(' '); 42 | argv.unshift('', ''); 43 | } 44 | 45 | program.parse(argv); 46 | 47 | if (program.init) { 48 | initLoom(); 49 | process.exit(); 50 | } 51 | 52 | if (program.quiet) { 53 | msg.silence(); 54 | } 55 | 56 | parse(program); 57 | 58 | run(program, function(env) { 59 | if (program.stdout) { 60 | msg.notify(program.loom.out); 61 | } 62 | if (callback) { callback(env) }; 63 | }); 64 | }; 65 | 66 | function initLoom() { 67 | [ 68 | 'loom', 69 | 'loom/generators', 70 | 'loom/templates', 71 | 'loom/engines' 72 | ].forEach(fs.mkdirpUnlessExistsSync); 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loom", 3 | "version": "3.1.2", 4 | "description": "Weave your wefts betwixt the warps of loom generators and scaffolds.", 5 | "main": "loom.js", 6 | "scripts": { 7 | "test": "node_modules/.bin/mocha --require should --reporter dot --ui bdd $(find test -name \"*.spec.js\")", 8 | "watch": "node_modules/.bin/mocha --watch --require should --reporter dot --ui bdd $(find test -name \"*.spec.js\")" 9 | }, 10 | "bin": { 11 | "generate": "./bin/generate" 12 | }, 13 | "author": "Ryan Florence", 14 | "license": "MIT", 15 | "peerDependencies": { 16 | "originate": "0.1.x" 17 | }, 18 | "dependencies": { 19 | "commander": "~2.0.0", 20 | "fs-extra": "~0.6.3", 21 | "loom-engine-hbs": "2.0.0", 22 | "cli-color": "~0.2.2", 23 | "cli-prompt": "~0.3.2", 24 | "async": "~0.2.9" 25 | }, 26 | "devDependencies": { 27 | "mocha": "~1.12.0", 28 | "sinon": "~1.7.3", 29 | "should": "~1.2.2" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git://github.com/rpflorence/loom" 34 | }, 35 | "keywords": [ 36 | "generator", 37 | "scaffold", 38 | "template", 39 | "templating" 40 | ] 41 | } -------------------------------------------------------------------------------- /test/fixtures/one/engines/noop.js: -------------------------------------------------------------------------------- 1 | module.exports = function(src, locals, callback) { 2 | callback({ src: src, locals: locals }); 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/one/engines/whack.js: -------------------------------------------------------------------------------- 1 | module.exports = function(template, locals) { 2 | return template.replace(/%FOO%/, 'foo'); 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/one/generators/default.js: -------------------------------------------------------------------------------- 1 | exports.present = function() {}; 2 | 3 | -------------------------------------------------------------------------------- /test/fixtures/one/generators/model.js: -------------------------------------------------------------------------------- 1 | exports.present = function(next, env) { 2 | next({ 3 | name: env.args[0], 4 | fields: env.params 5 | }); 6 | }; 7 | 8 | exports.templates = [ 9 | 'app/model.js.hbs', 10 | 'spec/model.spec.js.hbs' 11 | ]; 12 | 13 | -------------------------------------------------------------------------------- /test/fixtures/one/templates/app/model.js.hbs: -------------------------------------------------------------------------------- 1 | {{name}} {{#each fields}}{{this}}{{/each}} 2 | -------------------------------------------------------------------------------- /test/fixtures/one/templates/spec/model.spec.js.hbs: -------------------------------------------------------------------------------- 1 | {{name}} spec 2 | -------------------------------------------------------------------------------- /test/fixtures/one/templates/template_only.whack: -------------------------------------------------------------------------------- 1 | %FOO% 2 | -------------------------------------------------------------------------------- /test/fixtures/three/generators/model.js: -------------------------------------------------------------------------------- 1 | exports.present = function(next, env) { 2 | next({ 3 | name: env.args[0], 4 | fields: env.params 5 | }); 6 | }; 7 | 8 | exports.template = function(next) { 9 | next('app/model.js.hbs'); 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /test/fixtures/three/templates/app/model.js.hbs: -------------------------------------------------------------------------------- 1 | {{name}} other 2 | -------------------------------------------------------------------------------- /test/support/fixture.js: -------------------------------------------------------------------------------- 1 | module.exports = function(path) { 2 | return process.cwd()+'/test/fixtures/'+path; 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /test/support/tmp_dir.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | var dirPath = process.cwd()+'/test/tmp'; 3 | 4 | exports.create = function(cb) { 5 | if (fs.existsSync(dirPath)) { 6 | cb(); 7 | } else { 8 | fs.mkdir(dirPath, cb); 9 | } 10 | }; 11 | 12 | exports.remove = function(cb) { 13 | if (!fs.existsSync(dirPath)) { 14 | cb(); 15 | } else { 16 | fs.remove(dirPath, cb); 17 | } 18 | }; 19 | 20 | exports.path = dirPath; 21 | 22 | -------------------------------------------------------------------------------- /test/unit/add_contrib_paths.spec.js: -------------------------------------------------------------------------------- 1 | // addContribPaths needs refactoring to write some good tests 2 | 3 | -------------------------------------------------------------------------------- /test/unit/computed_property.spec.js: -------------------------------------------------------------------------------- 1 | var computed = require('../../lib/computed_property'); 2 | 3 | describe('computed_property', function() { 4 | it('returns non-function properties', function(done) { 5 | computed('foo', function(val) { 6 | val.should.eql('foo'); 7 | done(); 8 | }); 9 | }); 10 | 11 | it("returns a function's result", function(done) { 12 | var foo = function(callback) { callback('foo'); }; 13 | computed(foo, function(val) { 14 | val.should.eql('foo'); 15 | done(); 16 | }); 17 | }); 18 | }); 19 | 20 | -------------------------------------------------------------------------------- /test/unit/find_template.spec.js: -------------------------------------------------------------------------------- 1 | var findTemplate = require('../../lib/find_template'); 2 | var fixture = require('../support/fixture'); 3 | 4 | describe('find_template', function() { 5 | it('finds a template', function(done) { 6 | var expected = 'test/fixtures/one/templates/app/model.js.hbs'; 7 | findTemplate('app/model.js.hbs', 'test/fixtures/one', function(template) { 8 | template.should.equal(expected); 9 | done(); 10 | }); 11 | }); 12 | 13 | it('returns false if no template is found', function(done) { 14 | findTemplate('fake.js.hbs', 'test/fixtures/one', function(template) { 15 | template.should.be.false; 16 | done(); 17 | }); 18 | }); 19 | }); 20 | 21 | -------------------------------------------------------------------------------- /test/unit/fs.spec.js: -------------------------------------------------------------------------------- 1 | var sinon = require('sinon'); 2 | var fs = require('../../lib/fs'); 3 | var msg = require('../../lib/message'); 4 | var tmp = require('../support/tmp_dir'); 5 | 6 | msg.silence(); 7 | 8 | describe('fs', function() { 9 | var stub; 10 | 11 | beforeEach(function(next) { 12 | stub = sinon.stub(msg, 'confirm', function(question, callback) { 13 | callback(true); 14 | }); 15 | tmp.create(next); 16 | }); 17 | 18 | afterEach(function(next) { 19 | stub.restore(); 20 | tmp.remove(next); 21 | }); 22 | 23 | describe('confirmWriteFileSync', function() { 24 | it("writes files that don't exists", function(done) { 25 | var path = tmp.path+'/foo.txt'; 26 | fs.confirmWriteFile(path, 'bar', function(err) { 27 | fs.existsSync(path).should.equal(true); 28 | fs.readFileSync(path).toString().should.equal('bar'); 29 | done(); 30 | }); 31 | }); 32 | 33 | it("doesn't write files that exist when users says no", function(done) { 34 | var path = tmp.path+'/foo.txt'; 35 | fs.confirmWriteFile(path, 'bar', function() { 36 | fs.existsSync(path).should.equal(true); 37 | stub.restore(); 38 | stub = sinon.stub(msg, 'confirm', function(question, callback) { 39 | callback(false); 40 | }); 41 | fs.readFileSync(path).toString().should.equal('bar'); 42 | fs.confirmWriteFile(path, 'baz', function() { 43 | fs.readFileSync(path).toString().should.equal('bar'); 44 | done(); 45 | }); 46 | }); 47 | }); 48 | 49 | it("overwrites files that exist when users says yes", function(done) { 50 | var path = tmp.path+'/foo.txt'; 51 | fs.confirmWriteFile(path, 'bar', function() { 52 | fs.existsSync(path).should.equal(true); 53 | fs.readFileSync(path).toString().should.equal('bar'); 54 | fs.confirmWriteFile(path, 'baz', function() { 55 | fs.readFileSync(path).toString().should.equal('baz'); 56 | done(); 57 | }); 58 | }); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/unit/generic_generator.spec.js: -------------------------------------------------------------------------------- 1 | var generator = require('../../lib/generic_generator'); 2 | var fs = require(__dirname+'/../../lib/fs'); 3 | var sinon = require('sinon'); 4 | var fixture = require('../support/fixture'); 5 | var engine = require('../fixtures/one/engines/noop'); 6 | 7 | describe('generic_generator', function() { 8 | 9 | describe('present', function() { 10 | it('returns locals like this:', function(done) { 11 | var env = {name: 'model', args: ['user'], params: {name: 'string'}}; 12 | generator.present(function(locals) { 13 | locals.should.eql({ 14 | objectName: 'user', 15 | params: { name: 'string' } 16 | }); 17 | done(); 18 | }, env); 19 | }); 20 | }); 21 | 22 | describe('render', function() { 23 | it('renders a template', function(done) { 24 | var template = fixture('one/templates/app/model.js.hbs'); 25 | var src = fs.readFileSync(template).toString(); 26 | var locals = {foo: 'bar'}; 27 | generator.render(function(output) { 28 | output.should.eql({ 29 | src: src, 30 | locals: locals 31 | }); 32 | done(); 33 | }, {}, engine, template, locals); 34 | }); 35 | }); 36 | 37 | describe('template', function(done) { 38 | it('returns the name with handlebars as a default when no second extension is provided', function(done) { 39 | generator.template(function(output) { 40 | output.should.eql('foo.js.hbs'); 41 | done(); 42 | }, {name: 'foo.js'}); 43 | }); 44 | 45 | it('returns the template name when extension is provided', function(done) { 46 | generator.template(function(output) { 47 | output.should.eql('foo.js.whack'); 48 | done(); 49 | }, {name: 'foo.js.whack'}); 50 | }); 51 | }); 52 | 53 | describe('savePath', function() { 54 | it('transforms the template path to a sane save path', function(done) { 55 | var env = {name: 'model', args: ['user']}; 56 | generator.savePath(function(path) { 57 | path.should.equal('path/to/user.js'); 58 | done(); 59 | }, env, 'path/to/model.js.hbs'); 60 | }); 61 | }); 62 | 63 | describe('write', function() { 64 | it('writes a template', function() { 65 | var mock = sinon.mock(fs); 66 | var noop = function(){}; 67 | mock.expects('confirmWriteFile').withArgs('path/to/user.js', 'source'); 68 | var env = {name: 'model', args: ['user']}; 69 | generator.write(noop, env, 'path/to/user.js', 'source'); 70 | mock.verify(); 71 | mock.restore(); 72 | }); 73 | }); 74 | }); 75 | 76 | -------------------------------------------------------------------------------- /test/unit/get_generator.spec.js: -------------------------------------------------------------------------------- 1 | var getGenerator = require('../../lib/get_generator'); 2 | var fixture = require('../support/fixture'); 3 | 4 | describe('get_generator', function() { 5 | it('gets a generator from an env.path by name', function(done) { 6 | var expected = require(fixture('one/generators/model')); 7 | getGenerator('test/fixtures/one', 'model', function(generator) { 8 | generator.should.equal(expected); 9 | done(); 10 | }); 11 | }); 12 | 13 | it('gets a default generator if there is one and name matches nothing', function(done) { 14 | var expected = require(fixture('one/generators/default')); 15 | getGenerator('test/fixtures/one', 'nomatch', function(generator) { 16 | generator.should.equal(expected); 17 | done(); 18 | }); 19 | }); 20 | 21 | it('gets generic_generator if there are no generators found', function(done) { 22 | var expected = require('../../lib/generic_generator'); 23 | getGenerator('test/fixtures/three', 'nomatch', function(generator) { 24 | generator.should.equal(expected); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('merges matched generators with generic_generator', function(done) { 30 | getGenerator('test/fixtures/one', 'model', function(generator) { 31 | generator.should.have.property('before'); 32 | generator.should.have.property('present'); 33 | generator.should.have.property('render'); 34 | generator.should.have.property('templates'); 35 | generator.should.have.property('template'); 36 | generator.should.have.property('write'); 37 | done(); 38 | }); 39 | }); 40 | 41 | }); 42 | 43 | 44 | -------------------------------------------------------------------------------- /test/unit/get_templates.spec.js: -------------------------------------------------------------------------------- 1 | var getTemplates = require('../../lib/get_templates'); 2 | 3 | describe('getTemplates', function() { 4 | it('gets generator.template as an array', function(done) { 5 | getTemplates({ template: 'foo' }, {}, function(templates) { 6 | templates.should.eql(['foo']); 7 | done(); 8 | }); 9 | }); 10 | 11 | it('gets generator.templates', function(done) { 12 | getTemplates({ templates: ['foo', 'bar'] }, {}, function(templates) { 13 | templates.should.eql(['foo', 'bar']); 14 | done(); 15 | }); 16 | }); 17 | 18 | it('gets generator.template function as an array', function(done) { 19 | var generator = { 20 | template: function(next, env){ 21 | next('foo'); 22 | } 23 | }; 24 | getTemplates(generator, {}, function(templates) { 25 | templates.should.eql(['foo']); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('gets generator.templates function', function(done) { 31 | var generator = { 32 | templates: function(next, env){ 33 | next(['foo', 'bar']); 34 | } 35 | }; 36 | getTemplates(generator, {}, function(templates) { 37 | templates.should.eql(['foo', 'bar']); 38 | done(); 39 | }); 40 | }); 41 | }); 42 | 43 | -------------------------------------------------------------------------------- /test/unit/message.spec.js: -------------------------------------------------------------------------------- 1 | // meh ... 2 | 3 | -------------------------------------------------------------------------------- /test/unit/parser.spec.js: -------------------------------------------------------------------------------- 1 | var parse = require('../../lib/parser'); 2 | 3 | describe('parser', function() { 4 | it('parses arguments and params', function() { 5 | var program = { 6 | path: 'some/where', 7 | args: ['model', 'user', 'name:string'] 8 | }; 9 | parse(program); 10 | program.loom.should.eql({ 11 | args: ['user'], 12 | name: 'model', 13 | paths: ['some/where'], 14 | params: { name: 'string' } 15 | }); 16 | }); 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /test/unit/require_engine.spec.js: -------------------------------------------------------------------------------- 1 | var requireEngine = require('../../lib/require_engine'); 2 | 3 | describe('requireEngine', function() { 4 | it('requires an engine from env.paths', function() { 5 | var engine = require('../fixtures/one/engines/noop'); 6 | var env = { paths: ['test/fixtures/one'] }; 7 | var template = 'app/model.js.noop'; 8 | requireEngine(template, env).should.equal(engine); 9 | }); 10 | 11 | it('requires an engine from installed node_modules', function() { 12 | var engine = require('loom-engine-hbs'); 13 | var env = { paths: ['test/fixtures'] }; 14 | var template = 'app/model.js.hbs'; 15 | requireEngine(template, env).should.equal(engine); 16 | }); 17 | 18 | }); 19 | 20 | -------------------------------------------------------------------------------- /test/unit/runner.spec.js: -------------------------------------------------------------------------------- 1 | var run = require('../../lib/runner'); 2 | 3 | describe('runner', function(){ 4 | it('generates templates', function(done) { 5 | var program = { 6 | stdout: true, 7 | loom: { 8 | args: ['user'], 9 | name: 'model', 10 | paths: ['test/fixtures/one'], 11 | params: { name: 'string' } 12 | } 13 | }; 14 | run(program, function(env) { 15 | env.should.equal(program.loom); 16 | env.out.should.match(/user string/); 17 | env.out.should.match(/user spec/); 18 | done(); 19 | }); 20 | }); 21 | 22 | it('generates templates for all matching paths', function(done) { 23 | var program = { 24 | stdout: true, 25 | loom: { 26 | args: ['user'], 27 | name: 'model', 28 | paths: ['test/fixtures/one', 'test/fixtures/three'], 29 | params: { name: 'string' } 30 | } 31 | }; 32 | run(program, function(env) { 33 | env.should.equal(program.loom); 34 | env.out.should.match(/user string/); 35 | env.out.should.match(/user spec/); 36 | env.out.should.match(/user other/); 37 | done(); 38 | }); 39 | }); 40 | 41 | }); 42 | 43 | -------------------------------------------------------------------------------- /test/unit/try_require.spec.js: -------------------------------------------------------------------------------- 1 | var tryRequire = require('../../lib/try_require'); 2 | 3 | describe('tryRequire', function() { 4 | it('returns false instead of throwing an error', function() { 5 | tryRequire('flksajf').should.be.false; 6 | }); 7 | 8 | it('returns modules', function() { 9 | var fs = require('fs'); 10 | tryRequire('fs').should.equal(fs); 11 | }); 12 | }); 13 | 14 | --------------------------------------------------------------------------------