├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── RELEASE-CHECKLIST.md ├── TODO.md ├── bin └── headstart.js ├── gulpfile.js ├── lib └── hook.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | *node_modules* 2 | *.DS_Store* 3 | -------------------------------------------------------------------------------- /.npmignore : -------------------------------------------------------------------------------- 1 | .travis.yml 2 | TODO.md 3 | RELEASE-CHECKLIST.md -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.11" 5 | script: ./bin/headstart.js 6 | branches: 7 | only: 8 | - master 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Florian Vanthuyne 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Headstart](website-url), an automated front-end setup 2 | 3 | > Headstart is an all-in-one task runner that frees front-end developers of the little worries that come along with modern web development. If you ever wanted to use tools like [Grunt](http://gruntjs.com/) or [Gulp](http://gulpjs.com/), but found the configuration too troublesome, you will probably like this pre-configured setup better. 4 | 5 | [![NPM version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] [![Gitter][gitter-image]][gitter-url] 6 | 7 | ## Documentation 8 | 9 | - [Getting started][getting-started-url] 10 | - [Base Setup][base-setup-url] 11 | - [Upgrading][ugrading-url] 12 | 13 | ## ♥ Feedback 14 | 15 | What did you like? What didn't you like? Did you get stuck somewhere? Where the docs easy to follow, or did you give up at a certain point? 16 | 17 | This is a one-man project, so some approaches might be personated. Nevertheless, Headstart is meant to be used by other people as well, so your feedback is very valuable! 18 | 19 | [Mail me anything at all](mailto:hello@flovan.me) or [add an issue][issues-url]. 20 | 21 | ## Updates 22 | 23 | For all updates, follow [@headstartio][twitter-url] on Twitter. 24 | Changes can be found on [the changelog page][changelog-url]. 25 | 26 | [website-url]: http://headstart.io 27 | [getting-started-url]: http://headstart.io/installation 28 | [base-setup-url]: http://headstart.io/base-setup 29 | [changelog-url]: http://www.headstart.io/changelog 30 | [ugrading-url]: http://headstart.io/upgrading-guide 31 | [twitter-url]: https://twitter.com/headstartio 32 | [issues-url]: https://github.com/flovan/headstart/issues 33 | [npm-url]: https://npmjs.org/package/headstart 34 | [npm-image]: https://badge.fury.io/js/headstart.svg 35 | [travis-url]: https://travis-ci.org/flovan/headstart 36 | [travis-image]: https://travis-ci.org/flovan/headstart.svg 37 | [downloads-url]: https://github.com/flovan/headstart 38 | [downloads-image]: http://img.shields.io/npm/dm/headstart.svg 39 | [david-url]: https://david-dm.org/flovan/headstart 40 | [david-image]: https://david-dm.org/flovan/headstart.png?theme=shields.io 41 | [gitter-url]: https://gitter.im/flovan/headstart?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 42 | [gitter-image]: https://badges.gitter.im/Join%20Chat.svg 43 | -------------------------------------------------------------------------------- /RELEASE-CHECKLIST.md: -------------------------------------------------------------------------------- 1 | ## Release checklist 2 | 3 | A list of things that need to work for each and every release. 4 | 5 | ### Initialising 6 | 7 | - [ ] Boilerplate files have been updated and packaged into a release 8 | - [ ] The latest boilerplate release can be scaffolded through `hs init` *and* `headstart init` 9 | - [ ] Files can be served through the init 10 | - [ ] A browser can be opened from the init 11 | - [ ] An editor can be opened from the init 12 | 13 | ### CLI testing 14 | 15 | - [ ] A build is successfull through `hs build` 16 | - [ ] A production build is success through `hs build --p` *and* `hs build --production` 17 | - [ ] Files can be served in development through `hs build --s` *and* `hs build --serve` 18 | - [ ] Files can be served in production through `hs build --s --p` *and* `hs build --serve --production` 19 | - [ ] A browser can be opened through `hs build --s --o` *and* `hs build --serve --open` 20 | - [ ] A browser can not be opened without `--s` 21 | - [ ] An editor can be opened through `hs build --e` *and* `hs build --edit` *and* `hs build --s --e` *and* `hs build --s --edit` 22 | - [ ] A tunnel can be initiated through `hs build --s --t` *and* `hs build --s --tunnel` 23 | - [ ] A custom tunnel can be initiated through `hs build --s --t=bla` *and* `hs build --s --tunnel=bla` and returns `bla.localtunnel.me` (or something) when available 24 | - [ ] Google PSI can be initiated through `hs build --s --t --psi` 25 | - [ ] The "mobile" PSI strategy can be set through `hs build --s --t --psi --strategy=mobile` 26 | - [ ] Google PSI can not be initiated without both `--s` and `--t` 27 | 28 | ### Development 29 | 30 | - [ ] JS files get injected 31 | - [ ] Changes to JS files reload the page 32 | - [ ] Added/deleted JS files reload the page and update the injected files 33 | - [ ] CSS files get injected 34 | - [ ] Changes to CSS files update the page 35 | - [ ] Added/deleted CSS files reload the page and update the injected files 36 | - [ ] Images get copied over 37 | - [ ] Changes to images trigger a reload 38 | 39 | ### Production 40 | 41 | - [ ] A `.favicon` is generated in the root 42 | - [ ] A `.htaccess` is generated in the root 43 | - [ ] All `./misc` files are copied over to the root -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ### TODO's: 2 | 3 | - [ ] Fix crash when missing `// requires` file is found 4 | - [ ] Adding new view js files doesn't seem to add them to gaze 5 | 6 | ### A list of things to explore: 7 | 8 | - [ ] Think about fixed size columns for eg ads 9 | - [ ] Try to have revisioned images, even though updating references in other files will be hard (https://github.com/smysnk/gulp-rev-all, maybe https://www.npmjs.org/package/gulp-hash/ + https://www.npmjs.org/package/gulp-hash-references/) 10 | - [ ] Watch changes on config and folders like fonts/video, and rebuild (https://github.com/leny/gulp-supervisor & https://github.com/JacksonGariety/gulp-nodemon & https://www.npmjs.org/package/keepup/) 11 | - [ ] See if Commander is a better alternative to LiftOff (https://github.com/visionmedia/commander.js) 12 | - [ ] Try replacing gulp-inject with gulp-include-source (https://www.npmjs.org/package/gulp-include-source/) 13 | - [ ] See if scripts tasks can be made faster with gulp-remember (https://github.com/ahaurw01/gulp-remember) 14 | - [ ] Maybe do something with icon fonts from SVG files (https://www.npmjs.org/package/gulp-fontcustom/ or https://github.com/nfroidure/gulp-iconfont or https://github.com/nfroidure/gulp-svgicons2svgfont) 15 | - [ ] Make it possible to use LESS (https://www.npmjs.org/package/gulp-less/) 16 | - [ ] Make it possible to use Stylus (https://www.npmjs.org/package/gulp-stylus/) 17 | - [ ] Maybe replace some CSS processing modules with gulp-pleeease (https://www.npmjs.org/package/gulp-pleeease/) 18 | - [ ] Try having critical css inlined (http://css-tricks.com/authoring-critical-fold-css/ & https://github.com/pocketjoso/penthouse/#as-a-node-module OR https://github.com/filamentgroup/criticalcss) 19 | - [ ] Use SassDoc (https://github.com/SassDoc/gulp-sassdoc) 20 | - [ ] Split up gulpfile into task files (https://github.com/whitneyit/gulp-taskify or https://www.npmjs.org/package/gulp-hub/) 21 | - [ ] Remove deleted / renamed files from export folder with gulp-sync-files (https://www.npmjs.org/package/gulp-sync-files/) 22 | - [ ] Replace Ender by Cash when it gets out of alpha (https://github.com/kenwheeler/cash) 23 | - [ ] Check out HeadJS (http://headjs.com) 24 | - [ ] Implement gulp-foreach (https://www.npmjs.org/package/gulp-foreach/) 25 | - [ ] Make sure a key can be used with PSI (without any uncaught TypeError) 26 | - [ ] Simpler watch setup > https://gist.github.com/Snugug/2dc9ff47ce4b4acb28f6 27 | 28 | 29 | ### Take a look at these plugins also 30 | 31 | - https://www.npmjs.org/package/del/ instead of gulp-rimraf (will prolly have to use https://www.npmjs.org/package/gulp-filenames) 32 | - CDN Solution https://www.npmjs.org/package/gulp-cdnizer/ 33 | - https://www.npmjs.org/package/gulp-log-capture 34 | - https://www.npmjs.org/package/gulp-static-handlebars 35 | - https://www.npmjs.org/package/gulp-if-else 36 | - https://www.npmjs.org/package/gulp-headerfooter 37 | - https://www.npmjs.org/package/gulp-htmlrefs OR https://www.npmjs.org/package/gulp-rev-replace 38 | - https://www.npmjs.org/package/favicons 39 | 40 | ### Done 41 | 42 | - [x] Fix reloading when a layout/partial changes 43 | - [x] Find a better/smarter templating system https://www.npmjs.org/package/gulp-file-insert/ 44 | - [x] Find better (smaller, dep-less) way of stripping comments from --production HTML 45 | - [x] Allow custom repo's to be set for scaffolding 46 | - [x] Fix sass and htmlmin error crashes 47 | - [x] Add W3C validation option to config (https://www.npmjs.org/package/gulp-w3cjs/) 48 | - [x] Try out revisions to leverage cache control (https://github.com/sindresorhus/gulp-rev) 49 | - [x] Generate a cache manifeset for --production (https://www.npmjs.org/package/gulp-manifest/ + http://diveintohtml5.info/offline.html) 50 | - [x] Replace livereloading with browser-sync (https://github.com/shakyShane/browser-sync through http://shakyshane.com/gulpjs-sass-browsersync-ftw/) 51 | - [x] ^ Fix logging by gulp-connect (~~muting https://www.npmjs.org/package/mute-stream~~) 52 | - [x] Auto-check for updates (~~http://stackoverflow.com/questions/20686244/install-programmatically-a-npm-package-providing-its-version and http://stackoverflow.com/questions/11949419/nodejs-npm-show-latest-version-of-a-module~~ https://github.com/yeoman/update-notifier) 53 | - [x] Make box-sizing work through inherit (http://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice) 54 | - [x] Find a less crude way of muting module output through gulp-util 55 | - [x] Put Liftoff logo on website (https://www.npmjs.org/package/liftoff) 56 | - [x] Make HTML minifier options configurable through `config.json` 57 | - [x] Properly test gulp-combine-media-queries (Result: saves a few KB's, enabling by default in boilerplate v1.1.1) 58 | - [ ] ~~Use a different minifier (https://www.npmjs.org/package/gulp-compressor/ or https://www.npmjs.org/package/gulp-minifier/)~~ 59 | - [ ] ~~Check out csscss (https://www.npmjs.org/package/gulp-csscss/)~~ 60 | - [x] Properly test Uncss 61 | - [x] Pass gulp-ruby-sass errors instead of uncss notification 62 | - [x] Remove ender and underscore map from underscore.js 63 | - [x] Turn aliasing into state, and extend to doc/button/form module 64 | - [x] Fix url in update notice 65 | - [x] Fix jshint logs appearing in the middle of the progressbar 66 | - [x] Dry out modules 67 | - [x] Drop `open` in favour of opening the browser through `browsersync` 68 | - [x] A build starting with a Sass error will result in pages without (a) css file(s) 69 | - [x] Disable (and warn about disabling of) w3c validation when not ".html" 70 | - [x] Re-add `open` because openEditor won't work otherwise.. 71 | -------------------------------------------------------------------------------- /bin/headstart.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | // To see an extended Error Stack Trace, uncomment 5 | // Error.stackTraceLimit = 9000; 6 | 7 | // REQUIRES ------------------------------------------------------------------- 8 | // 9 | // Note: Gulp related requires are made further down to speed up the first 10 | // part of this script 11 | 12 | var 13 | path = require('path'), 14 | fs = require('fs'), 15 | chalk = require('chalk'), 16 | _ = require('lodash'), 17 | 18 | pkg = require('../package.json'), 19 | Liftoff = require('liftoff'), 20 | updateNotifier = require('update-notifier'), 21 | argv = require('minimist')(process.argv.slice(2)), 22 | gulp, 23 | gulpFile 24 | ; 25 | 26 | // CLI CONFIGURATION ---------------------------------------------------------- 27 | // 28 | 29 | var cli = new Liftoff({ 30 | name: 'headstart' 31 | }); 32 | 33 | // CHECK FOR UPDATES ---------------------------------------------------------- 34 | // 35 | 36 | var notifier = updateNotifier({ 37 | packageName: pkg.name, 38 | packageVersion: pkg.version 39 | }); 40 | 41 | if (notifier.update) { 42 | // Inlined from the update-notifier source for more control 43 | console.log( 44 | chalk.yellow('\n\n┌──────────────────────────────────────────┐\n|') + 45 | chalk.white(' Update available: ') + 46 | chalk.green(notifier.update.latest) + 47 | chalk.grey(' (current: ' + notifier.update.current + ') ') + 48 | chalk.yellow('|\n|') + 49 | chalk.white(' Instructions can be found on: ') + 50 | chalk.yellow('|\n|') + 51 | chalk.magenta(' http://headstart.io/upgrading-guide ') + 52 | chalk.yellow('|\n') + 53 | chalk.yellow('└──────────────────────────────────────────┘\n') 54 | ); 55 | } 56 | 57 | // LAUNCH CLI ----------------------------------------------------------------- 58 | // 59 | 60 | cli.launch({}, launcher); 61 | 62 | function launcher (env) { 63 | 64 | var 65 | versionFlag = argv.v || argv.version, 66 | infoFlag = argv.i || argv.info || argv.h || argv.help, 67 | 68 | allowedTasks = ['init', 'build'], 69 | task = argv._, 70 | numTasks = task.length 71 | ; 72 | 73 | // Check for version flag 74 | if (versionFlag) { 75 | logHeader(pkg); 76 | process.exit(0); 77 | } 78 | 79 | // Log info if no tasks are passed in 80 | if (!numTasks) { 81 | logInfo(pkg); 82 | process.exit(0); 83 | } 84 | 85 | // Warn if more than one tasks has been passed in 86 | if (numTasks > 1) { 87 | console.log(chalk.red('\nOnly one task can be provided. Aborting.\n')); 88 | logTasks(); 89 | process.exit(0); 90 | } 91 | 92 | // We are now sure we only have 1 task 93 | task = task[0]; 94 | 95 | // Print info if needed 96 | if(infoFlag) { 97 | logInfo(pkg); 98 | process.exit(0); 99 | } 100 | 101 | // Check if task is valid 102 | if (_.indexOf(allowedTasks, task) < 0) { 103 | console.log(chalk.red('\nThe provided task "' + task + '" was not recognized. Aborting.\n')); 104 | logTasks(); 105 | process.exit(0); 106 | } 107 | 108 | // Change directory to where Headstart was called from 109 | if (process.cwd() !== env.cwd) { 110 | process.chdir(env.cwd); 111 | console.log(chalk.cyan('Working directory changed to', chalk.magenta(env.cwd))); 112 | } 113 | 114 | // Require gulp assets 115 | gulp = require('gulp'); 116 | gulpFile = require(path.join(path.dirname(fs.realpathSync(__filename)), '../gulpfile.js')); 117 | 118 | // Start the task through Gulp 119 | process.nextTick(function () { 120 | gulp.start.apply(gulp, [task]); 121 | }); 122 | } 123 | 124 | // HELPER FUNCTIONS ----------------------------------------------------------- 125 | // 126 | 127 | function logInfo (pkg) { 128 | logHeader(pkg); 129 | logTasks(); 130 | } 131 | 132 | function logHeader (pkg) { 133 | console.log( 134 | chalk.cyan( 135 | '\n' + 136 | '| | | |\n' + 137 | '|---.,---.,---.,---|,---.|--- ,---.,---.|---\n' + 138 | '| ||---\',---|| |`---.| ,---|| |\n' + 139 | '` \'`---\'`---^`---\'`---\'`---\'`---^` `---\'\n\n' 140 | ) + 141 | chalk.cyan.inverse('➳ http://headstart.io') + 142 | ' ' + 143 | chalk.yellow.inverse('v' + pkg.version) + '\n' 144 | ); 145 | } 146 | 147 | function logTasks () { 148 | console.log( 149 | chalk.grey.underline('To start a new project, run:\n\n') + 150 | chalk.magenta('headstart init [flags]') + 151 | chalk.grey(' or ') + 152 | chalk.magenta('hs init [flags]\n\n') + 153 | chalk.white('--base ') + 154 | chalk.grey('\t\tUse a custom boilerplate repo, eg. user/repo#branch\n') 155 | ); 156 | console.log( 157 | chalk.grey.underline('To build the project, run:\n\n') + 158 | chalk.magenta('headstart build [flags]') + 159 | chalk.grey(' or ') + 160 | chalk.magenta('hs build [flags]\n\n') + 161 | chalk.white('--s, --serve') + 162 | chalk.grey('\t\tServe the files on a static address\n') + 163 | chalk.white('--o, --open') + 164 | chalk.grey('\t\tOpen up a browser for you (default Google Chrome)\n') + 165 | chalk.white('--e, --edit') + 166 | chalk.grey('\t\tOpen the files in your editor (default Sublime Text)\n') + 167 | chalk.white('--p, --production') + 168 | chalk.grey('\tMake a production ready build\n') + 169 | chalk.white('--t, --tunnel') + 170 | chalk.grey('\t\tTunnel your served files to the web (requires --serve)\n') + 171 | chalk.white('--psi') + 172 | chalk.grey('\t\t\tRun PageSpeed Insights (requires --serve and --tunnel)\n') + 173 | //chalk.white('--key ') + 174 | //chalk.grey('\t\tOptional, an API key for PSI\n') + 175 | chalk.white('--strategy ') + 176 | chalk.grey('\tRun PSI in either "desktop" (default) or "mobile" mode\n\n') + 177 | chalk.white('--verbose') + 178 | chalk.grey('\t\tOutput extra information while building\n') 179 | ); 180 | console.log( 181 | chalk.grey.underline('For information, run:\n\n') + 182 | chalk.magenta('headstart [flags]') + 183 | chalk.grey(' or ') + 184 | chalk.magenta('hs [flags]\n\n') + 185 | chalk.white('--i, --info,\n--h, --help') + 186 | chalk.grey('\t\tPrint out this message\n') + 187 | chalk.white('--v, --version') + 188 | chalk.grey('\t\tPrint out version\n') 189 | ); 190 | } 191 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /*global require, process, __dirname*/ 2 | 3 | 'use strict'; 4 | 5 | // REQUIRES ------------------------------------------------------------------- 6 | 7 | var 8 | path = require('path'), 9 | globule = require('globule'), 10 | http = require ('http'), 11 | fs = require('fs'), 12 | ncp = require('ncp').ncp, 13 | chalk = require('chalk'), 14 | _ = require('lodash'), 15 | prompt = require('inquirer').prompt, 16 | sequence = require('run-sequence'), 17 | ProgressBar = require('progress'), 18 | stylish = require('jshint-stylish'), 19 | open = require('open'), 20 | ghdownload = require('github-download'), 21 | browserSync = require('browser-sync'), 22 | psi = require('psi'), 23 | gulp = require('gulp'), 24 | plugins = require('gulp-load-plugins')({ 25 | config: path.join(__dirname, 'package.json') 26 | }), 27 | flags = require('minimist')(process.argv.slice(2)) 28 | ; 29 | 30 | // VARS ----------------------------------------------------------------------- 31 | 32 | var 33 | gitConfig = { 34 | user: 'flovan', 35 | repo: 'headstart-boilerplate', 36 | ref: '1.2.1' 37 | }, 38 | cwd = process.cwd(), 39 | tmpFolder = '.tmp', 40 | assetsFolder = 'assets', 41 | stdoutBuffer = [], 42 | lrStarted = false, 43 | htmlminOptions = { 44 | removeComments: true, 45 | collapseWhitespace: true, 46 | collapseBooleanAttributes: true, 47 | removeAttributeQuotes: true, 48 | useShortDoctype: true, 49 | removeScriptTypeAttributes: true, 50 | removeStyleLinkTypeAttributes: true, 51 | minifyJS: true, 52 | minifyCSS: true 53 | }, 54 | isProduction = ( flags.production || flags.p ) || false, 55 | isServe = ( flags.serve || flags.s ) || false, 56 | isOpen = ( flags.open || flags.o ) || false, 57 | isEdit = ( flags.edit || flags.e ) || false, 58 | isVerbose = flags.verbose || false, 59 | isTunnel = ( flags.tunnel || flags.t ) || false, 60 | tunnelUrl = null, 61 | isPSI = flags.psi || false, 62 | config = null, 63 | bar = null 64 | ; 65 | 66 | // LOGGING -------------------------------------------------------------------- 67 | // 68 | 69 | if (!isVerbose) { 70 | // To get a better grip on logging by either gulp-util, console.log or direct 71 | // writing to process.stdout, a hook is applied to stdout when not running 72 | // in --vebose mode 73 | require('./lib/hook.js')(process.stdout).hook('write', function (msg, encoding, fd, write) { 74 | 75 | // Validate message 76 | msg = validForWrite(msg); 77 | 78 | // If the message is not suited for output, block it 79 | if (!msg) { 80 | return; 81 | } 82 | 83 | if (msg.length === 1) return; 84 | 85 | // There is no progress bar, so just write 86 | if (_.isNull(bar)) { 87 | write(msg); 88 | return; 89 | } 90 | 91 | // There is a progress bar, but it hasn't completed yet, so buffer 92 | if (!bar.complete) { 93 | stdoutBuffer.push(msg); 94 | return; 95 | } 96 | 97 | // There is a buffer, prepend a newline to the array 98 | if(stdoutBuffer.length) { 99 | stdoutBuffer.unshift('\n'); 100 | } 101 | 102 | // Write out the buffer untill its empty 103 | while (stdoutBuffer.length) { 104 | write(stdoutBuffer.shift()); 105 | } 106 | 107 | // Finally, just write out 108 | write(msg); 109 | }); 110 | 111 | // Map console.warn to console.log to make sure gulp-sassgraph errors 112 | // get validated by the code above 113 | /*console.warn = console.log; /*function () { 114 | 115 | var args = Array.prototype.slice.call(arguments); 116 | console.error('passing this to console.log: ', args); 117 | console.log.apply(console, args); 118 | }*/ 119 | } 120 | 121 | // INIT ----------------------------------------------------------------------- 122 | // 123 | 124 | gulp.task('init', function (cb) { 125 | 126 | // Get all files in working directory 127 | // Exclude . files (such as .DS_Store on OS X) 128 | var cwdFiles = _.remove(fs.readdirSync(cwd), function (file) { 129 | 130 | return file.substring(0,1) !== '.'; 131 | }); 132 | 133 | // If there are any files 134 | if (cwdFiles.length > 0) { 135 | 136 | // Make sure the user knows what is about to happen 137 | console.log(chalk.yellow.inverse('\nThe current directory is not empty!')); 138 | prompt({ 139 | type: 'confirm', 140 | message: 'Initializing will empty the current directory. Continue?', 141 | name: 'override', 142 | default: false 143 | }, function (answer) { 144 | 145 | if (answer.override) { 146 | // Make really really sure that the user wants this 147 | prompt({ 148 | type: 'confirm', 149 | message: 'Removed files are gone forever. Continue?', 150 | name: 'overridconfirm', 151 | default: false 152 | }, function (answer) { 153 | 154 | if (answer.overridconfirm) { 155 | // Clean up directory, then start downloading 156 | console.log(chalk.grey('\nEmptying current directory')); 157 | sequence('clean-tmp', 'clean-cwd', downloadBoilerplateFiles); 158 | } 159 | // User is unsure, quit process 160 | else process.exit(0); 161 | }); 162 | } 163 | // User is unsure, quit process 164 | else process.exit(0); 165 | }); 166 | } 167 | // No files, start downloading 168 | else { 169 | console.log('\n'); 170 | downloadBoilerplateFiles(); 171 | } 172 | 173 | cb(null); 174 | }); 175 | 176 | // BUILD ---------------------------------------------------------------------- 177 | // 178 | 179 | gulp.task('build', function (cb) { 180 | 181 | // Load the config.json file 182 | console.log(chalk.grey('\nLoading config.json...')); 183 | fs.readFile('config.json', 'utf8', function (err, data) { 184 | 185 | if (err) { 186 | console.log(chalk.red('✘ Cannot find config.json. Have you initiated Headstart through `headstart init?'), err); 187 | process.exit(0); 188 | } 189 | 190 | // Try parsing the config data as JSON 191 | try { 192 | config = JSON.parse(data); 193 | } catch (err) { 194 | console.log(chalk.red('✘ The config.json file is not valid json. Aborting.'), err); 195 | process.exit(0); 196 | } 197 | 198 | // Allow customization of gulp-htmlmin through `config.json` 199 | if (!_.isNull(config.htmlminOptions)) { 200 | htmlminOptions = _.assign({}, htmlminOptions, config.htmlminOptions); 201 | } 202 | 203 | assetsFolder = config.assetsFolder || assetsFolder; 204 | 205 | // Instantiate a progressbar when not in verbose mode 206 | if (!isVerbose) { 207 | bar = new ProgressBar(chalk.grey('Building ' + (isProduction ? 'production' : 'development') + ' version [:bar] :percent done'), { 208 | complete: '#', 209 | incomplete: '-', 210 | total: 8 211 | }); 212 | updateBar(); 213 | } else { 214 | console.log(chalk.grey('Building ' + (isProduction ? 'production' : 'development') + ' version...')); 215 | } 216 | 217 | // Run build tasks 218 | // Serve files if Headstart was run with the --serve flag 219 | sequence( 220 | 'clean-export', 221 | [ 222 | 'sass-main', 223 | 'scripts-main', 224 | 'images', 225 | 'other' 226 | ], 227 | 'templates', 228 | 'manifest', 229 | 'uncss', 230 | function () { 231 | console.log(chalk.green((!isProduction ? '\n' : '') + '✔ Build complete')); 232 | if(isServe) { 233 | gulp.start('server'); 234 | } 235 | cb(null); 236 | } 237 | ); 238 | }); 239 | }); 240 | 241 | // CLEAN ---------------------------------------------------------------------- 242 | // 243 | 244 | gulp.task('clean-export', function (cb) { 245 | 246 | // Remove export folder and files 247 | return gulp.src([ 248 | config.export_templates, 249 | config.export_assets + '/' + assetsFolder 250 | ], {read: false}) 251 | .pipe(plugins.rimraf({force: true})) 252 | .on('end', updateBar) 253 | ; 254 | }); 255 | 256 | gulp.task('clean-cwd', function (cb) { 257 | 258 | // Remove cwd files 259 | return gulp.src(cwd + '/*', {read: false}) 260 | .pipe(plugins.rimraf({force: true})) 261 | ; 262 | }); 263 | 264 | gulp.task('clean-tmp', function (cb) { 265 | 266 | // Remove temp folder 267 | return gulp.src(tmpFolder, {read: false}) 268 | .pipe(plugins.rimraf({force: true})) 269 | ; 270 | }); 271 | 272 | gulp.task('clean-rev', function (cb) { 273 | 274 | verbose(chalk.grey('Running task "clean-rev"')); 275 | 276 | // Clean all revision files but the latest ones 277 | return gulp.src(config.export_assets + '/' + assetsFolder + '/**/*.*', {read: false}) 278 | .pipe(plugins.revOutdated(1)) 279 | .pipe(plugins.rimraf({force: true})) 280 | ; 281 | }); 282 | 283 | 284 | // SASS ----------------------------------------------------------------------- 285 | // 286 | 287 | gulp.task('sass-main', ['sass-ie'], function (cb) { 288 | 289 | // Flag to catch empty streams 290 | // https://github.com/floatdrop/gulp-watch/issues/87 291 | var isEmptyStream = true; 292 | 293 | verbose(chalk.grey('Running task "sass-main"')); 294 | 295 | // Process the .scss files 296 | // While serving, this task opens a continuous watch 297 | return gulp.src([ 298 | assetsFolder + '/sass/*.{scss, sass, css}', 299 | '!' + assetsFolder + '/sass/*ie.{scss, sass, css}' 300 | ]) 301 | .pipe(plugins.plumber()) 302 | .pipe(plugins.rubySass({ style: (isProduction ? 'compressed' : 'nested') })) 303 | .pipe(plugins.if(config.combineMediaQueries, plugins.combineMediaQueries())) 304 | .pipe(plugins.autoprefixer('last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4')) 305 | .pipe(plugins.if(config.revisionCaching, plugins.rev())) 306 | // TODO: When minifyCSS bug is fixed, drop the noAdvanced feature 307 | // (https://github.com/jakubpawlowicz/clean-css/issues/375) 308 | .pipe(plugins.if(isProduction, plugins.minifyCss({ compatibility: 'ie8', noAdvanced: true }))) 309 | .pipe(plugins.if(isProduction, plugins.rename({suffix: '.min'}))) 310 | .pipe(gulp.dest(config.export_assets + '/' + assetsFolder + '/css')) 311 | .on('data', function (cb) { 312 | 313 | // If revisioning is on, run templates again so the refresh contains 314 | // the newer stylesheet 315 | if (lrStarted && config.revisionCaching) { 316 | gulp.start('templates'); 317 | } 318 | 319 | // Continue the stream 320 | this.resume(); 321 | }) 322 | .on('end', updateBar) 323 | .pipe(plugins.if(lrStarted && !config.revisionCaching, browserSync.reload({stream:true}))) 324 | ; 325 | }); 326 | 327 | gulp.task('sass-ie', function (cb) { 328 | 329 | // Flag to catch empty streams 330 | // https://github.com/floatdrop/gulp-watch/issues/87 331 | var isEmptyStream = true; 332 | 333 | verbose(chalk.grey('Running task "sass-ie"')); 334 | 335 | // Process the .scss files 336 | // While serving, this task opens a continuous watch 337 | return gulp.src([ 338 | assetsFolder + '/sass/*ie.{scss, sass, css}' 339 | ]) 340 | .pipe(plugins.plumber()) 341 | .pipe(plugins.rubySass({ style: (isProduction ? 'compressed' : 'nested') })) 342 | .pipe(plugins.rename({suffix: '.min'})) 343 | .pipe(gulp.dest(config.export_assets + '/' + assetsFolder + '/css')) 344 | ; 345 | }); 346 | 347 | // UNCSS ---------------------------------------------------------------------- 348 | // 349 | // Clean up unused CSS styles 350 | 351 | gulp.task('uncss', function (cb) { 352 | 353 | // Quit this task if not configured through config 354 | if (!isProduction || !config.useUncss) { 355 | updateBar(); 356 | cb(null); 357 | return; 358 | } 359 | 360 | verbose(chalk.grey('Running task "uncss-main"')); 361 | 362 | // Grab all templates / partials / layout parts / etc 363 | var templates = globule.find([config.export_templates + '/**/*.*']); 364 | 365 | // Grab all css files and run them through Uncss, then overwrite 366 | // the originals with the new ones 367 | return gulp.src(config.export_assets + '/' + assetsFolder + '/css/*.css') 368 | .pipe(plugins.bytediff.start()) 369 | .pipe(plugins.uncss({ 370 | html: templates || [], 371 | ignore: config.uncssIgnore || [] 372 | })) 373 | .pipe(plugins.bytediff.stop(function (data) { 374 | updateBar(); 375 | 376 | data.percent = Math.round(data.percent*100); 377 | data.savings = Math.round(data.savings/1024); 378 | 379 | return chalk.grey('' + data.fileName + ' is now ') + chalk.green(data.percent + '% ' + (data.savings > 0 ? 'smaller' : 'larger')) + chalk.grey(' (saved ' + data.savings + 'KB)'); 380 | })) 381 | .pipe(gulp.dest(config.export_assets + '/' + assetsFolder + '/css')) 382 | ; 383 | }); 384 | 385 | // SCRIPTS -------------------------------------------------------------------- 386 | // 387 | 388 | // JSHint options: http://www.jshint.com/docs/options/ 389 | gulp.task('hint-scripts', function (cb) { 390 | 391 | // Quit this task if hinting isn't turned on 392 | if (!config.hint) { 393 | cb(null); 394 | return; 395 | } 396 | 397 | verbose(chalk.grey('Running task "hint-scripts"')); 398 | 399 | // Hint all non-lib js files and exclude _ prefixed files 400 | return gulp.src([ 401 | assetsFolder + '/js/*.js', 402 | assetsFolder + '/js/core/*.js', 403 | '!_*.js' 404 | ]) 405 | .pipe(plugins.plumber()) 406 | .pipe(plugins.jshint('.jshintrc')) 407 | .pipe(plugins.jshint.reporter(stylish)) 408 | ; 409 | }); 410 | 411 | gulp.task('scripts-main', ['hint-scripts', 'scripts-view', 'scripts-ie'], function () { 412 | 413 | var files = [ 414 | assetsFolder + '/js/libs/jquery*.js', 415 | assetsFolder + '/js/libs/ender*.js', 416 | 417 | (isProduction ? '!' : '') + assetsFolder + '/js/libs/dev/*.js', 418 | 419 | assetsFolder + '/js/libs/**/*.js', 420 | // TODO: remove later 421 | assetsFolder + '/js/core/**/*.js', 422 | // 423 | assetsFolder + '/js/*.js', 424 | '!' + assetsFolder + '/js/view-*.js', 425 | '!**/_*.js' 426 | ]; 427 | 428 | verbose(chalk.grey('Running task "scripts-main"')); 429 | 430 | if (isProduction) { 431 | var numFiles = globule.find(files).length; 432 | console.log(chalk.green('✄ Concatenated ' + numFiles + ' JS files')); 433 | } 434 | 435 | // Process .js files 436 | // Files are ordered for dependency sake 437 | return gulp.src(files, {base: '' + assetsFolder + '/js'}) 438 | .pipe(plugins.plumber()) 439 | .pipe(plugins.deporder()) 440 | .pipe(plugins.if(isProduction, plugins.stripDebug())) 441 | .pipe(plugins.if(isProduction, plugins.concat('core-libs.js'))) 442 | .pipe(plugins.if(config.revisionCaching, plugins.rev())) 443 | .pipe(plugins.if(isProduction, plugins.rename({extname: '.min.js'}))) 444 | .pipe(plugins.if(isProduction, plugins.uglify())) 445 | .pipe(gulp.dest(config.export_assets + '/' + assetsFolder + '/js')) 446 | .on('end', updateBar) 447 | ; 448 | }); 449 | 450 | gulp.task('scripts-view', function (cb) { 451 | 452 | verbose(chalk.grey('Running task "scripts-view"')); 453 | 454 | return gulp.src(assetsFolder + '/js/view-*.js') 455 | .pipe(plugins.plumber()) 456 | .pipe(plugins.if(config.revisionCaching, plugins.rev())) 457 | .pipe(plugins.if(isProduction, plugins.rename({suffix: '.min'}))) 458 | .pipe(plugins.if(isProduction, plugins.stripDebug())) 459 | .pipe(plugins.if(isProduction, plugins.uglify())) 460 | .pipe(gulp.dest(config.export_assets + '/' + assetsFolder + '/js')) 461 | ; 462 | }); 463 | 464 | gulp.task('scripts-ie', function (cb) { 465 | 466 | verbose(chalk.grey('Running task "scripts-ie"')); 467 | 468 | // Process .js files 469 | // Files are ordered for dependency sake 470 | gulp.src([ 471 | assetsFolder + '/js/ie/head/**/*.js', 472 | '!**/_*.js' 473 | ]) 474 | .pipe(plugins.plumber()) 475 | .pipe(plugins.deporder()) 476 | .pipe(plugins.concat('ie-head.js')) 477 | .pipe(plugins.if(isProduction, plugins.stripDebug())) 478 | .pipe(plugins.rename({extname: '.min.js'})) 479 | .pipe(plugins.uglify()) 480 | .pipe(gulp.dest(config.export_assets + '/' + assetsFolder + '/js')); 481 | 482 | gulp.src([ 483 | assetsFolder + '/js/ie/body/**/*.js', 484 | '!**/_*.js' 485 | ]) 486 | .pipe(plugins.plumber()) 487 | .pipe(plugins.deporder()) 488 | .pipe(plugins.concat('ie-body.js')) 489 | .pipe(plugins.if(isProduction, plugins.stripDebug())) 490 | .pipe(plugins.rename({extname: '.min.js'})) 491 | .pipe(plugins.uglify()) 492 | .pipe(gulp.dest(config.export_assets + '/' + assetsFolder + '/js')); 493 | 494 | cb(null); 495 | }); 496 | 497 | // IMAGES --------------------------------------------------------------------- 498 | // 499 | 500 | gulp.task('images', function (cb) { 501 | 502 | verbose(chalk.grey('Running task "images"')); 503 | 504 | // Make a copy of the favicon.png, and make a .ico version for IE 505 | // Move to root of export folder 506 | gulp.src(assetsFolder + '/images/icons/favicon.png') 507 | .pipe(plugins.rename({extname: '.ico'})) 508 | //.pipe(plugins.if(config.revisionCaching, plugins.rev())) 509 | .pipe(gulp.dest(config.export_misc)) 510 | ; 511 | 512 | // Grab all image files, filter out the new ones and copy over 513 | // In --production mode, optimize them first 514 | return gulp.src([ 515 | assetsFolder + '/images/**/*', 516 | '!_*' 517 | ]) 518 | .pipe(plugins.plumber()) 519 | .pipe(plugins.newer(config.export_assets + '/' + assetsFolder + '/images')) 520 | .pipe(plugins.if(isProduction, plugins.imagemin({ optimizationLevel: 3, progressive: true, interlaced: true }))) 521 | //.pipe(plugins.if(config.revisionCaching, plugins.rev())) 522 | .pipe(gulp.dest(config.export_assets + '/' + assetsFolder + '/images')) 523 | .pipe(plugins.if(lrStarted, browserSync.reload({stream:true}))) 524 | ; 525 | }); 526 | 527 | // OTHER ---------------------------------------------------------------------- 528 | // 529 | 530 | gulp.task('other', ['misc'], function (cb) { 531 | 532 | verbose(chalk.grey('Running task "other"')); 533 | 534 | // Make sure other files and folders are copied over 535 | // eg. fonts, videos, ... 536 | return gulp.src([ 537 | assetsFolder + '/**/*', 538 | '!' + assetsFolder + '/sass', 539 | '!' + assetsFolder + '/sass/**/*', 540 | '!' + assetsFolder + '/js/**/*', 541 | '!' + assetsFolder + '/images/**/*', 542 | '!_*' 543 | ]) 544 | .pipe(plugins.plumber()) 545 | //.pipe(plugins.if(config.revisionCaching, plugins.rev())) 546 | .pipe(gulp.dest(config.export_assets + '/' + assetsFolder)) 547 | .on('end', updateBar) 548 | ; 549 | }); 550 | 551 | // MISC ----------------------------------------------------------------------- 552 | // 553 | 554 | gulp.task('misc', function (cb) { 555 | 556 | // In --production mode, copy over all the other stuff 557 | if (isProduction) { 558 | verbose(chalk.grey('Running task "misc"')); 559 | 560 | // Make a functional version of the htaccess.txt 561 | gulp.src('misc/htaccess.txt') 562 | .pipe(plugins.rename('.htaccess')) 563 | .pipe(gulp.dest(config.export_misc)) 564 | ; 565 | 566 | gulp.src(['misc/*', '!misc/htaccess.txt', '!_*']) 567 | .pipe(gulp.dest(config.export_misc)) 568 | ; 569 | } 570 | 571 | cb(null); 572 | }); 573 | 574 | // TEMPLATES ------------------------------------------------------------------ 575 | // 576 | 577 | gulp.task('templates', ['clean-rev'], function (cb) { 578 | 579 | verbose(chalk.grey('Running task "templates"')); 580 | 581 | // If assebly is off, export all folders and files 582 | if (!config.assemble_templates) { 583 | gulp.src(['templates/**/*', '!templates/*.*', '!_*']) 584 | .pipe(gulp.dest(config.export_templates)); 585 | } 586 | 587 | // Find number of "root" templates to parse and keep count 588 | var numTemplates = globule.find(['templates/*.*', '!_*']).length, 589 | count = 0, 590 | unvalidatedFiles = []; 591 | 592 | // Go over all root template files 593 | gulp.src(['templates/*.*', '!_*']) 594 | .pipe(plugins.tap(function (htmlFile) { 595 | 596 | var 597 | // Extract bits from filename 598 | baseName = path.basename(htmlFile.path), 599 | nameParts = baseName.split('.'), 600 | ext = _.without(nameParts, _.first(nameParts)).join('.'), 601 | viewBaseName = _.last(nameParts[0].split('view-')), 602 | // Make sure Windows paths work down below 603 | cwdParts = cwd.replace(/\\/g, '/').split('/'), 604 | 605 | // Make a collection of file globs 606 | // Production will get 1 file only 607 | // Development gets raw base files 608 | injectItems = isProduction ? 609 | [ 610 | config.export_assets + '/' + assetsFolder + '/js/core-libs*.min.js', 611 | config.export_assets + '/' + assetsFolder + '/js/view-' + viewBaseName + '*.min.js' 612 | ] 613 | : 614 | [ 615 | config.export_assets + '/' + assetsFolder + '/js/libs/jquery*.js', 616 | config.export_assets + '/' + assetsFolder + '/js/libs/ender*.js', 617 | 618 | (isProduction ? '!' : '') + config.export_assets + '/' + assetsFolder + '/js/libs/dev/*.js', 619 | 620 | config.export_assets + '/' + assetsFolder + '/js/libs/*.js', 621 | config.export_assets + '/' + assetsFolder + '/js/core/*.js', 622 | config.export_assets + '/' + assetsFolder + '/js/**/*.js', 623 | 624 | '!' + config.export_assets + '/' + assetsFolder + '/**/_*.js', 625 | '!' + config.export_assets + '/' + assetsFolder + '/js/ie*.js' 626 | ] 627 | ; 628 | 629 | // Include the css 630 | injectItems.push(config.export_assets + '/' + assetsFolder + '/css/main*.css'); 631 | injectItems.push(config.export_assets + '/' + assetsFolder + '/css/view-' + viewBaseName + '*.css'); 632 | 633 | // Put items in a stream and order dependencies 634 | injectItems = gulp.src(injectItems) 635 | .pipe(plugins.plumber()) 636 | .pipe(plugins.ignore.include(function (file) { 637 | 638 | var fileBase = path.basename(file.path); 639 | 640 | // Exclude filenames with "view-" not matching the current view 641 | if (fileBase.indexOf('view-') > -1 && fileBase.indexOf('.js') > -1 && fileBase.indexOf(viewBaseName) < 0) { 642 | return false; 643 | } 644 | 645 | // Pass through all the other files 646 | return true; 647 | })) 648 | .pipe(plugins.deporder(baseName)); 649 | 650 | // On the current template 651 | gulp.src('templates/' + baseName) 652 | .pipe(plugins.plumber()) 653 | // Piping plugins.newer() blocks refreshes on partials and layout parts :( 654 | //.pipe(plugins.newer(config.export_templates + '/' + baseName)) 655 | .pipe(plugins.if(config.assemble_templates, plugins.compileHandlebars({ 656 | templateName: baseName 657 | }, { 658 | batch: ['templates/layout', 'templates/partials'], 659 | helpers: { 660 | equal: function (v1, v2, options) { 661 | return (v1 == v2) ? options.fn(this) : options.inverse(this); 662 | } 663 | } 664 | }))) 665 | .pipe(plugins.inject(injectItems, { 666 | ignorePath: [ 667 | _.without(cwdParts, cwdParts.splice(-1)[0]).join('/') 668 | ].concat(config.export_assets.split('/')), 669 | addRootSlash: false, 670 | addPrefix: config.template_asset_prefix || '' 671 | })) 672 | .pipe(plugins.if(config.w3c && ext === 'html', plugins.w3cjs({ 673 | doctype: 'HTML5', 674 | charset: 'utf-8' 675 | }))) 676 | .pipe(plugins.if(config.minifyHTML, plugins.htmlmin(htmlminOptions))) 677 | .pipe(gulp.dest(config.export_templates)) 678 | .on('end', function () { 679 | // Since above changes are made in a tapped stream 680 | // We have to count to make sure everything is parsed 681 | count = count + 1; 682 | if (count == numTemplates) { 683 | // Reload when serving 684 | if (lrStarted) { 685 | browserSync.reload(/*{stream:true}*/); 686 | } 687 | 688 | // Update the loadbar 689 | updateBar(); 690 | 691 | // Report unvalidated files 692 | if (unvalidatedFiles.length) { 693 | console.log(chalk.yellow('✘ Couldn\'t validate: ' + unvalidatedFiles.join(', '))); 694 | console.log(chalk.yellow.inverse('W3C validation only works for HTML files')); 695 | } 696 | 697 | // Report the end of this task 698 | cb(null); 699 | } 700 | }) 701 | ; 702 | 703 | if (config.w3c && ext !== 'html') { 704 | unvalidatedFiles.push(baseName); 705 | } 706 | })) 707 | ; 708 | }); 709 | 710 | // MANIFEST ------------------------------------------------------------------- 711 | // 712 | 713 | gulp.task('manifest', function (cb) { 714 | 715 | // Quit this task if the revisions aren't turned on 716 | if (!config.revisionCaching) { 717 | updateBar(); 718 | cb(null); 719 | return; 720 | } 721 | 722 | verbose(chalk.grey('Running task "manifest"')); 723 | 724 | return gulp.src([ 725 | config.export_assets + '/' + assetsFolder + '/js/*', 726 | config.export_assets + '/' + assetsFolder + '/css/*' 727 | ]) 728 | .pipe(plugins.manifest({ 729 | filename: 'app.manifest', 730 | exclude: 'app.manifest' 731 | })) 732 | .pipe(gulp.dest(config.export_misc)) 733 | .on('end', updateBar) 734 | ; 735 | }); 736 | 737 | // SERVER --------------------------------------------------------------------- 738 | // 739 | 740 | gulp.task('server', ['browsersync'], function (cb) { 741 | 742 | verbose(chalk.grey('Running task "server"')); 743 | console.log(chalk.grey('Watching files...')); 744 | 745 | gulp.watch([assetsFolder + '/sass/**/*.{scss, sass, css}', '!' + assetsFolder + '/sass/*ie.{scss, sass, css}'], ['sass-main']).on('change', watchHandler); 746 | gulp.watch([assetsFolder + '/js/**/view-*.js'], ['scripts-view', 'templates']).on('change', watchHandler); 747 | gulp.watch([assetsFolder + '/js/**/*.js', '!**/view-*.js'], ['scripts-main', 'templates']).on('change', watchHandler); 748 | gulp.watch([assetsFolder + '/images/**/*'], ['images']).on('change', watchHandler); 749 | gulp.watch(['templates/**/*'], ['templates']).on('change', watchHandler); 750 | 751 | cb(null); 752 | }); 753 | 754 | gulp.task('browsersync', function (cb) { 755 | 756 | verbose(chalk.grey('Running task "browsersync"')); 757 | console.log(chalk.grey('Launching server...')); 758 | 759 | // Grab the event emitter and add some listeners 760 | var evt = browserSync.emitter; 761 | evt.on('init', bsInitHandler); 762 | evt.on('service:running', bsRunningHandler); 763 | 764 | // Serve files and connect browsers 765 | browserSync.init(null, { 766 | server: _.isUndefined(config.proxy) ? { 767 | baseDir: config.export_templates 768 | } : false, 769 | logConnections: false, 770 | logLevel: 'silent', // 'debug' 771 | browser: config.browser || 'default', 772 | open: isOpen, 773 | port: config.port || 3000, 774 | proxy: config.proxy || false, 775 | tunnel: isTunnel || null 776 | }, function (err, data) { 777 | 778 | // Use this callback to catch errors, which aren't transmitted 779 | // through `init` 780 | if (err !== null) { 781 | console.log( 782 | chalk.red('✘ Setting up a local server failed... Please try again. Aborting.\n') + 783 | chalk.red(err) 784 | ); 785 | process.exit(0); 786 | } 787 | 788 | cb(null); 789 | }); 790 | }); 791 | 792 | // PAGESPEED INSIGHTS --------------------------------------------------------- 793 | // 794 | 795 | gulp.task('psi', function (cb) { 796 | 797 | // Quit this task if no flag was set 798 | if(!isPSI) { 799 | cb(null); 800 | return; 801 | } 802 | 803 | verbose(chalk.grey('Running task "psi"')); 804 | console.log(chalk.grey('Running PageSpeed Insights (might take a while)...')); 805 | 806 | // Define PSI options 807 | var opts = { 808 | url: tunnelUrl, 809 | strategy: flags.strategy || "desktop", 810 | threshold: 80 811 | }; 812 | 813 | // Set the key if one was passed in 814 | if (!!flags.key && _.isString(flags.key)) { 815 | console.log(chalk.yellow.inverse('Using a key is not yet supported as it just crashes the process. For now, continue using `--psi` without a key.')); 816 | // TODO: Fix key 817 | //opts.key = flags.key; 818 | } 819 | 820 | // Run PSI 821 | psi(opts, function (err, data) { 822 | 823 | // If there was an error, log it and exit 824 | if (err !== null) { 825 | console.log(chalk.red('✘ Threshold of ' + opts.threshold + ' not met with score of ' + data.score)); 826 | } else { 827 | console.log(chalk.green('✔ Threshold of ' + opts.threshold + ' exceeded with score of ' + data.score)); 828 | } 829 | 830 | cb(null); 831 | }); 832 | }); 833 | 834 | // HELPER FUNCTIONS ----------------------------------------------------------- 835 | // 836 | 837 | // Download the boilerplate files 838 | function downloadBoilerplateFiles () { 839 | 840 | console.log(chalk.grey('Downloading boilerplate files...')); 841 | 842 | // If a custom repo was passed in, use it 843 | if (!!flags.base) { 844 | 845 | // Check if there's a slash 846 | if (flags.base.indexOf('/') < 0) { 847 | console.log(chalk.red('✘ Please pass in a correct repository, eg. `username/repository` or `user/repo#branch. Aborting.\n')); 848 | process.exit(0); 849 | } 850 | 851 | // Check if there's a reference 852 | if (flags.base.indexOf('#') > -1) { 853 | flags.base = flags.base.split('#'); 854 | gitConfig.ref = flags.base[1]; 855 | flags.base = flags.base[0]; 856 | } else { 857 | gitConfig.ref = null; 858 | } 859 | 860 | // Extract username and repo 861 | flags.base = flags.base.split('/'); 862 | gitConfig.user = flags.base[0]; 863 | gitConfig.repo = flags.base[1]; 864 | 865 | // Extra validation 866 | if (gitConfig.user.length <= 0) { 867 | console.log(chalk.red('✘ The passed in username is invald. Aborting.\n')); 868 | process.exit(0); 869 | } 870 | if (gitConfig.repo.length <= 0) { 871 | console.log(chalk.red('✘ The passed in repository is invald. Aborting.\n')); 872 | process.exit(0); 873 | } 874 | } 875 | 876 | // Download the boilerplate files to a temp folder 877 | // This is to prevent a ENOEMPTY error 878 | ghdownload(gitConfig, tmpFolder) 879 | // Let the user know when something went wrong 880 | .on('error', function (error) { 881 | console.log(chalk.red('✘ An error occurred. Aborting.'), error); 882 | process.exit(0); 883 | }) 884 | // Download succeeded 885 | .on('end', function () { 886 | console.log( 887 | chalk.green('✔ Download complete!\n') + 888 | chalk.grey('Cleaning up...') 889 | ); 890 | 891 | // Move to working directory, clean temp, finish init 892 | ncp(tmpFolder, cwd, function (err) { 893 | 894 | if (err) { 895 | console.log(chalk.red('✘ Something went wrong. Please try again'), err); 896 | process.exit(0); 897 | } 898 | 899 | sequence('clean-tmp', function () { 900 | finishInit(); 901 | }); 902 | }); 903 | }) 904 | // TODO: Try to catch the error when a ZIP has "NOEND" 905 | ; 906 | } 907 | 908 | // Wrap up after running init and 909 | // downloading the boilerplate files 910 | function finishInit () { 911 | 912 | // Ask the user if he wants to continue and 913 | // have the files served and opened 914 | prompt({ 915 | type: 'confirm', 916 | message: 'Would you like to have these files served?', 917 | name: 'build', 918 | default: true 919 | }, function (buildAnswer) { 920 | 921 | if (buildAnswer.build) { 922 | isServe = true; 923 | prompt({ 924 | type: 'confirm', 925 | message: 'Should they be opened in the browser?', 926 | name: 'open', 927 | default: true 928 | 929 | }, function (openAnswer) { 930 | 931 | if (openAnswer.open) isOpen = true; 932 | prompt({ 933 | type: 'confirm', 934 | message: 'Should they be opened in an editor?', 935 | name: 'edit', 936 | default: true 937 | 938 | }, function (editAnswer) { 939 | 940 | if (editAnswer.edit) isEdit = true; 941 | gulp.start('build'); 942 | }); 943 | }); 944 | } 945 | else process.exit(0); 946 | }); 947 | } 948 | 949 | // Update the loadbar if one is set 950 | function updateBar () { 951 | if (!isVerbose && bar !== null) { 952 | bar.tick(); 953 | } 954 | } 955 | 956 | // Handle change events for Gulp watch instances 957 | function watchHandler (e) { 958 | console.log(chalk.grey('"' + e.path.split('/').pop() + '" was ' + e.type)); 959 | } 960 | 961 | // Browser Sync `init` event handler 962 | function bsInitHandler (data) { 963 | 964 | // Store started state globally 965 | lrStarted = true; 966 | 967 | // Show some logs 968 | console.log(chalk.cyan('🌐 Local access at'), chalk.magenta(data.options.urls.local)); 969 | console.log(chalk.cyan('🌐 Network access at'), chalk.magenta(data.options.urls.external)); 970 | 971 | if (isOpen) { 972 | console.log( 973 | chalk.cyan('☞ Opening in'), 974 | chalk.magenta(config.browser) 975 | ); 976 | } 977 | 978 | // Open an editor if needed 979 | if (isEdit) { 980 | openEditor(); 981 | } 982 | } 983 | 984 | // Browser Sync `service:running event handler 985 | function bsRunningHandler (data) { 986 | 987 | if (data.tunnel) { 988 | tunnelUrl = data.tunnel; 989 | console.log(chalk.cyan('🌐 Public access at'), chalk.magenta(tunnelUrl)); 990 | 991 | if (isPSI) { 992 | gulp.start('psi'); 993 | } 994 | } else if (isPSI) { 995 | console.log(chalk.red('✘ Running PSI cannot be started without a tunnel. Please restart Headstart with the `--tunnel` or `t` flag.')); 996 | } 997 | } 998 | 999 | // Open files in editor 1000 | function openEditor () { 1001 | 1002 | console.log( 1003 | chalk.cyan('☞ Editing in'), 1004 | chalk.magenta(config.editor) 1005 | ); 1006 | open(cwd, config.editor); 1007 | } 1008 | 1009 | // Make extra logs in verbose mode 1010 | function verbose (msg) { 1011 | 1012 | if(isVerbose) console.log(msg); 1013 | } 1014 | 1015 | // Check if the passed in string may be logged out 1016 | function validForWrite (msg, cleanMsg) { 1017 | 1018 | cleanMsg = chalk.stripColor(msg); 1019 | 1020 | // Detect gulp-util "[XX:XX:XX] ..." logs, 1021 | if (/^\[[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}\]/.test(cleanMsg)) { 1022 | var allowFlag = false; 1023 | 1024 | // Allow gulp-ruby-sass errors, 1025 | // but format them a bit 1026 | if (cleanMsg.indexOf('was changed') > -1) { 1027 | msg = cleanMsg.split(' '); 1028 | msg.shift(); 1029 | msg[0] = msg[0].split('/').pop(); 1030 | msg = msg.join(' ').trim(); 1031 | msg = chalk.grey(msg) + '\n'; 1032 | 1033 | allowFlag = true; 1034 | } 1035 | 1036 | // Allow gulp-plumber errors, 1037 | // but format them a bit 1038 | if (!allowFlag && cleanMsg.indexOf('Plumber found') > - 1) { 1039 | msg = cleanMsg.split('Plumber found unhandled error:').pop().trim(); 1040 | msg = chalk.red.inverse('ERROR') + ' ' + msg + '\n'; 1041 | 1042 | allowFlag = true; 1043 | } 1044 | 1045 | // Grab the result of gulp-imagemin 1046 | if (!allowFlag && cleanMsg.indexOf('gulp-imagemin: Minified') > -1) { 1047 | msg = cleanMsg.split('gulp-imagemin:').pop().trim(); 1048 | msg = chalk.green('✄ ' + msg) + '\n'; 1049 | 1050 | allowFlag = true; 1051 | } 1052 | 1053 | // Allow W3C validation errors, 1054 | // but format them a bit 1055 | if (!allowFlag && cleanMsg.indexOf('HTML Error:') > -1) { 1056 | msg = cleanMsg.split('HTML Error:').pop().trim(); 1057 | msg = chalk.red.inverse('HTML ERROR') + ' ' + msg + '\n'; 1058 | 1059 | allowFlag = true; 1060 | } 1061 | 1062 | // Grab the result of CSSMin 1063 | if (!allowFlag && cleanMsg.indexOf('.css is now') > -1) { 1064 | msg = cleanMsg.split(' ').slice(1).join(' ').trim(); 1065 | msg = chalk.green('✄ ' + msg) + '\n'; 1066 | 1067 | allowFlag = true; 1068 | } 1069 | 1070 | // Block all the others 1071 | if (!allowFlag) { 1072 | return false; 1073 | } 1074 | } 1075 | 1076 | // Block sass-graph errors 1077 | var graphMatches = _.filter(['failed to resolve', 'failed to add'], function (part) { 1078 | return cleanMsg.indexOf(part) > -1; 1079 | }); 1080 | if (/^failed to resolve|failed to add/.test(msg)) { 1081 | return false; 1082 | } 1083 | 1084 | return msg; 1085 | } 1086 | -------------------------------------------------------------------------------- /lib/hook.js: -------------------------------------------------------------------------------- 1 | // Module to (temporarily) replace a method on an object 2 | // 3 | // By Kevin (https://github.com/iamkvein) 4 | // 5 | // via http://stackoverflow.com/questions/9609393/catching-console-log-in-node-js 6 | // and https://gist.github.com/iamkvein/2006752 7 | 8 | module.exports = function(obj) { 9 | 10 | if (obj.hook || obj.unhook) { 11 | throw new Error('Object already has properties hook and/or unhook'); 12 | } 13 | 14 | obj.hook = function(_meth_name, _fn, _is_async) { 15 | var self = this, 16 | meth_ref; 17 | 18 | // Make sure method exists 19 | if (! (Object.prototype.toString.call(self[_meth_name]) === '[object Function]')) { 20 | throw new Error('Invalid method: ' + _meth_name); 21 | } 22 | 23 | // We should not hook a hook 24 | if (self.unhook.methods[_meth_name]) { 25 | throw new Error('Method already hooked: ' + _meth_name); 26 | } 27 | 28 | // Reference default method 29 | meth_ref = (self.unhook.methods[_meth_name] = self[_meth_name]); 30 | 31 | self[_meth_name] = function() { 32 | var args = Array.prototype.slice.call(arguments); 33 | 34 | // Our hook should take the same number of arguments 35 | // as the original method so we must fill with undefined 36 | // optional args not provided in the call 37 | while (args.length < meth_ref.length) { 38 | args.push(undefined); 39 | } 40 | 41 | // Last argument is always original method call 42 | args.push(function() { 43 | var args = arguments; 44 | 45 | if (_is_async) { 46 | process.nextTick(function() { 47 | meth_ref.apply(self, args); 48 | }); 49 | } else { 50 | meth_ref.apply(self, args); 51 | } 52 | }); 53 | 54 | _fn.apply(self, args); 55 | }; 56 | }; 57 | 58 | obj.unhook = function(_meth_name) { 59 | var self = this, 60 | ref = self.unhook.methods[_meth_name]; 61 | 62 | if (ref) { 63 | self[_meth_name] = self.unhook.methods[_meth_name]; 64 | delete self.unhook.methods[_meth_name]; 65 | } else { 66 | throw new Error('Method not hooked: ' + _meth_name); 67 | } 68 | }; 69 | 70 | obj.unhook.methods = {}; 71 | 72 | return obj; 73 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "headstart", 3 | "version": "1.3.2", 4 | "description": "An easy-to-use automated front-end setup.", 5 | "author": { 6 | "name": "Florian Vanthuyne", 7 | "email": "hello@flovan.me", 8 | "url": "http://headstart.io" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/flovan/headstart" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/flovan/headstart/issues" 16 | }, 17 | "license": "MIT", 18 | "dependencies": { 19 | "browser-sync": "^1.5.1", 20 | "chalk": "^0.5.1", 21 | "github-download": "^0.3.0", 22 | "globule": "^0.2.0", 23 | "gulp": "^3.8.8", 24 | "gulp-autoprefixer": "1.0.1", 25 | "gulp-bytediff": "^0.2.0", 26 | "gulp-combine-media-queries": "0.1.0", 27 | "gulp-compile-handlebars": "0.3.3", 28 | "gulp-concat": "^2.4.1", 29 | "gulp-deporder": "^1.0.0", 30 | "gulp-htmlmin": "^0.2.0", 31 | "gulp-if": "^1.2.4", 32 | "gulp-ignore": "^1.2.0", 33 | "gulp-imagemin": "^1.0.1", 34 | "gulp-inject": "^1.0.2", 35 | "gulp-jshint": "^1.8.4", 36 | "gulp-load-plugins": "^0.6.0", 37 | "gulp-manifest": "0.0.6", 38 | "gulp-minify-css": "^0.3.10", 39 | "gulp-newer": "^0.3.0", 40 | "gulp-plumber": "^0.6.5", 41 | "gulp-rename": "^1.2.0", 42 | "gulp-rev": "^1.1.0", 43 | "gulp-rev-outdated": "^0.0.6", 44 | "gulp-rimraf": "^0.1.0", 45 | "gulp-ruby-sass": "^0.7.1", 46 | "gulp-sass-graph": "git://github.com/lox/gulp-sass-graph#828d8efbf1925a09ff9dd3157603f58506c8ae67", 47 | "gulp-strip-debug": "^1.0.1", 48 | "gulp-tap": "^0.1.1", 49 | "gulp-uglify": "^1.0.1", 50 | "gulp-uncss": "^0.4.5", 51 | "gulp-w3cjs": "^0.2.1", 52 | "gulp-watch": "^1.0.7", 53 | "inquirer": "^0.7.3", 54 | "jshint-stylish": "^1.0.0", 55 | "liftoff": "^0.13.2", 56 | "lodash": "^2.4.1", 57 | "minimist": "1.1.0", 58 | "ncp": "^1.0.0", 59 | "progress": "^1.1.8", 60 | "psi": "^0.1.2", 61 | "run-sequence": "^0.3.7", 62 | "update-notifier": "^0.2.1", 63 | "open": "0.0.5" 64 | }, 65 | "devDependencies": { 66 | "gulp-debug": "^1.0.1", 67 | "gulp-duration": "0.0.0" 68 | }, 69 | "engines": { 70 | "node": ">=0.8.0" 71 | }, 72 | "bin": { 73 | "headstart": "./bin/headstart.js", 74 | "hs": "./bin/headstart.js" 75 | } 76 | } 77 | --------------------------------------------------------------------------------