diff --git a/Gruntfile.js b/Gruntfile.js index 074b1ba4f..0674d3800 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -3,948 +3,86 @@ module.exports = function (grunt) { 'use strict'; - function getPackageVersion() { - return grunt.file.readJSON('./package.json').version; + /* + * Welcome to our GruntFile.js! + * Configuration tasks (initConfig) are external JS modules that can be found in + * `./grunt/config`. Additional custom tasks can be found in `./grunt/task`. + * The "shared variables" belowcannot use grunt.config(), + * since it has not been initialized yet, until grunt.initConfig() is executed. + */ + + function getPackage() { + return grunt.file.readJSON('./package.json'); } // use --no-livereload to disable livereload. Helpful to 'serve' multiple projects var isLivereloadEnabled = (typeof grunt.option('livereload') !== 'undefined') ? grunt.option('livereload') : true; + // external libraries var semver = require('semver'); - + var packageVersion = getPackage().version; var fs = require('fs'); var path = require('path'); - var commonJSBundledReferenceModule = require('./grunt/commonjs-reference-module.js'); - - // Project configuration. - grunt.initConfig({ - // Metadata - bannerRelease: '/*!\n' + - ' * Fuel UX v<%= pkg.version %> \n' + - ' * Copyright 2012-<%= grunt.template.today("yyyy") %> <%= pkg.author.name %>\n' + - ' * Licensed under the <%= pkg.license.type %> license (<%= pkg.license.url %>)\n' + - ' */\n', - banner: '/*!\n' + - ' * Fuel UX EDGE - Built <%= grunt.template.today("yyyy/mm/dd, h:MM:ss TT") %> \n' + - ' * Previous release: v<%= pkg.version %> \n' + - ' * Copyright 2012-<%= grunt.template.today("yyyy") %> <%= pkg.author.name %>\n' + - ' * Licensed under the <%= pkg.license.type %> license (<%= pkg.license.url %>)\n' + - ' */\n', - bump: { - options: { - files: ['package.json'], - updateConfigs: ['pkg'], - commit: false, - createTag: false, - tagName: '%VERSION%', - tagMessage: '%VERSION%', - push: false, - gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d' - } - }, - // default variables for release task - release: { - files: ['dist', 'README.md', 'DETAILS.md', 'bower.json', 'package.json'], - localBranch: 'release', - remoteBaseBranch: 'master', - remoteDestinationBranch: '3.x', - remoteRepository: 'upstream', - }, - jqueryCheck: 'if (typeof jQuery === \'undefined\') { throw new Error(\'Fuel UX\\\'s JavaScript requires jQuery\') }\n\n', - bootstrapCheck: 'if (typeof jQuery.fn.dropdown === \'undefined\' || typeof jQuery.fn.collapse === \'undefined\') ' + - '{ throw new Error(\'Fuel UX\\\'s JavaScript requires Bootstrap\') }\n\n', - pkg: grunt.file.readJSON('package.json'), - // Try ENV variables (export SAUCE_ACCESS_KEY=XXXX), if key doesn't exist, try key file - sauceLoginFile: grunt.file.exists('SAUCE_API_KEY.yml') ? grunt.file.readYAML('SAUCE_API_KEY.yml') : undefined, - cdnLoginFile: grunt.file.exists('FUEL_CDN.yml') ? grunt.file.readYAML('FUEL_CDN.yml') : undefined, - sauceUser: process.env.SAUCE_USERNAME || 'fuelux', - sauceKey: process.env.SAUCE_ACCESS_KEY ? process.env.SAUCE_ACCESS_KEY : '<%= sauceLoginFile.key %>', - // TEST URLS - allTestUrls: ['2.1.0', '1.11.0', '1.9.1', 'browserGlobals', 'noMoment', 'codeCoverage' ].map(function (type) { - if (type === 'browserGlobals') { - return 'http://localhost:<%= connect.testServer.options.port %>/test/browser-globals.html'; - } - else if (type === 'codeCoverage') { - return 'http://localhost:<%= connect.testServer.options.port %>/test/?coverage=true'; - } - else if (type === 'noMoment') { - return 'http://localhost:<%= connect.testServer.options.port %>/test/?no-moment=true'; - } - else { - // test dist with multiple jQuery versions - return 'http://localhost:<%= connect.testServer.options.port %>/test/?testdist=true'; - } - }), - - //Tasks configuration - blanket_qunit: { - source: { - options: { - urls: ['http://localhost:<%= connect.testServer.options.port %>/test/?coverage=true&gruntReport'], - threshold: 1, - globalThreshold: 1 - } - } - }, - browserify: { - commonjs: { - files: { - 'test/commonjs-bundle.js': ['test/commonjs-test.js'], - } - } - }, - clean: { - dist: ['dist'], - zipsrc: ['dist/fuelux'],// temp folder - screenshots: ['page-at-timeout-*.jpg'] - }, - compress: { - zip: { - files: [ - { - cwd: 'dist/', - expand: true, - src: ['fuelux/**'] - } - ], - options: { - archive: 'dist/fuelux.zip', - mode: 'zip' - } - } - }, - concat: { - // manually concatenate JS files (due to dependency management) - fuelux: { - src: [ - 'js/checkbox.js', - 'js/combobox.js', - 'js/datepicker.js', - 'js/dropdown-autoflip.js', - 'js/loader.js', - 'js/placard.js', - 'js/radio.js', - 'js/search.js', - 'js/selectlist.js', - 'js/spinbox.js', - 'js/tree.js', - 'js/wizard.js', - - //items with dependencies on other controls - 'js/infinite-scroll.js', - 'js/pillbox.js', - 'js/repeater.js', - 'js/repeater-list.js', - 'js/repeater-thumbnail.js', - 'js/scheduler.js' - ], - dest: 'dist/js/<%= pkg.name %>.js' - }, - options: { - banner: '<%= banner %>' + '\n\n' + - '// For more information on UMD visit: https://github.com/umdjs/umd/' + '\n' + - '(function (factory) {' + '\n' + - '\tif (typeof define === \'function\' && define.amd) {' + '\n' + - '\t\tdefine([\'jquery\', \'bootstrap\'], factory);' + '\n' + - '\t} else {' + '\n' + - '\t\tfactory(jQuery);' + '\n' + - '\t}' + '\n' + - '}(function (jQuery) {\n\n' + - '<%= jqueryCheck %>' + - '<%= bootstrapCheck %>', - footer: '\n}));', - process: function (source) { - source = '(function ($) {\n\n' + - source.replace(/\/\/ -- BEGIN UMD WRAPPER PREFACE --(\n|.)*\/\/ -- END UMD WRAPPER PREFACE --/g, ''); - source = source.replace(/\/\/ -- BEGIN UMD WRAPPER AFTERWORD --(\n|.)*\/\/ -- END UMD WRAPPER AFTERWORD --/g, '') + '\n})(jQuery);\n\n'; - return source; - } - } - }, - connect: { - server: { - options: { - hostname: '*', - base: { - path: '.', - options: { - index: ['index.html', 'tests.html'], - } - }, - port: process.env.PORT || 8000, - useAvailablePort: true // increment port number, if unavailable... - } - }, - testServer: { - options: { - base: { - path: '.', - options: { - index: ['index.html', 'tests.html'], - } - }, - hostname: '*', - port: 9000, // allows main server to be run simultaneously - useAvailablePort: true// increment port number, if unavailable... - } - } - }, - copy: { - fonts: { - cwd: 'fonts/', - dest: 'dist/fonts/', - expand: true, - filter: 'isFile', - src: ['*'] - }, - zipsrc: { - cwd: 'dist/', - dest: 'dist/fuelux/', - expand: true, - src: ['**'] - } - }, - htmllint: { - options: { - ignore:[ - 'Section lacks heading. Consider using “h2”-“h6” elements to add identifying headings to all sections.', - 'Bad value X-UA-Compatible for attribute http-equiv on element meta.', - 'Element head is missing a required instance of child element title.' - ], - force: true - }, - src: ['index.html', 'markup/*.html', 'test/markup/*.html'] - }, - jsbeautifier: { - files: ['dist/js/fuelux.js'], - options: { - js: { - braceStyle: 'collapse', - breakChainedMethods: false, - e4x: false, - evalCode: false, - indentLevel: 0, - indentSize: 4, - indentWithTabs: true, - jslintHappy: false, - keepArrayIndentation: false, - keepFunctionIndentation: false, - maxPreserveNewlines: 10, - preserveNewlines: true, - spaceBeforeConditional: true, - spaceInParen: true, - unescapeStrings: false, - wrapLineLength: 0 - } - } - }, - jshint: { - options: { - boss: true, - browser: true, - curly: false, - eqeqeq: true, - eqnull: true, - globals: { - jQuery: true, - define: true, - require: true, - module: true - }, - immed: true, - latedef: true, - newcap: true, - noarg: true, - sub: true, - undef: true, - unused: false// changed - }, - sourceAndDist: ['Gruntfile.js', 'js/*.js', 'dist/fuelux.js'], - tests: { - options: { - latedef: false, - undef: false, - unused: false - }, - files: { - src: ['test/**/*.js','!test/commonjs-bundle.js'] - } - } - }, - qunit: { - //run with `grunt releasetest` or `grunt travisci`. Requires connect server to be running. - release: { - options: { - urls: '<%= allTestUrls %>', - screenshot: true, - page: { - viewportSize: { - width: 1280, - height: 1024 - } - } - } - }, - globals: { - options: { - urls: ['http://localhost:<%= connect.testServer.options.port %>/test/browser-globals.html'] - } - }, - noMoment: { - options: { - urls: ['http://localhost:<%= connect.testServer.options.port %>/test/?no-moment=true'] - } - }, - // `grunt qunit:source` will test the source files directly. - source: { - options: { - urls: ['http://localhost:<%= connect.testServer.options.port %>/test/'] - } - }, - dist: { - options: { - urls: ['http://localhost:<%= connect.testServer.options.port %>/test/?testdist=true', - 'http://localhost:<%= connect.testServer.options.port %>/test/commonjs.html'] - } - } - }, - less: { - dev: { - options: { - strictMath: true, - sourceMap: true, - outputSourceFiles: true, - sourceMapURL: '<%= pkg.name %>-dev.css.map', - sourceMapFilename: 'dist/css/<%= pkg.name %>-dev.css.map' - }, - files: { - 'dist/css/fuelux-dev.css': 'less/fuelux.less' - } - }, - dist: { - options: { - strictMath: true, - sourceMap: true, - outputSourceFiles: true, - sourceMapURL: '<%= pkg.name %>.css.map', - sourceMapFilename: 'dist/css/<%= pkg.name %>.css.map' - }, - files: { - 'dist/css/fuelux.css': 'less/fuelux.less' - } - }, - minify: { - options: { - cleancss: true, - compress: true, - report: 'min' - }, - files: { - 'dist/css/<%= pkg.name %>.min.css': 'dist/css/<%= pkg.name %>.css' - } - } - - }, - prompt: { - // asks for what version you want to build - 'tempbranch': { - options: { - questions: [ - { - config: 'release.remoteRepository', - default : '<%= release.remoteRepository %>', - type: 'input', - message: function() { - return 'What repository would like to base your local release branch from?'; - } - }, - { - // Assumption is made that you are releasing the code within a "release branch" currently - // on the upstream remote repo. This branch will be tracked locally and be used to run - // the build process in. It will be named release_{BUILD_VERSION}_{MMSS} (that is, it will - // use the version specified earlier and a "mini-timestamp" of the current hour and minute). - config: 'release.remoteBaseBranch', - type: 'input', - default : '<%= release.remoteBaseBranch %>', - message: function() { - return 'What remote branch from ' + grunt.config('release.remoteRepository') + - ' would like to build your release based on?'; - } - } - ] - } - }, - // asks for what version you want to build - 'build': { - options: { - questions: [ - { - config: 'release.buildSemVerType', - type: 'list', - message: 'What would you like to do?', - choices: [ - { - value: 'patch', - name: 'Patch: ' + semver.inc(getPackageVersion(), 'patch') + ' Backwards-compatible bug fixes.' - }, - { - value: 'minor', - name: 'Minor: ' + semver.inc(getPackageVersion(), 'minor') + ' Add functionality in a backwards-compatible manner.' - }, - { - value: 'major', - name: 'Major: ' + semver.inc(getPackageVersion(), 'major') + ' Incompatible API changes.' - }, - { - value: 'custom', - name: 'Custom: ?.?.? Specify version...' - } - ] - }, - { - // if custom bump is used with a specific version, see dorelease task - config: 'release.buildSemVerType', - type: 'input', - message: 'What specific version would you like', - when: function (answers) { - return answers['release.buildSemVerType'] === 'custom'; - }, - validate: function (value) { - var valid = semver.valid(value); - return valid || 'Must be a valid semver, such as 1.2.3-rc1. See http://semver.org/ for more details.'; - } - } - ] - } - }, - 'commit': { - options: { - questions: [ - { - config: 'release.commit', - type: 'confirm', - message: 'Please review your files. Would you like to commit?' - } - ], - then: function (answers, done) { - if (answers['release.commit'] === true) { - grunt.task.run(['shell:commit']); - } - return false; - } - } - }, - 'tag': { - options: { - questions: [ - { - config: 'release.tag', - type: 'confirm', - message: 'Would you like to tag as ' + '<%= pkg.version %>' + '?' - } - ], - then: function (answers, done) { - if (answers['release.tag'] === true) { - grunt.task.run(['shell:tag']); - } - return false; - } - } - }, - 'pushLocalBranchToUpstream': { - options: { - questions: [ - { - config: 'release.remoteDestinationBranch', - type: 'input', - message: function() { - return 'What upstream branch would you like to push ' + grunt.config('release.localBranch') + - ' to (probably ' + grunt.config('release.remoteDestinationBranch') + ')? (leave blank to skip)'; - } - } - ], - then: function (answers, done) { - if (answers['release.remoteDestinationBranch'] !== '' && answers['release.remoteDestinationBranch'] !== 'n' ) { - grunt.task.run(['shell:pushLocalBranchToUpstream']); - } - return false; - } - } - }, - 'pushTagToUpstream': { - options: { - questions: [ - { - config: 'release.upstreamTag', - type: 'confirm', - message: 'Would you like to push tag ' + '<%= pkg.version %>' + ' to upstream?' - } - ], - then: function (answers, done) { - if (answers['release.upstreamTag'] === true) { - grunt.task.run(['shell:pushTagToUpstream']); - } - return false; - } - } - }, - 'uploadToCDN': { - options: { - questions: [ - { - config: 'release.uploadToCDN', - type: 'confirm', - message: 'Would you like to upload the `dist folder to fuelcdn.com?' - } - ], - then: function (answers, done) { - if (answers['release.uploadToCDN'] === true) { - grunt.task.run(['shell:uploadToCDN']); - } - return false; - } - } - }, - 'pushLocalBranchToUpstreamMaster': { - options: { - questions: [ - { - config: 'release.pushToUpstreamMaster', - type: 'confirm', - message: 'Would you like to push your local release branch to upstream\'s master branch?' - } - ], - then: function (answers, done) { - if (answers['release.pushToUpstreamMaster'] === true) { - grunt.task.run(['shell:pushLocalBranchToUpstreamMaster']); - } - return false; - } - } - }, - 'deleteLocalReleaseBranch': { - options: { - questions: [ - { - config: 'release.deleteLocalReleaseBranch', - type: 'confirm', - message: function() { - return 'Would you like to delete your local release branch' + '?'; - } - } - ], - then: function (answers, done) { - if (answers['release.deleteLocalReleaseBranch'] === true) { - grunt.task.run(['shell:deleteLocalReleaseBranch']); - } - return false; - } - } - } - }, - replace: { - readme: { - src: ['DETAILS.md', 'README.md'], - overwrite: true,// overwrite matched source files - replacements: [{ - from: /fuelux\/\d\.\d\.\d/g, - to: "fuelux/<%= pkg.version %>" - }] - } - }, - 'saucelabs-qunit': { - trickyBrowsers: { - options: { - username: '<%= sauceUser %>', - key: '<%= sauceKey %>', - tunnelTimeout: 45, - testInterval: 3000, - tags: ['<%= sauceUser %>' + '@' + process.env.TRAVIS_BRANCH || '<%= sauceUser %>' + '@local'], - browsers: grunt.file.readYAML('sauce_browsers_tricky.yml'), - build: process.env.TRAVIS_BUILD_NUMBER || '<%= pkg.version %>', - testname: process.env.TRAVIS_JOB_ID || Math.floor((new Date()).getTime() / 1000 - 1230768000).toString(), - urls: ['http://localhost:<%= connect.testServer.options.port %>/test/?testdist=true'] - } - }, - defaultBrowsers: { - options: { - username: '<%= sauceUser %>', - key: '<%= sauceKey %>', - tunnelTimeout: 45, - testInterval: 3000, - tags: ['<%= pkg.version %>', '<%= sauceUser %>' + '@' + process.env.TRAVIS_BRANCH || '<%= sauceUser %>@local'], - browsers: grunt.file.readYAML('sauce_browsers.yml'), - build: process.env.TRAVIS_BUILD_NUMBER || '<%= pkg.version %>', - testname: process.env.TRAVIS_JOB_ID || '<%= pkg.version %>-<%= grunt.template.today("dddd, mmmm dS, yyyy, h:MM:ss TT") %>', - urls: ['http://localhost:<%= connect.testServer.options.port %>/test/?testdist=true'], - maxPollRetries: 4, - throttled: 3, - maxRetries: 3 - } - }, - all: { - options: { - username: '<%= sauceUser %>', - key: '<%= sauceKey %>', - browsers: grunt.file.readYAML('sauce_browsers.yml'), - build: process.env.TRAVIS_BUILD_NUMBER || '<%= pkg.version %>', - testname: 'grunt-<%= grunt.template.today("dddd, mmmm dS, yyyy, h:MM:ss TT") %>', - urls: '<%= allTestUrls %>' - } - } - }, - shell: { - // Compile release notes while waiting for tests to pass. Needs Ruby gem and ONLY LOOKS AT THE REMOTE NAMED ORIGIN. - // Install with: gem install github_changelog_generator - notes: { - command: 'github_changelog_generator --no-author --unreleased-only --compare-link' - }, - checkoutRemoteReleaseBranch: { - // this makes a local branch based on the prior prompt, such as release_{TIMESTAMP} - // then update tags from remote in order to prevent duplicate tags - command: function() { - grunt.config('release.localBranch', 'release_' + new Date().getTime() ); - var command = [ - 'git checkout -b ' + grunt.config('release.localBranch') + ' ' + - grunt.config('release.remoteRepository') + '/' + grunt.config('release.remoteBaseBranch'), - 'git fetch ' + grunt.config('release.remoteRepository') + ' --tag' - ].join(' && '); - grunt.log.write('Checking out new local branch based on ' + grunt.config('release.remoteBaseBranch') + ': ' + command); - return command; - } - }, - addReleaseFiles: { - command: function() { - var command = 'git add ' + grunt.config('release.files').join(' '); - grunt.log.write('Staging: ' + command); - return command; - } - }, - commit: { - command: function() { - var command = 'git commit -m "release ' + grunt.config('pkg.version') + '"'; - grunt.log.write('Committing: ' + command); - return command; - } - }, - tag: { - command: function() { - var command = 'git tag -a "' + getPackageVersion() + '" -m "' + getPackageVersion() + '"'; - grunt.log.write('Tagging: ' + command); - return command; - } - }, - pushLocalBranchToUpstream: { - command: function() { - var command = 'git push ' + grunt.config('release.remoteRepository') + ' ' + - grunt.config('release.localBranch') + ':' + grunt.config('release.remoteDestinationBranch'); - grunt.log.write('Pushing: ' + command); - return command; - } - }, - pushTagToUpstream: { - command: function() { - var command = 'git push ' + grunt.config('release.remoteRepository') + ' ' + getPackageVersion(); - grunt.log.write('Publishing tag: ' + command); - return command; - } - }, - pushLocalBranchToUpstreamMaster: { - command: function() { - var command = 'git push ' + grunt.config('release.remoteRepository') + ' ' + - grunt.config('release.localBranch') + ':master'; - grunt.log.write(command); - return command; - } - }, - uploadToCDN: { - command: function() { - - function createUploadCommand(version) { - return ['mv dist ' + version, - 'scp -i ~/.ssh/fuelcdn -r "' + version + '"/ ' + - '<%= cdnLoginFile.user %>' + '@' + '<%= cdnLoginFile.server %>' + ':' + '<%= cdnLoginFile.folder %>', - 'mv "' + version + '" dist', - 'echo "Done uploading files."'].join(' && '); - } - var command = [ - getPackageVersion(), - semver.major(getPackageVersion()) + '.' + semver.minor(getPackageVersion()), - semver.major(getPackageVersion()) - ].map(createUploadCommand).join(' && '); - grunt.log.write('Uploading: ' + command); - grunt.log.writeln(''); - return command; - } - } - }, - uglify: { - options: { - report: 'min' - }, - fuelux: { - options: { - banner: '<%= banner %>' - }, - src: 'dist/js/<%= pkg.name %>.js', - dest: 'dist/js/<%= pkg.name %>.min.js' - } - }, - usebanner: { - dist: { - options: { - position: 'top', - banner: '<%= banner %>' - }, - files: { - src: [ - 'dist/css/<%= pkg.name %>.css', - 'dist/css/<%= pkg.name %>.min.css' - ] - } - } - }, - watch: { - //watch everything and test everything (test dist) - full: { - files: ['Gruntfile.js', 'fonts/**', 'js/**', 'less/**', 'test/**', 'index.html', 'dev.html'], - options: { - livereload: isLivereloadEnabled - }, - tasks: ['jshint', 'blanket_qunit:source', 'qunit:noMoment', 'qunit:globals', 'dist', 'qunit:dist', 'htmllint'] - }, - //watch everything but only perform source qunit tests (don't test dist) - source: { - files: ['Gruntfile.js', 'fonts/**', 'js/**', 'less/**', 'test/**', 'index.html', 'dev.html'], - options: { - livereload: isLivereloadEnabled - }, - tasks: ['jshint', 'connect:testServer', 'blanket_qunit:source', 'qunit:noMoment', 'qunit:globals', 'htmllint'] - }, - //only watch and dist less, useful when doing LESS/CSS work - less: { - files: ['fonts/**', 'less/**'], - options: { - livereload: isLivereloadEnabled - }, - tasks: ['distcss'] - }, - cssdev: { - files: ['Gruntfile.js', 'less/**', 'index.html', 'index-dev.html', 'dev.html', '!less/fuelux-no-namespace.less'], - options: { - livereload: isLivereloadEnabled - }, - tasks: ['distcssdev'] - }, - //watch things that need compiled, useful for debugging/developing against dist - dist: { - files: ['fonts/**', 'js/**', 'less/**'], - options: { - livereload: isLivereloadEnabled - }, - tasks: ['dist'] - }, - //just keep the server running, best for when you are just in the zone slinging code and don't want to be interrupted with tests - lite: { - files: [], - options: { - livereload: isLivereloadEnabled - }, - tasks: [] - } + var commonJSBundledReferenceModule = require('./grunt/other/commonjs-reference-module.js'); + + // variables used in shared variables below + var connectTestServerOptionsPort = 9000; + + // load and initialize configuration tasks, including package.json's devDependencies + require('load-grunt-config')(grunt, { + configPath: path.join(process.cwd(), 'grunt/config'), + loadGruntTasks: { + pattern: 'grunt-*', + config: require('./package.json'), + scope: 'devDependencies' + }, + data: { + // Variables shared across configuration tasks, use templates, <%= %>, to access + // within configuration tasks + bannerRelease: '/*!\n' + + ' * Fuel UX v<%= pkg.version %> \n' + + ' * Copyright 2012-<%= grunt.template.today("yyyy") %> <%= pkg.author.name %>\n' + + ' * Licensed under the <%= pkg.license.type %> license (<%= pkg.license.url %>)\n' + + ' */\n', + banner: '/*!\n' + + ' * Fuel UX EDGE - Built <%= grunt.template.today("yyyy/mm/dd, h:MM:ss TT") %> \n' + + ' * Previous release: v<%= pkg.version %> \n' + + ' * Copyright 2012-<%= grunt.template.today("yyyy") %> <%= pkg.author.name %>\n' + + ' * Licensed under the <%= pkg.license.type %> license (<%= pkg.license.url %>)\n' + + ' */\n', + jqueryCheck: 'if (typeof jQuery === \'undefined\') { throw new Error(\'Fuel UX\\\'s JavaScript requires jQuery\') }\n\n', + bootstrapCheck: 'if (typeof jQuery.fn.dropdown === \'undefined\' || typeof jQuery.fn.collapse === \'undefined\') ' + + '{ throw new Error(\'Fuel UX\\\'s JavaScript requires Bootstrap\') }\n\n', + pkg: getPackage(), + // Try ENV variables (export SAUCE_ACCESS_KEY=XXXX), if key doesn't exist, try key file + sauceLoginFile: grunt.file.exists('SAUCE_API_KEY.yml') ? grunt.file.readYAML('SAUCE_API_KEY.yml') : undefined, + cdnLoginFile: grunt.file.exists('FUEL_CDN.yml') ? grunt.file.readYAML('FUEL_CDN.yml') : undefined, + sauceUser: process.env.SAUCE_USERNAME || 'fuelux', + sauceKey: process.env.SAUCE_ACCESS_KEY ? process.env.SAUCE_ACCESS_KEY : '<%= sauceLoginFile.key %>', + // TEST URLS + allTestUrls: ['2.1.0', '1.11.0', '1.9.1', 'browserGlobals', 'noMoment', 'codeCoverage' ].map(function (type) { + if (type === 'browserGlobals') { + return 'http://localhost:' + connectTestServerOptionsPort + '/test/browser-globals.html'; + } + else if (type === 'codeCoverage') { + return 'http://localhost:' + connectTestServerOptionsPort + '/test/?coverage=true'; + } + else if (type === 'noMoment') { + return 'http://localhost:' + connectTestServerOptionsPort + '/test/?no-moment=true'; + } + else { + // test dist with multiple jQuery versions + return 'http://localhost:' + connectTestServerOptionsPort + '/test/?testdist=true'; + } + }), + connectTestServerOptionsPort: connectTestServerOptionsPort, } }); - // Look ma! Load all grunt plugins in one line from package.json - require('load-grunt-tasks')(grunt, { - scope: 'devDependencies' - }); - - /* ------------- - BUILD - ------------- */ - // JS distribution task - grunt.registerTask('distjs', 'concat, uglify', ['concat', 'uglify', 'jsbeautifier']); - - // CSS distribution task - grunt.registerTask('distcss', 'Compile LESS into CSS', ['less:dist', 'less:minify', 'usebanner']); - - // CSS distribution task (dev) - grunt.registerTask('distcssdev', 'Compile LESS into the dev CSS', [ 'less:dev', 'delete-temp-less-file']); - - // Temporary LESS file deletion task - grunt.registerTask('delete-temp-less-file', 'Delete the temporary LESS file created during the build process', function () { - var options = { - force: true - }; - grunt.file.delete('less/fuelux-no-namespace.less', options); - }); - - // ZIP distribution task - grunt.registerTask('distzip', 'Compress and zip "dist"', ['copy:zipsrc', 'compress', 'clean:zipsrc']); - - // create dist/js/npm.js - grunt.registerTask('commonjs', 'Create CommonJS "bundled reference" module in `dist`', function () { - var files = grunt.config.get('concat.fuelux.src'); - var bundledReferenceFile = 'dist/js/npm.js'; - commonJSBundledReferenceModule(grunt, files, bundledReferenceFile); - }); - - // Full distribution task - grunt.registerTask('dist', 'Build "dist." Contributors: do not commit "dist."', - ['clean:dist', 'distcss', 'copy:fonts', 'distjs', 'commonjs', 'distzip']); - - - /* ------------- - TESTS - ------------- */ - // The default build task grunt.registerTask('default', 'Run source file tests. Pass --no-resetdist to keep "dist" changes from being wiped out', ['test', 'clean:screenshots', 'resetdist']); - // to be run prior to submitting a PR - grunt.registerTask('test', 'run jshint, qunit source w/ coverage, and validate HTML', - ['jshint', 'connect:testServer', 'blanket_qunit:source', 'qunit:noMoment', 'qunit:globals', 'htmllint']); - - //If qunit:source is working but qunit:full is breaking, check to see if the dist broke the code. This would be especially useful if we start mangling our code, but, is 99.99% unlikely right now - grunt.registerTask('validate-dist', 'run qunit:source, dist, and then qunit:full', - ['connect:testServer', 'qunit:source', 'dist', 'browserify:commonjs', 'qunit:dist']); - - // multiple jQuery versions, then run SauceLabs VMs - grunt.registerTask('releasetest', 'run jshint, build dist, all source tests, validation, and qunit on SauceLabs', - ['test', 'dist', 'browserify:commonjs', 'qunit:dist', 'saucelabs-qunit:defaultBrowsers']); - - // can be run locally instead of through TravisCI, but requires the Fuel UX Saucelabs API key file which is not public at this time. - grunt.registerTask('saucelabs', 'run jshint, and qunit on saucelabs', - ['connect:testServer', 'jshint', 'saucelabs-qunit:defaultBrowsers']); - - // can be run locally instead of through TravisCI, but requires the FuelUX Saucelabs API key file which is not public at this time. - grunt.registerTask('trickysauce', 'run tests, jshint, and qunit for "tricky browsers" (IE8-11)', - ['connect:testServer', 'jshint', 'saucelabs-qunit:trickyBrowsers']); - - // Travis CI task. This task no longer uses SauceLabs. Please run 'grunt saucelabs' manually. - grunt.registerTask('travisci', 'Tests to run when in Travis CI environment', - ['test', 'dist', 'browserify:commonjs', 'qunit:dist']); - - //if you've already accidentally added your files for commit, this will at least unstage them. If you haven't, this will wipe them out. - grunt.registerTask('resetdist', 'resets changes to dist to keep them from being checked in', function () { - //default resetdist to true... basically. - if (typeof grunt.option('resetdist') === "undefined" || grunt.option('resetdist')) { - var exec = require('child_process').exec; - exec('git reset HEAD dist/*'); - exec('git checkout -- dist/*'); - } - }); - - /* ------------- - RELEASE - ------------- */ - grunt.registerTask('notes', 'Run a ruby gem that will request from Github unreleased pull requests', ['shell:notes']); - - // Maintainers: Run prior to a release. Includes SauceLabs VM tests. - grunt.registerTask('release', 'Release a new version, push it and publish it', function() { - if ( typeof grunt.config('sauceLoginFile') === 'undefined' ) { - grunt.log.write('The file SAUCE_API_KEY.yml is needed in order to run tests in SauceLabs.'['red'].bold + - ' Please contact another maintainer to obtain this file.'); - } - - if ( typeof grunt.config('cdnLoginFile') === 'undefined' ) { - grunt.log.write('The file FUEL_CDN.yml is needed in order to upload the release files to the CDN.'['red'].bold + - ' Please contact another maintainer to obtain this file.'); - } - - // update local variable to make sure build prompt is using temp branch's package version - grunt.task.run(['prompt:tempbranch', 'shell:checkoutRemoteReleaseBranch', - 'prompt:build', 'dorelease']); - }); - -// formerally dorelease task - grunt.registerTask('dorelease', '', function () { - - grunt.log.writeln(''); - - if (!grunt.option('no-tests')) { - grunt.task.run(['releasetest']); //If phantom timeouts happening because of long-running qunit tests, look into setting `resourceTimeout` in phantom: http://phantomjs.org/api/webpage/property/settings.html - // Delete any screenshots that may have happened if it got this far. This isn't foolproof - // because it relies on the phantomjs server/page timeout, which can take longer than this - // grunt task depending on how long saucelabs takes to run... - grunt.task.run('clean:screenshots'); - } - - grunt.config('banner', '<%= bannerRelease %>'); - - // Run dist again to grab the latest version numbers. Yeah, we're running it twice... ¯\_(ツ)_/¯ - grunt.task.run(['bump-only:' + grunt.config('release.buildSemVerType'), 'replace:readme', 'dist', - 'shell:addReleaseFiles', 'prompt:commit', 'prompt:tag', 'prompt:pushLocalBranchToUpstream', - 'prompt:pushTagToUpstream', 'prompt:uploadToCDN', 'prompt:pushLocalBranchToUpstreamMaster']); - }); - - - /* ------------- - SERVE - ------------- */ - // default serve task that runs tests and builds and tests dist by default. - grunt.registerTask('serve', 'Test, build, serve files. (~20s)', function () { - var tasks = ['test', 'servedist']; - grunt.task.run(tasks); - }); - - // serve task that runs tests and builds and tests dist by default (~20s). - grunt.registerTask('serveslow', 'Serve files. Run all tests. Does not build. (~20s)', function () { - var tasks = ['connect:server', 'test', 'watch:source']; - grunt.task.run(tasks); - }); - - //Fastest serve command for freely slinging code (no tests will run by default). - grunt.registerTask('servefast', 'Serve the files (no watch), --test to run minimal tests. (~0s)', function () { - grunt.task.run(['connect:server']); - - if (grunt.option('test')) { - grunt.task.run(['connect:testServer', 'qunit:source', 'watch:source']); - } else { - grunt.task.run(['watch:lite']); - } - }); - - // Fastest serve command when you're working on LESS - grunt.registerTask('serveless', 'Compile LESS and serve the files. pass --tests to run test. (~3s)', function () { - grunt.task.run(['distcss']); - - if (grunt.option('test')) { - // add qunit:source as a watch task for watch:less since they want tests - grunt.config.merge({ - watch: { - less: { - tasks: ['qunit:source'] - } - } - }); - grunt.task.run(['qunit:source']); - } - - grunt.task.run(['connect:server', 'watch:less']); - }); - - // Complies the less files into the -dev versions, does not overwrite the main css files. - grunt.registerTask('servedev', 'Serve the files with no "dist" build or tests. Optional --no-less to also disable compiling less into css.', function() { - if (! grunt.option('no-less') ) { - grunt.task.run(['distcssdev']); - } - grunt.task.run(['connect:server', 'watch:cssdev']); - }); - - // same as `grunt serve` but tests default to being off - grunt.registerTask('servedist', 'Compile and serve everything, pass --test to run tests. (~7s)', function () { - grunt.task.run(['dist']); - - //start up the servers here so we can run tests if appropriate - grunt.task.run(['connect:server']); - grunt.task.run(['connect:testServer']); - - if (grunt.option('test')) { - grunt.task.run(['browserify:commonjs','qunit:dist', 'watch:full']); - } else { - grunt.task.run(['watch:dist']); - } - }); -}; + // load custom build, release, serve, and test tasks from the folder specified + grunt.loadTasks('./grunt/tasks'); + +}; \ No newline at end of file diff --git a/grunt/config/blanket_qunit.js b/grunt/config/blanket_qunit.js new file mode 100644 index 000000000..7013bcc20 --- /dev/null +++ b/grunt/config/blanket_qunit.js @@ -0,0 +1,13 @@ +module.exports = function (grunt) { + + return { + source: { + options: { + urls: ['http://localhost:' + '<%= connectTestServerOptionsPort %>' + '/test/?coverage=true&gruntReport'], + threshold: 1, + globalThreshold: 1 + } + } + } + +}; \ No newline at end of file diff --git a/grunt/config/browserify.js b/grunt/config/browserify.js new file mode 100644 index 000000000..a512e27e9 --- /dev/null +++ b/grunt/config/browserify.js @@ -0,0 +1,7 @@ +module.exports = { + commonjs: { + files: { + 'test/commonjs-bundle.js': ['test/commonjs-test.js'] + } + } +}; \ No newline at end of file diff --git a/grunt/config/bump.js b/grunt/config/bump.js new file mode 100644 index 000000000..c36def938 --- /dev/null +++ b/grunt/config/bump.js @@ -0,0 +1,12 @@ +module.exports = { + options: { + files: ['package.json'], + updateConfigs: ['pkg'], + commit: false, + createTag: false, + tagName: '%VERSION%', + tagMessage: '%VERSION%', + push: false, + gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d' + } +}; \ No newline at end of file diff --git a/grunt/config/clean.js b/grunt/config/clean.js new file mode 100644 index 000000000..f572a4fa6 --- /dev/null +++ b/grunt/config/clean.js @@ -0,0 +1,5 @@ +module.exports = { + dist: ['dist'], + zipsrc: ['dist/fuelux'], + screenshots: ['page-at-timeout-*.jpg'] +}; \ No newline at end of file diff --git a/grunt/config/compress.js b/grunt/config/compress.js new file mode 100644 index 000000000..95ba89884 --- /dev/null +++ b/grunt/config/compress.js @@ -0,0 +1,13 @@ +module.exports = { + zip: { + files: [{ + cwd: 'dist/', + expand: true, + src: ['fuelux/**'] + }], + options: { + archive: 'dist/fuelux.zip', + mode: 'zip' + } + } +}; \ No newline at end of file diff --git a/grunt/config/concat.js b/grunt/config/concat.js new file mode 100644 index 000000000..7b62f5d63 --- /dev/null +++ b/grunt/config/concat.js @@ -0,0 +1,38 @@ +module.exports = function (grunt) { + + return { + fuelux: { + src: ['js/checkbox.js', + 'js/combobox.js', + 'js/datepicker.js', + 'js/dropdown-autoflip.js', + 'js/loader.js', + 'js/placard.js', + 'js/radio.js', + 'js/search.js', + 'js/selectlist.js', + 'js/spinbox.js', + 'js/tree.js', + 'js/wizard.js', + 'js/infinite-scroll.js', + 'js/pillbox.js', + 'js/repeater.js', + 'js/repeater-list.js', + 'js/repeater-thumbnail.js', + 'js/scheduler.js' + ], + dest: 'dist/js/' + '<%= pkg.name %>' + '.js' + }, + options: { + banner: '<%= banner %>' + '\n\n// For more information on UMD visit: https://github.com/umdjs/umd/\n(function (factory) {\n\tif (typeof define === \'function\' && define.amd) {\n\t\tdefine([\'jquery\', \'bootstrap\'], factory);\n\t} else {\n\t\tfactory(jQuery);\n\t}\n}(function (jQuery) {\n\n<%= jqueryCheck %><%= bootstrapCheck %>', + footer: '\n}));', + process: function (source) { + source = '(function ($) {\n\n' + + source.replace(/\/\/ -- BEGIN UMD WRAPPER PREFACE --(\n|.)*\/\/ -- END UMD WRAPPER PREFACE --/g, ''); + source = source.replace(/\/\/ -- BEGIN UMD WRAPPER AFTERWORD --(\n|.)*\/\/ -- END UMD WRAPPER AFTERWORD --/g, '') + '\n})(jQuery);\n\n'; + return source; + } + } + } + +}; \ No newline at end of file diff --git a/grunt/config/connect.js b/grunt/config/connect.js new file mode 100644 index 000000000..f13fd0ec6 --- /dev/null +++ b/grunt/config/connect.js @@ -0,0 +1,32 @@ +module.exports = function (grunt) { + + return { + server: { + options: { + hostname: '*', + base: { + path: '.', + options: { + index: ['index.html', 'tests.html'] + } + }, + port: 8000, + useAvailablePort: true + } + }, + testServer: { + options: { + base: { + path: '.', + options: { + index: ['index.html', 'tests.html'] + } + }, + hostname: '*', + port: '<%= connectTestServerOptionsPort %>', + useAvailablePort: true + } + } + } + +}; \ No newline at end of file diff --git a/grunt/config/copy.js b/grunt/config/copy.js new file mode 100644 index 000000000..8c1d7afd8 --- /dev/null +++ b/grunt/config/copy.js @@ -0,0 +1,15 @@ +module.exports = { + fonts: { + cwd: 'fonts/', + dest: 'dist/fonts/', + expand: true, + filter: 'isFile', + src: ['*'] + }, + zipsrc: { + cwd: 'dist/', + dest: 'dist/fuelux/', + expand: true, + src: ['**'] + } +}; \ No newline at end of file diff --git a/grunt/config/htmllint.js b/grunt/config/htmllint.js new file mode 100644 index 000000000..9ffb0d0a7 --- /dev/null +++ b/grunt/config/htmllint.js @@ -0,0 +1,11 @@ +module.exports = { + options: { + ignore:[ + 'Section lacks heading. Consider using “h2”-“h6” elements to add identifying headings to all sections.', + 'Bad value X-UA-Compatible for attribute http-equiv on element meta.', + 'Element head is missing a required instance of child element title.' + ], + force: true + }, + src: ['index.html', 'markup/*.html', 'test/markup/*.html'] +}; \ No newline at end of file diff --git a/grunt/config/jsbeautifier.js b/grunt/config/jsbeautifier.js new file mode 100644 index 000000000..0c9ee066b --- /dev/null +++ b/grunt/config/jsbeautifier.js @@ -0,0 +1,23 @@ +module.exports = { + files: ['dist/js/fuelux.js'], + options: { + js: { + braceStyle: 'collapse', + breakChainedMethods: false, + e4x: false, + evalCode: false, + indentLevel: 0, + indentSize: 4, + indentWithTabs: true, + jslintHappy: false, + keepArrayIndentation: false, + keepFunctionIndentation: false, + maxPreserveNewlines: 10, + preserveNewlines: true, + spaceBeforeConditional: true, + spaceInParen: true, + unescapeStrings: false, + wrapLineLength: 0 + } + } +}; \ No newline at end of file diff --git a/grunt/config/jshint.js b/grunt/config/jshint.js new file mode 100644 index 000000000..b99bb15f4 --- /dev/null +++ b/grunt/config/jshint.js @@ -0,0 +1,33 @@ +module.exports = { + options: { + boss: true, + browser: true, + curly: false, + eqeqeq: true, + eqnull: true, + globals: { + jQuery: true, + define: true, + require: true, + module: true + }, + immed: true, + latedef: true, + newcap: true, + noarg: true, + sub: true, + undef: true, + unused: false + }, + sourceAndDist: ['Gruntfile.js', 'js/*.js', 'dist/fuelux.js'], + tests: { + options: { + latedef: false, + undef: false, + unused: false + }, + files: { + src: ['test/**/*.js', '!test/commonjs-bundle.js'] + } + } +}; \ No newline at end of file diff --git a/grunt/config/less.js b/grunt/config/less.js new file mode 100644 index 000000000..496215b67 --- /dev/null +++ b/grunt/config/less.js @@ -0,0 +1,47 @@ +module.exports = function (grunt) { + + var minifiedOutput = 'dist/css/' + '<%= pkg.name %>' + '.min.css'; + + var minifyFiles = function() { + var minifyFiles = {}; + var output = 'dist/css/' + '<%= pkg.name %>' + '.min.css' + minifyFiles[output] = 'dist/css/' + '<%= pkg.name %>' + '.css' + return minifyFiles + }; + + return { + dev: { + options: { + strictMath: true, + sourceMap: true, + outputSourceFiles: true, + sourceMapURL: grunt.config('pkg.name') + '-dev.css.map', + sourceMapFilename: 'dist/css/' + '<%= pkg.name %>' + '-dev.css.map' + }, + files: { + 'dist/css/fuelux-dev.css': 'less/fuelux.less' + } + }, + dist: { + options: { + strictMath: true, + sourceMap: true, + outputSourceFiles: true, + sourceMapURL: '<%= pkg.name %>' + '.css.map', + sourceMapFilename: 'dist/css/' + '<%= pkg.name %>' + '.css.map' + }, + files: { + 'dist/css/fuelux.css': 'less/fuelux.less' + } + }, + minify: { + options: { + cleancss: true, + compress: true, + report: 'min' + }, + files: minifyFiles() + } + } + +}; \ No newline at end of file diff --git a/grunt/config/prompt.js b/grunt/config/prompt.js new file mode 100644 index 000000000..b86658672 --- /dev/null +++ b/grunt/config/prompt.js @@ -0,0 +1,219 @@ +module.exports = function (grunt) { + var semver = require('semver'); + var packageVersion = require('../../package.json').version; + + return { + // asks for what version you want to build + 'tempbranch': { + options: { + questions: [ + { + config: 'release.remoteRepository', + default : '<%= release.remoteRepository %>', + type: 'input', + message: function() { + return 'What repository would like to base your local release branch from?'; + } + }, + { + // Assumption is made that you are releasing the code within a "release branch" currently + // on the upstream remote repo. This branch will be tracked locally and be used to run + // the build process in. It will be named release_{BUILD_VERSION}_{MMSS} (that is, it will + // use the version specified earlier and a "mini-timestamp" of the current hour and minute). + config: 'release.remoteBaseBranch', + type: 'input', + default : '<%= release.remoteBaseBranch %>', + message: function() { + return 'What remote branch from ' + grunt.config('release.remoteRepository') + + ' would like to build your release based on?'; + } + } + ] + } + }, + // asks for what version you want to build + 'build': { + options: { + questions: [ + { + config: 'release.buildSemVerType', + type: 'list', + message: 'What would you like to do?', + choices: [ + { + value: 'patch', + name: 'Patch: ' + semver.inc(packageVersion, 'patch') + ' Backwards-compatible bug fixes.' + }, + { + value: 'minor', + name: 'Minor: ' + semver.inc(packageVersion, 'minor') + ' Add functionality in a backwards-compatible manner.' + }, + { + value: 'major', + name: 'Major: ' + semver.inc(packageVersion, 'major') + ' Incompatible API changes.' + }, + { + value: 'custom', + name: 'Custom: ?.?.? Specify version...' + } + ] + }, + { + // if custom bump is used with a specific version, see dorelease task + config: 'release.buildSemVerType', + type: 'input', + message: 'What specific version would you like', + when: function (answers) { + return answers['release.buildSemVerType'] === 'custom'; + }, + validate: function (value) { + var valid = semver.valid(value); + return valid || 'Must be a valid semver, such as 1.2.3-rc1. See http://semver.org/ for more details.'; + } + } + ] + } + }, + 'commit': { + options: { + questions: [ + { + config: 'release.commit', + type: 'confirm', + message: 'Please review your files. Would you like to commit?' + } + ], + then: function (answers, done) { + if (answers['release.commit'] === true) { + grunt.task.run(['shell:commit']); + } + return false; + } + } + }, + 'tag': { + options: { + questions: [ + { + config: 'release.tag', + type: 'confirm', + message: 'Would you like to tag as ' + packageVersion + '?' + } + ], + then: function (answers, done) { + if (answers['release.tag'] === true) { + grunt.task.run(['shell:tag']); + } + return false; + } + } + }, + 'pushLocalBranchToUpstream': { + options: { + questions: [ + { + config: 'release.remoteDestinationBranch', + type: 'input', + message: function() { + return 'What upstream branch would you like to push ' + grunt.config('release.localBranch') + + ' to (probably ' + grunt.config('release.remoteDestinationBranch') + ')? (leave blank to skip)'; + } + } + ], + then: function (answers, done) { + if (answers['release.remoteDestinationBranch'] !== '' && answers['release.remoteDestinationBranch'] !== 'n' ) { + grunt.task.run(['shell:pushLocalBranchToUpstream']); + } + return false; + } + } + }, + 'pushTagToUpstream': { + options: { + questions: [ + { + config: 'release.upstreamTag', + type: 'confirm', + message: 'Would you like to push tag ' + packageVersion + ' to upstream?' + } + ], + then: function (answers, done) { + if (answers['release.upstreamTag'] === true) { + grunt.task.run(['shell:pushTagToUpstream']); + } + return false; + } + } + }, + 'uploadToCDN': { + options: { + questions: [ + { + config: 'release.uploadToCDN', + type: 'confirm', + message: 'Would you like to upload the `dist folder to fuelcdn.com?' + } + ], + then: function (answers, done) { + if (answers['release.uploadToCDN'] === true) { + grunt.task.run(['shell:uploadToCDN']); + } + return false; + } + } + }, + 'pushLocalBranchToUpstreamMaster': { + options: { + questions: [ + { + config: 'release.pushToUpstreamMaster', + type: 'confirm', + message: 'Would you like to push your local release branch to upstream\'s master branch?' + } + ], + then: function (answers, done) { + if (answers['release.pushToUpstreamMaster'] === true) { + grunt.task.run(['shell:pushLocalBranchToUpstreamMaster']); + } + return false; + } + } + }, + 'deleteLocalReleaseBranch': { + options: { + questions: [ + { + config: 'release.deleteLocalReleaseBranch', + type: 'confirm', + message: function() { + return 'Would you like to delete your local release branch' + '?'; + } + } + ], + then: function (answers, done) { + if (answers['release.deleteLocalReleaseBranch'] === true) { + grunt.task.run(['shell:deleteLocalReleaseBranch']); + } + return false; + } + } + }, + 'publishToNPM': { + options: { + questions: [ + { + config: 'release.publishToNPM', + type: 'confirm', + message: 'Would you like to publish to NPM?' + } + ], + then: function (answers, done) { + if (answers['release.publishToNPM'] === true) { + grunt.task.run(['shell:publishToNPM']); + } + return false; + } + } + } + } +} \ No newline at end of file diff --git a/grunt/config/qunit.js b/grunt/config/qunit.js new file mode 100644 index 000000000..4127907ec --- /dev/null +++ b/grunt/config/qunit.js @@ -0,0 +1,40 @@ +module.exports = function (grunt) { + + return { + release: { + options: { + urls: grunt.config('allTestUrls'), + screenshot: true, + page: { + viewportSize: { + width: 1280, + height: 1024 + } + } + } + }, + globals: { + options: { + urls: ['http://localhost:' + '<%= connectTestServerOptionsPort %>' + '/test/browser-globals.html'] + } + }, + noMoment: { + options: { + urls: ['http://localhost:' + '<%= connectTestServerOptionsPort %>' + '/test/?no-moment=true'] + } + }, + source: { + options: { + urls: ['http://localhost:' + '<%= connectTestServerOptionsPort %>' + '/test/'] + } + }, + dist: { + options: { + urls: ['http://localhost:' + '<%= connectTestServerOptionsPort %>' + '/test/?testdist=true', + 'http://localhost:' + '<%= connectTestServerOptionsPort %>' + '/test/commonjs.html' + ] + } + } + } + +}; \ No newline at end of file diff --git a/grunt/config/replace.js b/grunt/config/replace.js new file mode 100644 index 000000000..6e1a7b304 --- /dev/null +++ b/grunt/config/replace.js @@ -0,0 +1,18 @@ +module.exports = function (grunt) { + + function getPackage() { + return grunt.file.readJSON('./package.json'); + } + + return { + readme: { + src: ['DETAILS.md', 'README.md'], + overwrite: true, + replacements: [{ + from: /fuelux\/\d\.\d\.\d/g, + to: 'fuelux/' + getPackage().version + }] + } + } + +}; \ No newline at end of file diff --git a/grunt/config/saucelabs-qunit.js b/grunt/config/saucelabs-qunit.js new file mode 100644 index 000000000..fa2909951 --- /dev/null +++ b/grunt/config/saucelabs-qunit.js @@ -0,0 +1,48 @@ +module.exports = function (grunt) { + function getPackage() { + return grunt.file.readJSON('./package.json'); + } + + return { + trickyBrowsers: { + options: { + username: '<%= sauceUser %>', + key: '<%= sauceKey %>', + tunnelTimeout: 45, + testInterval: 3000, + tags: ['<%= sauceUser %>' + '@' + process.env.TRAVIS_BRANCH || '<%= sauceUser %>' + '@local'], + browsers: grunt.file.readYAML('sauce_browsers_tricky.yml'), + build: process.env.TRAVIS_BUILD_NUMBER || getPackage().version, + testname: process.env.TRAVIS_JOB_ID || Math.floor((new Date()).getTime() / 1000 - 1230768000).toString(), + urls: ['http://localhost:<%= connect.testServer.options.port %>/test/?testdist=true'] + } + }, + defaultBrowsers: { + options: { + username: '<%= sauceUser %>', + key: '<%= sauceKey %>', + tunnelTimeout: 45, + testInterval: 3000, + tags: [getPackage().version, '<%= sauceUser %>' + '@' + process.env.TRAVIS_BRANCH || '<%= sauceUser %>@local'], + browsers: grunt.file.readYAML('sauce_browsers.yml'), + build: process.env.TRAVIS_BUILD_NUMBER || getPackage().version, + testname: process.env.TRAVIS_JOB_ID || getPackage().version + '-<%= grunt.template.today("dddd, mmmm dS, yyyy, h:MM:ss TT") %>', + urls: ['http://localhost:<%= connect.testServer.options.port %>/test/?testdist=true'], + maxPollRetries: 4, + throttled: 3, + maxRetries: 3 + } + }, + all: { + options: { + username: '<%= sauceUser %>', + key: '<%= sauceKey %>', + browsers: grunt.file.readYAML('sauce_browsers.yml'), + build: process.env.TRAVIS_BUILD_NUMBER || '<%= pkg.version %>', + testname: 'grunt-<%= grunt.template.today("dddd, mmmm dS, yyyy, h:MM:ss TT") %>', + urls: '<%= allTestUrls %>' + } + } + } + +}; \ No newline at end of file diff --git a/grunt/config/shell.js b/grunt/config/shell.js new file mode 100644 index 000000000..d8f27e954 --- /dev/null +++ b/grunt/config/shell.js @@ -0,0 +1,101 @@ +module.exports = function (grunt) { + var semver = require('semver'); + + function getPackage() { + return grunt.file.readJSON('./package.json'); + } + + return { + // Compile release notes while waiting for tests to pass. Needs Ruby gem and ONLY LOOKS AT THE REMOTE NAMED ORIGIN. + // Install with: gem install github_changelog_generator + notes: { + command: 'github_changelog_generator --no-author --unreleased-only --compare-link' + }, + checkoutRemoteReleaseBranch: { + // this makes a local branch based on the prior prompt, such as release_{TIMESTAMP} + // then update tags from remote in order to prevent duplicate tags + command: function() { + grunt.config('release.localBranch', 'release_' + new Date().getTime() ); + var command = [ + 'git checkout -b ' + grunt.config('release.localBranch') + ' ' + + grunt.config('release.remoteRepository') + '/' + grunt.config('release.remoteBaseBranch'), + 'git fetch ' + grunt.config('release.remoteRepository') + ' --tag' + ].join(' && '); + grunt.log.write('Checking out new local branch based on ' + grunt.config('release.remoteBaseBranch') + ': ' + command); + return command; + } + }, + addReleaseFiles: { + command: function() { + var command = 'git add ' + grunt.config('release.files').join(' '); + grunt.log.write('Staging: ' + command); + return command; + } + }, + commit: { + command: function() { + var command = 'git commit -m "release ' + getPackage().version + '"'; + grunt.log.write('Committing: ' + command); + return command; + } + }, + tag: { + command: function() { + var command = 'git tag -a "' + getPackage().version + '" -m "' + getPackage().version + '"'; + grunt.log.write('Tagging: ' + command); + return command; + } + }, + pushLocalBranchToUpstream: { + command: function() { + var command = 'git push ' + grunt.config('release.remoteRepository') + ' ' + + grunt.config('release.localBranch') + ':' + grunt.config('release.remoteDestinationBranch'); + grunt.log.write('Pushing: ' + command); + return command; + } + }, + pushTagToUpstream: { + command: function() { + var command = 'git push ' + grunt.config('release.remoteRepository') + ' ' + packageVersion; + grunt.log.write('Publishing tag: ' + command); + return command; + } + }, + pushLocalBranchToUpstreamMaster: { + command: function() { + var command = 'git push ' + grunt.config('release.remoteRepository') + ' ' + + grunt.config('release.localBranch') + ':master'; + grunt.log.write(command); + return command; + } + }, + uploadToCDN: { + command: function() { + + function createUploadCommand(version) { + return ['mv dist ' + version, + 'scp -i ~/.ssh/fuelcdn -r "' + version + '"/ ' + + '<%= cdnLoginFile.user %>' + '@' + '<%= cdnLoginFile.server %>' + ':' + '<%= cdnLoginFile.folder %>', + 'mv "' + version + '" dist', + 'echo "Done uploading files."'].join(' && '); + } + var command = [ + packageVersion, + semver.major(packageVersion) + '.' + semver.minor(packageVersion), + semver.major(packageVersion) + ].map(createUploadCommand).join(' && '); + grunt.log.write('Uploading: ' + command); + grunt.log.writeln(''); + return command; + } + }, + publishToNPM: { + command: function() { + var command = 'npm publish'; + grunt.log.write(command); + return command; + } + } + } + +} \ No newline at end of file diff --git a/grunt/config/uglify.js b/grunt/config/uglify.js new file mode 100644 index 000000000..d5d348323 --- /dev/null +++ b/grunt/config/uglify.js @@ -0,0 +1,16 @@ +module.exports = function (grunt) { + + return { + options: { + report: 'min' + }, + fuelux: { + options: { + banner: '<%= banner %>' + }, + src: 'dist/js/<%= pkg.name %>.js', + dest: 'dist/js/<%= pkg.name %>.min.js' + } + } + +}; \ No newline at end of file diff --git a/grunt/config/usebanner.js b/grunt/config/usebanner.js new file mode 100644 index 000000000..7df1450e2 --- /dev/null +++ b/grunt/config/usebanner.js @@ -0,0 +1,13 @@ +module.exports = { + dist: { + options: { + position: 'top', + banner: '<%= banner %>' + }, + files: { + src: ['dist/css/<%= pkg.name %>.css', + 'dist/css/<%= pkg.name %>.min.css' + ] + } + } +}; \ No newline at end of file diff --git a/grunt/config/watch.js b/grunt/config/watch.js new file mode 100644 index 000000000..fe6f1ff03 --- /dev/null +++ b/grunt/config/watch.js @@ -0,0 +1,84 @@ +module.exports = { + full: { + files: ['Gruntfile.js', + 'fonts/**', + 'grunt/**', + 'js/**', + 'less/**', + 'test/**', + 'index.html', + 'dev.html' + ], + options: { + livereload: true + }, + tasks: ['jshint', + 'blanket_qunit:source', + 'qunit:noMoment', + 'qunit:globals', + 'dist', + 'qunit:dist', + 'htmllint' + ] + }, + source: { + files: ['Gruntfile.js', + 'fonts/**', + 'grunt/**', + 'js/**', + 'less/**', + 'test/**', + 'index.html', + 'dev.html' + ], + options: { + livereload: true + }, + tasks: ['jshint', + 'connect:testServer', + 'blanket_qunit:source', + 'qunit:noMoment', + 'qunit:globals', + 'htmllint' + ] + }, + less: { + files: ['fonts/**', + 'less/**'], + options: { + livereload: true + }, + tasks: ['distcss'] + }, + cssdev: { + files: ['Gruntfile.js', + 'grunt/**', + 'less/**', + 'index.html', + 'index-dev.html', + 'dev.html', + '!less/fuelux-no-namespace.less' + ], + options: { + livereload: true + }, + tasks: ['distcssdev'] + }, + dist: { + files: ['fonts/**', + 'grunt/**', + 'js/**', + 'less/**'], + options: { + livereload: true + }, + tasks: ['dist'] + }, + lite: { + files: [], + options: { + livereload: true + }, + tasks: [] + } +}; \ No newline at end of file diff --git a/grunt/commonjs-reference-module.js b/grunt/other/commonjs-reference-module.js similarity index 90% rename from grunt/commonjs-reference-module.js rename to grunt/other/commonjs-reference-module.js index f340c4244..65d4666aa 100644 --- a/grunt/commonjs-reference-module.js +++ b/grunt/other/commonjs-reference-module.js @@ -18,13 +18,14 @@ var banner = '// This file has been created by the `commonjs` Grunt task.' + module.exports = function createBundledReferenceModule(grunt, files, destFile) { var destDir = path.dirname(destFile); + var files = files || []; function srcPathToDestRequire(files) { var requirePath = path.relative(destDir, files).replace(/\\/g, '/').replace(/\.js$/g, ''); return 'require(\'' + requirePath + '\');'; } - var moduleOutputJs = banner + files.map(srcPathToDestRequire).join('\n'); + var moduleOutputJs = banner + String(files.map(srcPathToDestRequire).join('\n')); try { fs.writeFileSync(destFile, moduleOutputJs); } catch (err) { diff --git a/grunt/tasks/build.js b/grunt/tasks/build.js new file mode 100644 index 000000000..9a0710e8f --- /dev/null +++ b/grunt/tasks/build.js @@ -0,0 +1,34 @@ +module.exports = function(grunt) { + + var commonJSBundledReferenceModule = require('../other/commonjs-reference-module.js'); + + /* ------------- + BUILD + ------------- */ + // JS distribution task + grunt.registerTask('distjs', 'concat, uglify', ['concat', 'uglify', 'jsbeautifier']); + + // CSS distribution task + grunt.registerTask('distcss', 'Compile LESS into CSS', ['less:dist', 'less:minify', 'usebanner']); + + // CSS distribution task (dev) + grunt.registerTask('distcssdev', 'Compile LESS into the dev CSS', [ 'less:dev']); + + // ZIP distribution task + grunt.registerTask('distzip', 'Compress and zip "dist"', ['copy:zipsrc', 'compress', 'clean:zipsrc']); + + // create dist/js/npm.js + grunt.registerTask('commonjs', 'Create CommonJS "bundled reference" module in `dist`', function () { + var files = grunt.config.get('concat.fuelux.src'); + var bundledReferenceFile = 'dist/js/npm.js'; + + // console.log(grunt.config.get('concat.fuelux.src')); + commonJSBundledReferenceModule(grunt, files, bundledReferenceFile); + }); + + // Full distribution task + grunt.registerTask('dist', 'Build "dist." Contributors: do not commit "dist."', + ['clean:dist', 'distcss', 'copy:fonts', 'distjs', 'commonjs', 'distzip']); + + +}; \ No newline at end of file diff --git a/grunt/tasks/release.js b/grunt/tasks/release.js new file mode 100644 index 000000000..8cbe909a2 --- /dev/null +++ b/grunt/tasks/release.js @@ -0,0 +1,68 @@ +module.exports = function(grunt) { + var packageVersion = require('../../package.json').version; + + /* ------------- + RELEASE + ------------- */ + grunt.registerTask('notes', 'Run a ruby gem that will request from Github unreleased pull requests', ['shell:notes']); + + grunt.registerTask('updateRelease', '', function () { + packageVersion = require('./package.json').version; + }); + + // Maintainers: Run prior to a release. Includes SauceLabs VM tests. + grunt.registerTask('release', 'Release a new version, push it and publish it', function() { + + // default variables for release task + var releaseDefaults = { + release: { + files: ['dist', 'README.md', 'DETAILS.md', 'bower.json', 'package.json'], + localBranch: 'release', + remoteBaseBranch: 'master', + remoteDestinationBranch: '3.x', + remoteRepository: 'upstream' + } + }; + + // add releaseDefaults + grunt.config.merge(releaseDefaults); + + if ( typeof grunt.config('sauceLoginFile') === 'undefined' ) { + grunt.log.write('The file SAUCE_API_KEY.yml is needed in order to run tests in SauceLabs.'['red'].bold + + ' Please contact another maintainer to obtain this file.'); + } + + if ( typeof grunt.config('cdnLoginFile') === 'undefined' ) { + grunt.log.write('The file FUEL_CDN.yml is needed in order to upload the release files to the CDN.'['red'].bold + + ' Please contact another maintainer to obtain this file.'); + } + + // update local variable to make sure build prompt is using temp branch's package version + grunt.task.run(['prompt:tempbranch', 'shell:checkoutRemoteReleaseBranch', + 'updateRelease', 'prompt:build', 'dorelease']); + }); + +// formerally dorelease task + grunt.registerTask('dorelease', '', function () { + + grunt.log.writeln(''); + + if (!grunt.option('no-tests')) { + grunt.task.run(['releasetest']); //If phantom timeouts happening because of long-running qunit tests, look into setting `resourceTimeout` in phantom: http://phantomjs.org/api/webpage/property/settings.html + // Delete any screenshots that may have happened if it got this far. This isn't foolproof + // because it relies on the phantomjs server/page timeout, which can take longer than this + // grunt task depending on how long saucelabs takes to run... + grunt.task.run('clean:screenshots'); + } + + grunt.config('banner', '<%= bannerRelease %>'); + + // Run dist again to grab the latest version numbers. Yeah, we're running it twice... ¯\_(ツ)_/¯ + grunt.task.run(['bump-only:' + grunt.config('release.buildSemVerType'), 'replace:readme', 'dist', + 'shell:addReleaseFiles', 'prompt:commit', 'prompt:tag', 'prompt:pushLocalBranchToUpstream', + 'prompt:pushTagToUpstream', 'prompt:uploadToCDN', 'prompt:pushLocalBranchToUpstreamMaster', 'publishToNPM']); + }); + + + +}; \ No newline at end of file diff --git a/grunt/tasks/serve.js b/grunt/tasks/serve.js new file mode 100644 index 000000000..314e20e77 --- /dev/null +++ b/grunt/tasks/serve.js @@ -0,0 +1,72 @@ +module.exports = function(grunt) { + + /* ------------- + SERVE + ------------- */ + // default serve task that runs tests and builds and tests dist by default. + grunt.registerTask('serve', 'Test, build, serve files. (~20s)', function () { + var tasks = ['test', 'servedist']; + grunt.task.run(tasks); + }); + + // serve task that runs tests and builds and tests dist by default (~20s). + grunt.registerTask('serveslow', 'Serve files. Run all tests. Does not build. (~20s)', function () { + var tasks = ['connect:server', 'test', 'watch:source']; + grunt.task.run(tasks); + }); + + //Fastest serve command for freely slinging code (no tests will run by default). + grunt.registerTask('servefast', 'Serve the files (no watch), --test to run minimal tests. (~0s)', function () { + grunt.task.run(['connect:server']); + + if (grunt.option('test')) { + grunt.task.run(['connect:testServer', 'qunit:source', 'watch:source']); + } else { + grunt.task.run(['watch:lite']); + } + }); + + // Fastest serve command when you're working on LESS + grunt.registerTask('serveless', 'Compile LESS and serve the files. pass --tests to run test. (~3s)', function () { + grunt.task.run(['distcss']); + + if (grunt.option('test')) { + // add qunit:source as a watch task for watch:less since they want tests + grunt.config.merge({ + watch: { + less: { + tasks: ['qunit:source'] + } + } + }); + grunt.task.run(['qunit:source']); + } + + grunt.task.run(['connect:server', 'watch:less']); + }); + + // Complies the less files into the -dev versions, does not overwrite the main css files. + grunt.registerTask('servedev', 'Serve the files with no "dist" build or tests. Optional --no-less to also disable compiling less into css.', function() { + if (! grunt.option('no-less') ) { + grunt.task.run(['distcssdev']); + } + grunt.task.run(['connect:server', 'watch:cssdev']); + }); + + // same as `grunt serve` but tests default to being off + grunt.registerTask('servedist', 'Compile and serve everything, pass --test to run tests. (~7s)', function () { + grunt.task.run(['dist']); + + //start up the servers here so we can run tests if appropriate + grunt.task.run(['connect:server']); + grunt.task.run(['connect:testServer']); + + if (grunt.option('test')) { + grunt.task.run(['browserify:commonjs','qunit:dist', 'watch:full']); + } else { + grunt.task.run(['watch:dist']); + } + }); + + +}; \ No newline at end of file diff --git a/grunt/tasks/test.js b/grunt/tasks/test.js new file mode 100644 index 000000000..6f3508c1a --- /dev/null +++ b/grunt/tasks/test.js @@ -0,0 +1,41 @@ +module.exports = function(grunt) { + + /* ------------- + TESTS + ------------- */ + + // to be run prior to submitting a PR + grunt.registerTask('test', 'run jshint, qunit source w/ coverage, and validate HTML', + ['jshint', 'connect:testServer', 'blanket_qunit:source', 'qunit:noMoment', 'qunit:globals', 'htmllint']); + + //If qunit:source is working but qunit:full is breaking, check to see if the dist broke the code. This would be especially useful if we start mangling our code, but, is 99.99% unlikely right now + grunt.registerTask('validate-dist', 'run qunit:source, dist, and then qunit:full', + ['connect:testServer', 'qunit:source', 'dist', 'browserify:commonjs', 'qunit:dist']); + + // multiple jQuery versions, then run SauceLabs VMs + grunt.registerTask('releasetest', 'run jshint, build dist, all source tests, validation, and qunit on SauceLabs', + ['test', 'dist', 'browserify:commonjs', 'qunit:dist', 'saucelabs-qunit:defaultBrowsers']); + + // can be run locally instead of through TravisCI, but requires the Fuel UX Saucelabs API key file which is not public at this time. + grunt.registerTask('saucelabs', 'run jshint, and qunit on saucelabs', + ['connect:testServer', 'jshint', 'saucelabs-qunit:defaultBrowsers']); + + // can be run locally instead of through TravisCI, but requires the FuelUX Saucelabs API key file which is not public at this time. + grunt.registerTask('trickysauce', 'run tests, jshint, and qunit for "tricky browsers" (IE8-11)', + ['connect:testServer', 'jshint', 'saucelabs-qunit:trickyBrowsers']); + + // Travis CI task. This task no longer uses SauceLabs. Please run 'grunt saucelabs' manually. + grunt.registerTask('travisci', 'Tests to run when in Travis CI environment', + ['test', 'dist', 'browserify:commonjs', 'qunit:dist']); + + // if you've already accidentally added your files for commit, this will at least unstage them. If you haven't, this will wipe them out. + grunt.registerTask('resetdist', 'resets changes to dist to keep them from being checked in', function () { + //default resetdist to true... basically. + if (typeof grunt.option('resetdist') === "undefined" || grunt.option('resetdist')) { + var exec = require('child_process').exec; + exec('git reset HEAD dist/*'); + exec('git checkout -- dist/*'); + } + }); + +}; \ No newline at end of file diff --git a/package.json b/package.json index a2a7ecfd0..5aa1b06ce 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "grunt-text-replace": "0.x", "grunt-umd": "2.x", "grunt-zip": "0.x", + "load-grunt-config": "0.x", "load-grunt-tasks": "3.x", "qunitjs": "1.x", "semver": "5.x",