├── .eslintrc ├── .gitignore ├── .istanbul.server.yml ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── HISTORY.md ├── LICENSE.txt ├── README.md ├── bin └── denim.js ├── lib ├── index.js ├── init.js ├── prompts.js ├── task.js └── templates.js ├── package.json └── test ├── .eslintrc └── server ├── fixtures ├── formidagon.png ├── formidagon.svg └── formidagon.tmpl.svg ├── mocha.opts ├── setup.js ├── spec ├── base.spec.js ├── bin │ └── denim.js └── lib │ ├── init.spec.js │ ├── prompts.spec.js │ ├── task.spec.js │ └── templates.spec.js └── util.js /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - "formidable/configurations/es5-node" 4 | 5 | rules: 6 | consistent-return: ["error", { "treatUndefinedAsUnspecified": true }] 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | \.git 2 | \.hg 3 | 4 | \.DS_Store 5 | \.project 6 | bower_components 7 | node_modules 8 | npm-debug\.log* 9 | phantomjsdriver\.log 10 | 11 | # Build 12 | dist 13 | */dist 14 | build 15 | */build 16 | coverage 17 | Procfile 18 | yarn.lock 19 | -------------------------------------------------------------------------------- /.istanbul.server.yml: -------------------------------------------------------------------------------- 1 | reporting: 2 | dir: coverage/server 3 | reports: 4 | - lcov 5 | - json 6 | - text-summary 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/bin 3 | !/lib 4 | !/test 5 | !LICENSE.txt 6 | !CONTRIBUTING.md 7 | !HISTORY.md 8 | !README.md 9 | !package.json 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.10" 5 | - "0.12" 6 | - "4" 7 | - "5" 8 | - "6" 9 | 10 | # Use container-based Travis infrastructure. 11 | sudo: false 12 | 13 | branches: 14 | only: 15 | - master 16 | 17 | env: 18 | matrix: 19 | - TEST_NPM_VERSION=2 20 | - TEST_NPM_VERSION=3 21 | - TEST_NPM_VERSION=4 22 | 23 | before_install: 24 | # GUI for real browsers. 25 | - export DISPLAY=:99.0 26 | - sh -e /etc/init.d/xvfb start 27 | 28 | # Use the requested version of npm 29 | - npm install -g "npm@$TEST_NPM_VERSION" 30 | 31 | script: 32 | - npm --version 33 | - npm run check-ci 34 | 35 | # Manually send coverage reports to coveralls. 36 | # - Aggregate client results 37 | # - Single server and func test results 38 | - ls coverage/server/lcov.info | cat 39 | - cat coverage/server/lcov.info | node_modules/.bin/coveralls || echo "Coveralls upload failed" 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Before publishing a new release, make sure to: 5 | 6 | * Run `npm run check` 7 | * Run `npm run build` to rebuild the TOCs in markdown docs. Commit the 8 | file changes. 9 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | History 2 | ======= 3 | 4 | ## 0.1.2 5 | 6 | * Fix bug from previous bugfix where any negated `.gitignore` expression 7 | prevented **any** files from being created from templates. 8 | [#11](https://github.com/FormidableLabs/denim/issues/11) 9 | 10 | ## 0.1.1 11 | 12 | * Fix bug wherein `gitignore-parser` did not correctly match `.gitignore` glob 13 | patterns like `git` actually does. Switch to `parse-gitignore` library and 14 | add regression tests. 15 | [#9](https://github.com/FormidableLabs/denim/issues/9) 16 | 17 | ## 0.1.0 18 | 19 | * Add `_templatesFilter` default derived special variable support. 20 | [#4](https://github.com/FormidableLabs/denim/issues/4) 21 | * Change internal `.gitignore` default filtering to use the resolved path name 22 | (e.g., `"foo/bar.txt"`) instead of unexpanded template path (e.g., 23 | `"{{varForFoo}}/bar.txt"`). 24 | 25 | ## 0.0.3 26 | 27 | * Publish `test/` for other project usage. 28 | 29 | ## 0.0.2 30 | 31 | * Add `package.json:main`. 32 | 33 | ## 0.0.1 34 | 35 | * Initial release. 36 | 37 | [@ryan-roemer]: https://github.com/ryan-roemer 38 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Formidable Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Travis Status][trav_img]][trav_site] 2 | [![Coverage Status][cov_img]][cov_site] 3 | 4 | Denim 5 | =================== 6 | 7 | A lightweight, npm-based template engine. 8 | 9 | 10 | 11 | 12 | 13 | 14 | - [Installation](#installation) 15 | - [Usage](#usage) 16 | - [Installing from a Relative Path on the Local Filesystem](#installing-from-a-relative-path-on-the-local-filesystem) 17 | - [Automating Prompts](#automating-prompts) 18 | - [Template Modules](#template-modules) 19 | - [Templates Module Data](#templates-module-data) 20 | - [Special Variables](#special-variables) 21 | - [Imports and Dependencies](#imports-and-dependencies) 22 | - [User Prompts](#user-prompts) 23 | - [Derived Data](#derived-data) 24 | - [Special Data and Scenarios](#special-data-and-scenarios) 25 | - [`.npmignore`, `.gitignore`](#npmignore-gitignore) 26 | - [Templates Directory Ingestion](#templates-directory-ingestion) 27 | - [Template Parsing](#template-parsing) 28 | - [File Name Parsing](#file-name-parsing) 29 | - [Tips, Tricks, & Notes](#tips-tricks--notes) 30 | - [npmrc File](#npmrc-file) 31 | 32 | 33 | 34 | ## Installation 35 | 36 | Install this package as a global dependency. 37 | 38 | ```sh 39 | $ npm install -g denim 40 | ``` 41 | 42 | Although we generally disfavor global installs, this tool _creates_ new projects 43 | from scratch, so you have to start somewhere... 44 | 45 | 46 | ## Usage 47 | 48 | `denim` can initialize any package that `npm` can 49 | [install](https://docs.npmjs.com/cli/install), including npm, GitHub, file, etc. 50 | 51 | Invocation: 52 | 53 | ```sh 54 | $ denim [flags] 55 | ``` 56 | 57 | Flags: 58 | 59 | ``` 60 | --help 61 | --version 62 | --prompts 63 | ``` 64 | 65 | Examples: 66 | 67 | ```sh 68 | $ denim templates-module 69 | $ denim templates-module@0.2.0 70 | $ denim FormidableLabs/templates-module 71 | $ denim FormidableLabs/templates-module#v0.2.0 72 | $ denim git+ssh://git@github.com:FormidableLabs/templates-module.git 73 | $ denim git+ssh://git@github.com:FormidableLabs/templates-module.git#v0.2.0 74 | $ denim /FULL/PATH/TO/templates-module 75 | ``` 76 | 77 | Internally, `denim` utilizes [`npm pack`](https://docs.npmjs.com/cli/pack) 78 | to download (but not install) a templates package from npm, GitHub, file, etc. 79 | There is a slight performance penalty for things like local files which have to 80 | be compressed and then expanded again, but we gain the very nice benefit of 81 | allowing `denim` to install anything `npm` can in exactly the same 82 | manner that `npm` does. 83 | 84 | ### Installing from a Relative Path on the Local Filesystem 85 | 86 | One exception to the "install like `npm` does" rule is installation from the 87 | **local filesystem**. Internally, `denim` creates a temporary directory 88 | to expand the download from `npm pack` and executes the process in that 89 | directory, meaning that relative paths to a target modules are now incorrect. 90 | 91 | Accordingly, if you _want_ to simulate a relative path install, you can try 92 | something like: 93 | 94 | ```sh 95 | # Mac / Linux 96 | $ denim "${PWD}/../templates-module" 97 | 98 | # Windows 99 | $ denim "%cd%\..\templates-module" 100 | ``` 101 | 102 | ### Automating Prompts 103 | 104 | To facilitate automation, notably testing a module by generating a project 105 | with `denim` and running the project's tests as part of CI, there is a 106 | special `--prompts=JSON_OBJECT` flag that skips the actual input prompts and 107 | injects fields straight from a JSON object. 108 | 109 | ```sh 110 | $ denim \ 111 | --prompts'{"name":"bob","quest":"popcorn","destination":"my-project"}' 112 | ``` 113 | 114 | Note that _all_ required fields must be provided in the JSON object, no defaults 115 | are used, and the init process will fail if there are any missing fields. 116 | **Tip**: You will need a `destination` value, which is added to all prompts. 117 | 118 | 119 | ## Template Modules 120 | 121 | Templates are created within a first class `npm` module. It could be your 122 | projects shared utilities module or a standalone template bootstrap module. 123 | The main point is creating something `npm`-installable that is lightweight for 124 | bootstrapping your templated projects. 125 | 126 | A `denim` project is controlled with: 127 | 128 | * **`denim.js`**: A control file for user prompts and data. 129 | * **`templates/`**: A directory of templates to inflate during initialization. 130 | This directory can be configured with user prompts / data by setting the 131 | special `_templatesDir` variable to something different than `"templates"`. 132 | 133 | For example, in `templates-module`, we have a control file and templates 134 | as follows: 135 | 136 | ``` 137 | denim.js 138 | templates/ 139 | .babelrc 140 | .editorconfig 141 | .travis.yml 142 | CONTRIBUTING.md 143 | demo/app.jsx 144 | demo/index.html 145 | LICENSE.txt 146 | package.json 147 | README.md 148 | src/components/{{componentPath}}.jsx 149 | src/index.js 150 | test/client/main.js 151 | test/client/spec/components/{{componentPath}}.spec.jsx 152 | test/client/test.html 153 | {{_gitignore}} 154 | {{_npmignore}} 155 | ``` 156 | 157 | ### Templates Module Data 158 | 159 | Packages provide data for template expansion via a `denim.js` file in the 160 | root of the module. The structure of the file is: 161 | 162 | ```js 163 | module.exports = { 164 | destination: // A special prompt for output destination directory. 165 | prompts: // Questions and responses for the user 166 | derived: // Other fields derived from the data provided by the user 167 | }; 168 | ``` 169 | 170 | Note that `denim` requires `destination` output directories to not exist 171 | before writing for safety and initialization sanity. 172 | 173 | #### Special Variables 174 | 175 | There are several default data fields provided by denim that can be overridden 176 | in `denim.js` configuration files. A brief list: 177 | 178 | * _Control_ 179 | * `_templatesDir` (`"templates"`): The directory root of the templates to 180 | use during inflation. 181 | * `_templatesFilter` (_a noop function_): A function with the signature 182 | `(filePath, isIncluded)` where `filePath` is the resolved path to a file 183 | (relative to templates dir), and `isIncluded` is a boolean indicating 184 | whether or not denim would ordinarily include it (e.g., it is not excluded 185 | by the `.gitignore`). An overriding function should return `true` or 186 | `false` based on custom logic and can optionally use the `isIncluded` 187 | parameter from denim's default logic. 188 | * _File naming helpers_ 189 | * `_gitignore` (`".gitignore"`) 190 | * `_npmignore` (`".npmignore"`) 191 | * `_npmrc` (`".npmrc"`): 192 | * `_eslintrc` (`".eslintrc"`) 193 | 194 | #### Imports and Dependencies 195 | 196 | The `denim.js` file is `require`-ed from a temporary `extracted` directory 197 | containing the full module. However, an `npm install` is not run in the 198 | module directory prior to starting the initialization process. This means 199 | that you can `require` in: 200 | 201 | * Files contained in the module itself. 202 | * Any standard node libraries. (E.g., `require("path")`, `require("fs")`). 203 | 204 | Unfortunately, you cannot require third party libraries or things that may 205 | be found in `/node_modules/`. (E.g., `require("lodash")`). 206 | 207 | This is a good thing, because the common case is that you will need nearly 208 | _none_ of the dependencies in `denim.js` prompting that are used in the module 209 | itself, so `denim` remains lightening quick by _not_ needing to do any 210 | `npm install`-ing. 211 | 212 | #### User Prompts 213 | 214 | User prompts and responses are ingested using [inquirer][]. The `prompts` field 215 | of the `denim.js` object can either be an _array_ or _object_ of inquirer 216 | [question objects][inq-questions]. For example: 217 | 218 | ```js 219 | module.exports = { 220 | // Destination directory to write files to. 221 | // 222 | // This field is deep merged and added _last_ to the prompts so that module 223 | // authors can add `default` values or override the default message. You 224 | // could further override the `validate` function, but we suggest using the 225 | // existing default as it checks the directory does not already exist (which 226 | // is enforced later in code). 227 | destination: { 228 | default: function (data) { 229 | // Use the early `name` prompt as the default value for our dest directory 230 | return data.name; 231 | } 232 | }, 233 | 234 | prompts: [ 235 | { 236 | name: "name", 237 | message: "What is your name?", 238 | validate: function (val) { 239 | // Validate functions return `true` if valid. 240 | // If invalid, return `false` or an error message. 241 | return !!val.trim() || "Must enter a name!"; 242 | } 243 | }, 244 | { 245 | name: "quest", 246 | message: "What is your quest?" 247 | } 248 | ] 249 | }; 250 | ``` 251 | 252 | `denim` provides a short-cut of placing the `name` field as the key 253 | value for a `prompts` object instead of an array: 254 | 255 | ```js 256 | module.exports = { 257 | prompts: { 258 | name: { 259 | message: "What is your name?", 260 | validate: function (val) { return !!val.trim() || "Must enter a name!"; } 261 | }, 262 | quest: { 263 | message: "What is your quest?" 264 | } 265 | } 266 | }; 267 | ``` 268 | 269 | **Note - Async**: Inquirer has some nice features, one of which is enabling 270 | functions like `validate` to become async by using `this.async()`. For 271 | example: 272 | 273 | ```js 274 | name: { 275 | message: "What is your name?", 276 | validate: function (val) { 277 | var done = this.async(); 278 | 279 | // Let's wait a second. 280 | setTimeout(function () { 281 | done(!!val.trim() || "Must enter a name!") 282 | }, 1000); 283 | } 284 | } 285 | ``` 286 | 287 | #### Derived Data 288 | 289 | Module authors may not wish to expose _all_ data for user input. Thus, 290 | `denim` supports a simple bespoke scheme for taking the existing user 291 | data and adding derived fields. 292 | 293 | The `derived` field of the `denim.js` object is an object of functions with 294 | the signature: 295 | 296 | ```js 297 | derived: { 298 | // - `data` All existing data from user prompts. 299 | // - `callback` Callback of form `(error, derivedData)` 300 | upperName: function (data, cb) { 301 | // Uppercase the existing `name` data. 302 | cb(null, data.name.toUpperCase()); 303 | } 304 | } 305 | ``` 306 | 307 | ### Special Data and Scenarios 308 | 309 | #### `.npmignore`, `.gitignore` 310 | 311 | **The Problem** 312 | 313 | Special files like `.npmrc`, `.npmignore`, and `.gitignore` in a `templates/` 314 | directory are critical to the correct publishing / git lifecycle of a created 315 | project. However, publishing `templates/` to npm as part of publishing the 316 | module and even initializing off of a local file path via `npm pack` does not 317 | work well with the basic layout of: 318 | 319 | ``` 320 | templates/ 321 | .gitignore 322 | .npmignore 323 | .npmrc 324 | ``` 325 | 326 | The problem is that the `.npmignore` affects and filters out files that will 327 | be available for template use in an undesirable fashion. For example, in 328 | `templates-module` which has an `.npmignore` which includes: 329 | 330 | ``` 331 | demo 332 | test 333 | .editor* 334 | .travis* 335 | ``` 336 | 337 | natural `npm` processes would exclude all of the following template files: 338 | 339 | ``` 340 | templates/.editorconfig 341 | templates/.travis.yml 342 | templates/test/client/main.js 343 | templates/test/client/spec/components/{{componentPath}}.spec.jsx 344 | templates/test/client/test.html 345 | templates/demo/app.jsx 346 | templates/demo/index.html 347 | ``` 348 | 349 | Adding even more complexity to the situation is the fact that if `npm` doesn't 350 | find a `.npmignore` on publishing or `npm pack` it will rename `.gitignore` to 351 | `.npmignore`. 352 | 353 | **The Solution** 354 | 355 | To address this, we have special `derived` values built in by default to 356 | `denim`. You do _not_ need to add them to your `denim.js`: 357 | 358 | * `{{_gitignore}}` -> `.gitignore` 359 | * `{{_npmignore}}` -> `.npmignore` 360 | * `{{_npmrc}}` -> `.npmrc` 361 | * `{{_eslintrc}}` -> `.eslintrc` 362 | 363 | In your module `templates` directory you should add any / none of these files 364 | with the following names instead of their real ones: 365 | 366 | ``` 367 | templates/ 368 | {{_gitignore}} 369 | {{_npmignore}} 370 | {{_npmrc}} 371 | {{_eslintrc}} 372 | ``` 373 | 374 | As a side note for your git usage, this now means that `templates/.gitignore` 375 | doesn't control the templates anymore and your module's root `.gitignore` 376 | must appropriately ignore files in `templates/` for git commits. 377 | 378 | 379 | ### Templates Directory Ingestion 380 | 381 | As a preliminary matter, `templates/` is the out-of-the box templates directory 382 | default for a special prompts variable `_templatesDir`. You can override this in 383 | an `denim.js` either via `prompts` (allowing a user to pick a value) or `derived` 384 | data. Either of these approaches can choose 1+ different directories to find 385 | templates than the default `templates/`. 386 | 387 | `denim` mostly just walks the templates directory of a module looking 388 | for any files with the following features: 389 | 390 | * An empty templates directory is permitted, but a non-existent one will produce 391 | an error. 392 | * If an `<_templatesDir>/.gitignore` file is found, the files matched in the 393 | templates directory will be filtered to ignore any `.gitignore` glob matches. 394 | This filtering is done at _load_ time before file name template strings are 395 | expanded (in case that matters). 396 | 397 | `denim` tries to intelligently determine if files in the templates 398 | directory are actually text template files with the following heuristic: 399 | 400 | 1. Inspect the magic numbers for known text files and opportunistically the 401 | byte range of the file buffer with https://github.com/gjtorikian/isBinaryFile. 402 | If binary bytes detected, don't process. 403 | 2. Inspect the magic numbers for known binary types with 404 | https://github.com/sindresorhus/file-type 405 | If known binary type detected, don't process. 406 | 3. Otherwise, try to process as a template. 407 | 408 | If this heuristic approach proves too complicated / problematic, we'll 409 | reconsider the approach. 410 | 411 | ### Template Parsing 412 | 413 | `denim` uses Lodash templates, with the following customizations: 414 | 415 | * ERB-style templates are the only supported format. The new ES-style template 416 | strings are disabled because the underlying processed code is likely to 417 | include JS code with ES templates. 418 | * HTML escaping by default is disabled so that we can easily process `<`, `>`, 419 | etc. symbols in JS. 420 | 421 | The Lodash templates documentation can be found at: 422 | https://github.com/lodash/lodash/blob/master/lodash.js#L12302-L12365 423 | 424 | And, here's a quick refresher: 425 | 426 | **Variables** 427 | 428 | ```js 429 | var compiled = _.template("Hi <%= user %>!"); 430 | console.log(compiled({ user: "Bob" })); 431 | // => "Hi Bob!" 432 | ``` 433 | 434 | ```js 435 | var compiled = _.template( 436 | "Hi <%= _.map(users, function (u) { return u.toUpperCase(); }).join(\", \") %>!"); 437 | console.log(compiled({ users: ["Bob", "Sally"] })); 438 | // => Hi BOB, SALLY! 439 | ``` 440 | 441 | **JavaScript Interpolation** 442 | 443 | ```js 444 | var compiled = _.template( 445 | "Hi <% _.each(users, function (u, i) { %>" + 446 | "<%- i === 0 ? '' : ', ' %>" + 447 | "<%- u.toUpperCase() %>" + 448 | "<% }); %>!"); 449 | console.log(compiled({ users: ["Bob", "Sally"] })); 450 | // => Hi BOB, SALLY! 451 | ``` 452 | 453 | ### File Name Parsing 454 | 455 | In addition file _content_, `denim` also interpolates and parses file 456 | _names_ using an alternate template parsing scheme, inspired by Mustache 457 | templates. (The rationale for this is that ERB syntax is not file-system 458 | compliant on all OSes). 459 | 460 | So, if we have data: `packageName: "whiz-bang-component"` and want to create 461 | a file-system path: 462 | 463 | ``` 464 | src/components/whiz-bang-component.jsx 465 | ``` 466 | 467 | The source module should contain a full file path like: 468 | 469 | ``` 470 | templates/src/components/{{packageName}}.jsx 471 | ``` 472 | 473 | `denim` will validate the expanded file tokens to detect clashes with 474 | other static file names provided by the generator. 475 | 476 | 477 | ## Tips, Tricks, & Notes 478 | 479 | ### npmrc File 480 | 481 | If you use Private npm, or a non-standard registry, or anything leveraging a 482 | custom [`npmrc`](https://docs.npmjs.com/files/npmrc) file, you need to set 483 | a **user** (`~/.npmrc`) or **global** (`$PREFIX/etc/npmrc`) npmrc file. 484 | 485 | `denim` relies on `npm pack` under the hood and runs from a temporary 486 | directory completely outside of the current working directory. So, while 487 | `npm info ` or `npm pack ` would work just fine with an 488 | `.npmrc` file in the current working directory, `denim` will not. 489 | 490 | 491 | [inquirer]: https://github.com/SBoudrias/Inquirer.js 492 | [inq-questions]: https://github.com/SBoudrias/Inquirer.js#question 493 | [trav_img]: https://api.travis-ci.org/FormidableLabs/denim.svg 494 | [trav_site]: https://travis-ci.org/FormidableLabs/denim 495 | [cov_img]: https://img.shields.io/coveralls/FormidableLabs/denim.svg 496 | [cov_site]: https://coveralls.io/r/FormidableLabs/denim 497 | 498 | 499 | ## Maintenance Status 500 | 501 | **Archived:** This project is no longer maintained by Formidable. We are no longer responding to issues or pull requests unless they relate to security concerns. We encourage interested developers to fork this project and make it their own! 502 | -------------------------------------------------------------------------------- /bin/denim.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | var _ = require("lodash"); 5 | 6 | var init = require("../lib/index").init; 7 | var pkg = require("../package.json"); 8 | 9 | var SCRIPT_NAME = "denim"; 10 | var INIT_FILE = "denim.js"; 11 | var TEMPLATES_DIR = "templates"; 12 | 13 | // Runner 14 | var run = module.exports = function (opts, callback) { 15 | return init(_.extend({ 16 | script: SCRIPT_NAME, 17 | version: pkg.version, 18 | initFile: INIT_FILE, 19 | prompts: { 20 | derived: { 21 | // Directory containing templates 22 | _templatesDir: function (data, cb) { cb(null, TEMPLATES_DIR); } 23 | } 24 | } 25 | }, opts), callback); 26 | }; 27 | 28 | // Script 29 | if (require.main === module) { 30 | run(null, function (err) { 31 | // Try to get full stack, then full string if not. 32 | if (err) { 33 | console.error(err.stack || err.toString()); // eslint-disable-line no-console 34 | } 35 | 36 | process.exit(err ? err.code || 1 : 0); // eslint-disable-line no-process-exit 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var init = require("./init"); 4 | 5 | module.exports = { 6 | init: init 7 | }; 8 | -------------------------------------------------------------------------------- /lib/init.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var path = require("path"); 4 | var async = require("async"); 5 | var chalk = require("chalk"); 6 | 7 | var prompts = require("../lib/prompts"); 8 | var Templates = require("../lib/templates"); 9 | var Task = require("../lib/task"); 10 | 11 | /** 12 | * Initialization wrapper. 13 | * 14 | * @param {Object} opts Options object for Task. 15 | * @param {Array} opts.argv Arguments array (Default: `process.argv`) 16 | * @param {Object} opts.env Environment object to mutate (Default `process.env`) 17 | * @param {Object} opts.script Environment object to mutate (Default `opts.argv[1]`) 18 | * @param {String} opts.version Script version. (Default: `UNKNOWN`) 19 | * @param {String} opts.initFile Initialization file. 20 | * @param {Object} opts.prompts Prompts, derived defaults. 21 | * @param {Function} callback Callback from script run `(err, results)`. 22 | * @returns {void} 23 | */ 24 | module.exports = function (opts, callback) { 25 | opts = opts || {}; 26 | var task = new Task(opts); 27 | var bin = chalk.green.bold("[" + task.script + "]"); 28 | 29 | // Help, version, etc. - just call straight up. 30 | if (!task.isInflate()) { 31 | return void task.execute(callback); 32 | } 33 | 34 | // Actual initialization. 35 | async.auto({ 36 | download: task.execute.bind(task), 37 | 38 | data: ["download", function (results, cb) { 39 | process.stdout.write( 40 | bin + " Preparing templates for: " + chalk.magenta(task.modName) + "\n" 41 | ); 42 | 43 | prompts({ 44 | init: results.download.data, 45 | defaults: opts.prompts, 46 | src: results.download.src 47 | }, cb); 48 | }], 49 | 50 | templates: ["data", function (results, cb) { 51 | if (!results.data.destination) { 52 | return void cb(new Error("Destination field missing from prompts")); 53 | } 54 | 55 | var templates = new Templates({ 56 | src: results.download.src, 57 | dest: path.resolve(results.data.destination), 58 | data: results.data 59 | }); 60 | 61 | templates.process(function (err, outFiles) { 62 | if (err) { return void cb(err); } 63 | 64 | process.stdout.write( 65 | "\n" + bin + " Wrote files: \n" + 66 | (outFiles || []).map(function (obj) { 67 | return " - " + chalk.cyan(path.relative(process.cwd(), obj.dest)); 68 | }).join("\n") + "\n" 69 | ); 70 | 71 | cb(); 72 | }); 73 | }] 74 | }, function (err, results) { 75 | if (!err) { 76 | process.stdout.write( 77 | "\n" + bin + " New " + chalk.magenta(task.modName) + " project is ready at: " + 78 | chalk.cyan(path.relative(process.cwd(), results.data.destination)) + "\n" 79 | ); 80 | } 81 | 82 | callback(err); 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /lib/prompts.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"); 4 | var path = require("path"); 5 | var _ = require("lodash"); 6 | var async = require("async"); 7 | var inquirer = require("inquirer"); 8 | 9 | /** 10 | * Default prompts values added to all prompt calls. 11 | */ 12 | var DEFAULTS = { 13 | destination: { 14 | message: "Destination directory to write", 15 | validate: function (val) { 16 | var done = this.async(); 17 | 18 | if (!val) { 19 | return void done("Must specify a destination directory"); 20 | } 21 | 22 | fs.stat(val, function (err) { 23 | if (err && err.code === "ENOENT") { return void done(true); } 24 | 25 | return void done("Destination directory must not already exist"); 26 | }); 27 | } 28 | }, 29 | derived: { 30 | // Directory containing templates 31 | _templatesDir: function (data, cb) { cb(null, "templates"); }, 32 | 33 | // Filter function for inclusion of resolved template files. 34 | // 35 | // Should call back with a function of the signature `(filePath, isIncluded)` 36 | // where the parameters are: 37 | // - `filePath`: resolved filepath of a template file 38 | // - `isIncluded`: boolean indicating if denim default logic would include 39 | // - return value: a boolean w/ `true` for "keep", `false` for "exclude" 40 | _templatesFilter: function (data, cb) { 41 | cb(null, function (filePath, isIncluded) { 42 | return isIncluded; // Simple proxy default included value. 43 | }); 44 | }, 45 | 46 | // `.npmignore` and `.gitignore` need to be proxied as a template to avoid 47 | // NPM losing dev files in `init/` when uploading and executing `npm pack` 48 | // so we provide them by default here. 49 | _npmignore: function (data, cb) { cb(null, ".npmignore"); }, 50 | _gitignore: function (data, cb) { cb(null, ".gitignore"); }, 51 | _eslintrc: function (data, cb) { cb(null, ".eslintrc"); }, 52 | _npmrc: function (data, cb) { cb(null, ".npmrc"); } 53 | } 54 | }; 55 | 56 | /** 57 | * Special extra data fields to add after prompts are proceed. 58 | */ 59 | var EXTRA_DATA_FIELDS = { 60 | _extractedModulePath: function (opts) { 61 | return opts.src ? path.join(opts.src, "extracted") : null; 62 | } 63 | }; 64 | 65 | /** 66 | * Prompt user for input, validate, and add derived fields to final data object. 67 | * 68 | * @param {Object} opts Options object 69 | * @param {Object} opts.init Initialization configuration (`prompts`, `derived`) 70 | * @param {Object} opts.defaults Defaults merged in with lib defaults (`prompts`, `derived`) 71 | * @param {Object} opts.src Extracted module path. 72 | * @param {Function} callback Calls back with `(err, data)` 73 | * @returns {void} 74 | */ 75 | // eslint-disable-next-line max-statements, complexity 76 | module.exports = function (opts, callback) { 77 | // Params 78 | opts = _.clone(opts || {}); 79 | var defaults = opts.defaults || {}; 80 | var derivedBase = _.extend({}, DEFAULTS.derived, defaults.derived); 81 | 82 | // Validate. 83 | var init = opts.init || null; 84 | if (!init) { return void callback(new Error("Invalid init object")); } 85 | if (init.prompts) { 86 | if (!(_.isArray(init.prompts) || _.isObject(init.prompts))) { 87 | return void callback(new Error("Invalid prompts type: " + typeof init.prompts)); 88 | } else if (_.some(init.prompts, function (v, k) { 89 | // Disallow `_` prompts that **aren't** special variables 90 | return k.indexOf("_") === 0 && !derivedBase[k]; 91 | })) { 92 | return void callback(new Error( 93 | "User prompts cannot start with '_' unless overriding special variables: " + 94 | JSON.stringify(init.prompts))); 95 | } 96 | } 97 | 98 | var promptsBase = init.prompts && !_.isArray(init.prompts) ? {} : []; 99 | var prompts = _.extend(promptsBase, DEFAULTS.prompts, defaults.prompts, init.prompts); 100 | var dest = _.extend({}, DEFAULTS.destination, defaults.destination, init.destination); 101 | 102 | // Mutate objects to arrays if needed. 103 | prompts = _.isArray(prompts) ? prompts : _.map(prompts, function (val, key) { 104 | return _.extend({ name: key }, val); 105 | }); 106 | 107 | // Add in special `destination` prompt field if not provided by `init.js` 108 | if (!_.includes(prompts, { name: "destination" })) { 109 | prompts.push(_.merge({ name: "destination" }, dest)); 110 | } 111 | 112 | // Prompt overrides to skip actual user input. 113 | var overrides; 114 | if (init.overrides) { 115 | try { 116 | overrides = module.exports._parseOverrides(init.overrides); 117 | } catch (err) { 118 | return void callback(new Error("Prompt overrides loading failed with: " + err.message)); 119 | } 120 | } 121 | 122 | // Execute prompts, then derive final data. 123 | async.auto({ 124 | extra: function (cb) { 125 | var data = _.reduce(EXTRA_DATA_FIELDS, function (memo, fn, field) { 126 | memo[field] = fn(opts); 127 | return memo; 128 | }, {}); 129 | 130 | cb(null, data); 131 | }, 132 | 133 | prompts: function (cb) { 134 | // Allow `--prompts=JSON_STRING` overrides. 135 | if (overrides) { return void cb(null, overrides); } 136 | 137 | // Get user prompts. No error, because will prompt user for new input. 138 | inquirer.prompt(prompts, function (data) { cb(null, data); }); 139 | }, 140 | 141 | derived: ["extra", "prompts", function (results, cb) { 142 | // Create object of functions bound to user input data and invoke. 143 | var data = _.merge({}, results.extra, results.prompts); 144 | 145 | // Add deriveds in order of built-in defaults, program, then init. 146 | // 147 | // **Note**: This _does_ mean that simply overriding a default will not 148 | // prevent it's execution. But it also allows us to chain together groups 149 | // of defaults. We _may_ revisit this logic in the future. 150 | // 151 | // Takeaway: Ensure it's OK that any defaults run. 152 | async.eachSeries([ 153 | DEFAULTS.derived, 154 | defaults.derived, 155 | init.derived 156 | ], function (derivedObj, eachCb) { 157 | 158 | var fns = _(derivedObj) 159 | // Remove keys that are set from prompts. 160 | .pickBy(function (fn, key) { 161 | return !_.has(results.prompts, key); 162 | }) 163 | // Add in our mutating data object. 164 | .mapValues(function (fn) { 165 | return fn.bind(null, data); 166 | }) 167 | .value(); 168 | 169 | async.auto(fns, function (err, eachResults) { 170 | // Mutate data. 171 | data = _.merge(data, eachResults); 172 | 173 | eachCb(err); 174 | }); 175 | }, function (err) { 176 | cb(err, data); 177 | }); 178 | }] 179 | }, function (err, results) { 180 | var data = results ? _.extend({}, results.prompts, results.derived) : null; 181 | callback(err, data); 182 | }); 183 | }; 184 | 185 | /** 186 | * Parse overrides string into object. 187 | * 188 | * Also handles a few scenarios like surrounding single / double quotes. 189 | * 190 | * @param {String} str JSON string 191 | * @returns {Object} JS object 192 | */ 193 | module.exports._parseOverrides = function (str) { 194 | // Remove quotes. 195 | str = str 196 | .trim() 197 | .replace(/^"{(.*)}"$/, "{$1}") 198 | .replace(/^'{(.*)}'$/, "{$1}"); 199 | 200 | return JSON.parse(str); 201 | }; 202 | 203 | // Expose helpers for testing. 204 | module.exports._DEFAULTS = DEFAULTS; 205 | module.exports._EXTRA_DATA_FIELDS = EXTRA_DATA_FIELDS; 206 | 207 | -------------------------------------------------------------------------------- /lib/task.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var childProc = require("child_process"); 4 | var path = require("path"); 5 | var zlib = require("zlib"); 6 | var _ = require("lodash"); 7 | var fs = require("fs-extra"); 8 | var async = require("async"); 9 | var chalk = require("chalk"); 10 | var nopt = require("nopt"); 11 | var tar = require("tar"); 12 | var temp = require("temp").track(); // track: Clean up all files on process exit. 13 | 14 | var OPTIONS = { 15 | "help": Boolean, 16 | "version": Boolean, 17 | "prompts": String 18 | }; 19 | 20 | var SHORT_OPTIONS = { 21 | "h": ["--help"], 22 | "v": ["--version"] 23 | }; 24 | 25 | /** 26 | * Task wrapper. 27 | * 28 | * @param {Object} opts Options object 29 | * @param {Array} opts.argv Arguments array (Default: `process.argv`) 30 | * @param {Object} opts.env Environment object to mutate (Default `process.env`) 31 | * @param {Object} opts.script Script name. (Default: `opts.argv[1]`) 32 | * @param {String} opts.version Script version. (Default: `UNKNOWN`) 33 | * @param {String} opts.initFile Initialization file. 34 | * @returns {void} 35 | */ 36 | // eslint-disable-next-line max-statements 37 | var Task = module.exports = function (opts) { 38 | opts = opts || {}; 39 | this.env = opts.env || process.env; 40 | var argv = opts.argv || process.argv; 41 | this.script = opts.script || argv[1]; 42 | this.scriptVersion = opts.version || "UNKNOWN"; 43 | 44 | this.initFile = opts.initFile; 45 | if (!this.initFile) { 46 | throw new Error("initFile option required"); 47 | } 48 | 49 | // Parse args. 50 | var parsed = nopt(OPTIONS, SHORT_OPTIONS, argv); 51 | this.promptsOverrides = (parsed.prompts || "").trim(); 52 | this.modules = parsed.argv.remain; 53 | 54 | // Decide task. 55 | this.task = this.inflate; 56 | if (parsed.version) { 57 | this.task = this.version; 58 | } else if (parsed.help || this.modules.length === 0) { 59 | this.task = this.help; 60 | } 61 | }; 62 | 63 | /** 64 | * Selected task _is_ inflate. 65 | * 66 | * @returns {Boolean} `true` if inflate task 67 | */ 68 | Task.prototype.isInflate = function () { 69 | return this.task === this.inflate; 70 | }; 71 | 72 | /** 73 | * Help. 74 | * 75 | * ```sh 76 | * $