/** * * Material Design Lite * Copyright 2015 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License * */ // jscs:disable jsDoc 'use strict'; // Include Gulp & Tools We'll Use import fs from 'fs'; import path from 'path'; import mergeStream from 'merge-stream'; import del from 'del'; import vinylPaths from 'vinyl-paths'; import runSequence from 'run-sequence'; import browserSync from 'browser-sync'; import through from 'through2'; import swig from 'swig'; import gulp from 'gulp'; import closureCompiler from 'gulp-closure-compiler'; import gulpLoadPlugins from 'gulp-load-plugins'; import uniffe from './utils/uniffe.js'; import pkg from './package.json'; const $ = gulpLoadPlugins(); const reload = browserSync.reload; const hostedLibsUrlPrefix = 'https://code.getmdl.io'; const templateArchivePrefix = 'mdl-template-'; const bucketProd = 'gs://www.getmdl.io'; const bucketStaging = 'gs://mdl-staging'; const bucketCode = 'gs://code.getmdl.io'; const banner = ['/**', ' * <%= pkg.name %> - <%= pkg.description %>', ' * @version v<%= pkg.version %>', ' * @license <%= pkg.license %>', ' * @copyright 2015 Google, Inc.', ' * @link https://github.com/google/material-design-lite', ' */', ''].join('\n'); let codeFiles = ''; const AUTOPREFIXER_BROWSERS = [ 'ie >= 10', 'ie_mob >= 10', 'ff >= 30', 'chrome >= 34', 'safari >= 7', 'opera >= 23', 'ios >= 7', 'android >= 4.4', 'bb >= 10' ]; const SOURCES = [ // Component handler 'src/mdlComponentHandler.js', // Polyfills/dependencies 'src/third_party/**/*.js', // Base components 'src/button/button.js', 'src/checkbox/checkbox.js', 'src/icon-toggle/icon-toggle.js', 'src/menu/menu.js', 'src/progress/progress.js', 'src/radio/radio.js', 'src/slider/slider.js', 'src/snackbar/snackbar.js', 'src/spinner/spinner.js', 'src/switch/switch.js', 'src/tabs/tabs.js', 'src/textfield/textfield.js', 'src/tooltip/tooltip.js', // Complex components (which reuse base components) 'src/layout/layout.js', 'src/data-table/data-table.js', // And finally, the ripples 'src/ripple/ripple.js' ]; // ***** Development tasks ****** // // Lint JavaScript gulp.task('lint', () => { return gulp.src([ 'src/**/*.js', 'gulpfile.babel.js' ]) .pipe(reload({stream: true, once: true})) .pipe($.jshint()) .pipe($.jscs()) .pipe($.jshint.reporter('jshint-stylish')) .pipe($.jscs.reporter()) .pipe($.if(!browserSync.active, $.jshint.reporter('fail'))) .pipe($.if(!browserSync.active, $.jscs.reporter('fail'))); }); // ***** Production build tasks ****** // // Optimize Images // TODO: Update image paths in final CSS to match root/images gulp.task('images', () => { return gulp.src('src/**/*.{svg,png,jpg}') .pipe($.flatten()) .pipe($.cache($.imagemin({ progressive: true, interlaced: true }))) .pipe(gulp.dest('dist/images')) .pipe($.size({title: 'images'})); }); // Compile and Automatically Prefix Stylesheets (dev) gulp.task('styles:dev', () => { return gulp.src('src/**/*.scss') .pipe($.sass({ precision: 10, onError: console.error.bind(console, 'Sass error:') })) .pipe($.cssInlineImages({ webRoot: 'src' })) .pipe($.autoprefixer(AUTOPREFIXER_BROWSERS)) .pipe(gulp.dest('.tmp/styles')) .pipe($.size({title: 'styles'})); }); // Compile and Automatically Prefix Stylesheet Templates (production) gulp.task('styletemplates', () => { // For best performance, don't add Sass partials to `gulp.src` return gulp.src('src/template.scss') // Generate Source Maps .pipe($.sourcemaps.init()) .pipe($.sass({ precision: 10, onError: console.error.bind(console, 'Sass error:') })) .pipe($.cssInlineImages({webRoot: 'src'})) .pipe($.autoprefixer(AUTOPREFIXER_BROWSERS)) .pipe(gulp.dest('.tmp')) // Concatenate Styles .pipe($.concat('material.css.template')) .pipe(gulp.dest('dist')) // Minify Styles .pipe($.if('*.css.template', $.csso())) .pipe($.concat('material.min.css.template')) .pipe($.header(banner, {pkg})) .pipe($.sourcemaps.write('.')) .pipe(gulp.dest('dist')) .pipe($.size({title: 'styles'})); }); // Compile and Automatically Prefix Stylesheets (production) gulp.task('styles', () => { // For best performance, don't add Sass partials to `gulp.src` return gulp.src('src/material-design-lite.scss') // Generate Source Maps .pipe($.sourcemaps.init()) .pipe($.sass({ precision: 10, onError: console.error.bind(console, 'Sass error:') })) .pipe($.cssInlineImages({webRoot: 'src'})) .pipe($.autoprefixer(AUTOPREFIXER_BROWSERS)) .pipe(gulp.dest('.tmp')) // Concatenate Styles .pipe($.concat('material.css')) .pipe($.header(banner, {pkg})) .pipe(gulp.dest('dist')) // Minify Styles .pipe($.if('*.css', $.csso())) .pipe($.concat('material.min.css')) .pipe($.header(banner, {pkg})) .pipe($.sourcemaps.write('.')) .pipe(gulp.dest('dist')) .pipe($.size({title: 'styles'})); }); // Only generate CSS styles for the MDL grid gulp.task('styles-grid', () => { return gulp.src('src/material-design-lite-grid.scss') .pipe($.sass({ precision: 10, onError: console.error.bind(console, 'Sass error:') })) .pipe($.autoprefixer(AUTOPREFIXER_BROWSERS)) .pipe(gulp.dest('.tmp')) // Concatenate Styles .pipe($.concat('material-grid.css')) .pipe($.header(banner, {pkg})) .pipe(gulp.dest('dist')) // Minify Styles .pipe($.if('*.css', $.csso())) .pipe($.concat('material-grid.min.css')) .pipe($.header(banner, {pkg})) .pipe(gulp.dest('dist')) .pipe($.size({title: 'styles-grid'})); }); // Build with Google's Closure Compiler, requires Java 1.7+ installed. gulp.task('closure', () => { return gulp.src(SOURCES) .pipe(closureCompiler({ compilerPath: 'node_modules/google-closure-compiler/compiler.jar', fileName: 'material.closure.min.js', compilerFlags: { // jscs:disable requireCamelCaseOrUpperCaseIdentifiers compilation_level: 'ADVANCED_OPTIMIZATIONS', language_in: 'ECMASCRIPT6_STRICT', language_out: 'ECMASCRIPT5_STRICT', warning_level: 'VERBOSE' // jscs:enable requireCamelCaseOrUpperCaseIdentifiers } })) .pipe(gulp.dest('./dist')); }); // Concatenate And Minify JavaScript gulp.task('scripts', ['lint'], () => { return gulp.src(SOURCES) .pipe($.if(/mdlComponentHandler\.js/, $.util.noop(), uniffe())) .pipe($.sourcemaps.init()) // Concatenate Scripts .pipe($.concat('material.js')) .pipe($.iife({useStrict: true})) .pipe(gulp.dest('dist')) // Minify Scripts .pipe($.uglify({ sourceRoot: '.', sourceMapIncludeSources: true })) .pipe($.header(banner, {pkg})) .pipe($.concat('material.min.js')) // Write Source Maps .pipe($.sourcemaps.write('.')) .pipe(gulp.dest('dist')) .pipe($.size({title: 'scripts'})); }); // Clean Output Directory gulp.task('clean', () => del(['dist', '.publish'])); // Copy package manger and LICENSE files to dist gulp.task('metadata', () => { return gulp.src([ 'package.json', 'bower.json', 'LICENSE' ]) .pipe(gulp.dest('dist')); }); // Build Production Files, the Default Task gulp.task('default', ['clean'], cb => { runSequence( ['styles', 'styles-grid'], ['scripts'], ['mocha'], cb); }); // Build production files and microsite gulp.task('all', ['clean'], cb => { runSequence( ['styletemplates'], ['styles-grid', 'styles:gen'], ['scripts'], ['mocha'], ['assets', 'pages', 'templates', 'images', 'metadata'], ['zip'], cb); }); // ***** Testing tasks ***** // gulp.task('mocha', ['styles'], () => { return gulp.src('test/index.html') .pipe($.mochaPhantomjs({reporter: 'tap'})); }); gulp.task('mocha:closure', ['closure'], () => { return gulp.src('test/index.html') .pipe($.replace('src="../dist/material.js"', 'src="../dist/material.closure.min.js"')) .pipe($.rename('temp.html')) .pipe(gulp.dest('test')) .pipe($.mochaPhantomjs({reporter: 'tap'})) .on('finish', () => del.sync('test/temp.html')) .on('error', () => del.sync('test/temp.html')); }); gulp.task('test', [ 'lint', 'mocha', 'mocha:closure' ]); gulp.task('test:visual', () => { browserSync({ notify: false, server: '.', startPath: 'test/visual/index.html' }); gulp.watch('test/visual/**', reload); }); // ***** Landing page tasks ***** // /** * Site metadata for use with templates. * @type {Object} */ const site = {}; /** * Generates an HTML file based on a template and file metadata. */ function applyTemplate() { return through.obj((file, enc, cb) => { const data = { site, page: file.page, content: file.contents.toString() }; const templateFile = path.join( __dirname, 'docs', '_templates', `${file.page.layout}.html`); const tpl = swig.compileFile(templateFile, {cache: false}); file.contents = new Buffer(tpl(data)); cb(null, file); }); } /** * Generates an index.html file for each README in MDL/src directory. */ gulp.task('components', ['demos'], () => { return gulp.src('src/**/README.md', {base: 'src'}) // Add basic front matter. .pipe($.header('---\nlayout: component\nbodyclass: component\ninclude_prefix: ../../\n---\n\n')) .pipe($.frontMatter({ property: 'page', remove: true })) .pipe($.marked()) .pipe((() => { return through.obj((file, enc, cb) => { file.page.component = file.relative.split('/')[0]; cb(null, file); }); })()) .pipe(applyTemplate()) .pipe($.rename(path => path.basename = 'index')) .pipe(gulp.dest('dist/components')); }); /** * Copies demo files from MDL/src directory. */ gulp.task('demoresources', () => { return gulp.src([ 'src/**/demos.css', 'src/**/demo.css', 'src/**/demo.js' ], {base: 'src'}) .pipe($.if('*.scss', $.sass({ precision: 10, onError: console.error.bind(console, 'Sass error:') }))) .pipe($.cssInlineImages({webRoot: 'src'})) .pipe($.if('*.css', $.autoprefixer(AUTOPREFIXER_BROWSERS))) .pipe(gulp.dest('dist/components')); }); /** * Generates demo files for testing made of all the snippets and the demo file * put together. */ gulp.task('demos', ['demoresources'], () => { /** * Retrieves the list of component folders. */ function getComponentFolders() { return fs.readdirSync('src') .filter(file => fs.statSync(path.join('src', file)).isDirectory()); } const tasks = getComponentFolders().map(component => { return gulp.src([ path.join('src', component, 'snippets', '*.html'), path.join('src', component, 'demo.html') ]) .pipe($.concat('/demo.html')) // Add basic front matter. .pipe($.header('---\nlayout: demo\nbodyclass: demo\ninclude_prefix: ../../\n---\n\n')) .pipe($.frontMatter({ property: 'page', remove: true })) .pipe($.marked()) .pipe((() => { return through.obj((file, enc, cb) => { file.page.component = component; cb(null, file); }); })()) .pipe(applyTemplate()) .pipe(gulp.dest(path.join('dist', 'components', component))); }); return mergeStream(tasks); }); /** * Generates an HTML file for each md file in _pages directory. */ gulp.task('pages', ['components'], () => { return gulp.src('docs/_pages/*.md') .pipe($.frontMatter({ property: 'page', remove: true })) .pipe($.marked()) .pipe(applyTemplate()) .pipe($.replace('$$version$$', pkg.version)) .pipe($.replace('$$hosted_libs_prefix$$', hostedLibsUrlPrefix)) .pipe($.replace('$$template_archive_prefix$$', templateArchivePrefix)) /* Replacing code blocks class name to match Prism's. */ .pipe($.replace('class="lang-', 'class="language-')) /* Translate html code blocks to "markup" because that's what Prism uses. */ .pipe($.replace('class="language-html', 'class="language-markup')) .pipe($.rename(path => { if (path.basename !== 'index') { path.dirname = path.basename; path.basename = 'index'; } })) .pipe(gulp.dest('dist')); }); /** * Copies assets from MDL and _assets directory. */ gulp.task('assets', () => { return gulp.src([ 'docs/_assets/**/*', 'node_modules/clippy/build/clippy.swf', 'node_modules/swfobject-npm/swfobject/src/swfobject.js', 'node_modules/prismjs/prism.js', 'node_modules/prismjs/components/prism-markup.min.js', 'node_modules/prismjs/components/prism-javascript.min.js', 'node_modules/prismjs/components/prism-css.min.js', 'node_modules/prismjs/components/prism-bash.min.js', 'node_modules/prismjs/dist/prism-default/prism-default.css' ]) .pipe($.if(/\.js/i, $.replace('$$version$$', pkg.version))) .pipe($.if(/\.js/i, $.replace('$$hosted_libs_prefix$$', hostedLibsUrlPrefix))) .pipe($.if(/\.(svg|jpg|png)$/i, $.imagemin({ progressive: true, interlaced: true }))) .pipe($.if(/\.css/i, $.autoprefixer(AUTOPREFIXER_BROWSERS))) .pipe($.if(/\.css/i, $.csso())) .pipe($.if(/\.js/i, $.uglify({ preserveComments: 'some', sourceRoot: '.', sourceMapIncludeSources: true }))) .pipe(gulp.dest('dist/assets')); }); /** * Defines the list of resources to watch for changes. */ function watch() { gulp.watch(['src/**/*.js', '!src/**/README.md'], ['scripts', 'demos', 'components', reload]); gulp.watch(['src/**/*.{scss,css}'], ['styles', 'styles-grid', 'styletemplates', reload]); gulp.watch(['src/**/*.html'], ['pages', reload]); gulp.watch(['src/**/*.{svg,png,jpg}'], ['images', reload]); gulp.watch(['src/**/README.md'], ['pages', reload]); gulp.watch(['templates/**/*'], ['templates', reload]); gulp.watch(['docs/**/*'], ['pages', 'assets', reload]); gulp.watch(['package.json', 'bower.json', 'LICENSE'], ['metadata']); } /** * Serves the landing page from "out" directory. */ gulp.task('serve:browsersync', () => { browserSync({ notify: false, server: { baseDir: ['dist'] } }); watch(); }); gulp.task('serve', () => { $.connect.server({ root: 'dist', port: 5000, livereload: true }); watch(); gulp.src('dist/index.html') .pipe($.open({uri: 'http://localhost:5000'})); }); // Generate release archive containing just JS, CSS, Source Map deps gulp.task('zip:mdl', () => { return gulp.src([ 'dist/material?(.min)@(.js|.css)?(.map)', 'LICENSE', 'bower.json', 'package.json' ]) .pipe($.zip('mdl.zip')) .pipe(gulp.dest('dist')); }); /** * Returns the list of children directories inside the given directory. * @param {string} dir the parent directory * @return {Array} list of child directories */ function getSubDirectories(dir) { return fs.readdirSync(dir) .filter(file => fs.statSync(path.join(dir, file)).isDirectory()); } // Generate release archives containing the templates and assets for templates. gulp.task('zip:templates', () => { const templates = getSubDirectories('dist/templates'); // Generate a zip file for each template. const generateZips = templates.map(template => { return gulp.src([ `dist/templates/${template}/**/*.*`, 'LICENSE' ]) .pipe($.rename(path => { path.dirname = path.dirname.replace(`dist/templates/${template}`, ''); })) .pipe($.zip(`${templateArchivePrefix}${template}.zip`)) .pipe(gulp.dest('dist')); }); return mergeStream(generateZips); }); gulp.task('zip', [ 'zip:templates', 'zip:mdl' ]); gulp.task('genCodeFiles', () => { return gulp.src([ 'dist/material.*@(js|css)?(.map)', 'dist/mdl.zip', `dist/${templateArchivePrefix}*.zip` ], {read: false}) .pipe($.tap(file => { codeFiles += ` dist/${path.basename(file.path)}`; })); }); // Push the latest version of code resources (CSS+JS) to Google Cloud Storage. // Public-read objects in GCS are served by a Google provided and supported // global, high performance caching/content delivery network (CDN) service. // This task requires gsutil to be installed and configured. // For info on gsutil: https://cloud.google.com/storage/docs/gsutil. gulp.task('pushCodeFiles', () => { const dest = bucketCode; console.log(`Publishing ${pkg.version} to CDN (${dest})`); // Build cache control and gsutil cmd to copy // each object into a GCS bucket. The dest is a version specific path. // The gsutil -m option requests parallel copies. // The gsutil -h option is used to set metadata headers // (cache control, in this case). // Code files should NEVER be touched after uploading, therefore // 30 days caching is a safe value. const cacheControl = '-h "Cache-Control:public,max-age=2592000"'; const gsutilCpCmd = 'gsutil -m cp -z js,css,map '; const gsutilCacheCmd = `gsutil -m setmeta -R ${cacheControl}`; // Upload the goodies to a separate GCS bucket with versioning. // Using a sep bucket avoids the risk of accidentally blowing away // old versions in the microsite bucket. return gulp.src('') .pipe($.shell([ `${gsutilCpCmd}${codeFiles} ${dest}/${pkg.version}`, `${gsutilCacheCmd} ${dest}/${pkg.version}` ])); }); gulp.task('publish:code', cb => { runSequence( ['zip:mdl', 'zip:templates'], 'genCodeFiles', 'pushCodeFiles', cb); }); /** * Function to publish staging or prod version from local tree, * or to promote staging to prod, per passed arg. * @param {string} pubScope the scope to publish to. */ function mdlPublish(pubScope) { let cacheTtl = null; let src = null; let dest = null; if (pubScope === 'staging') { // Set staging specific vars here. cacheTtl = 0; src = 'dist/*'; dest = bucketStaging; } else if (pubScope === 'prod') { // Set prod specific vars here. cacheTtl = 60; src = 'dist/*'; dest = bucketProd; } else if (pubScope === 'promote') { // Set promote (essentially prod) specific vars here. cacheTtl = 60; src = `${bucketStaging}/*`; dest = bucketProd; } let infoMsg = `Publishing ${pubScope}/${pkg.version} to GCS (${dest})`; if (src) { infoMsg += ` from ${src}`; } console.log(infoMsg); // Build gsutil commands: // The gsutil -h option is used to set metadata headers. // The gsutil -m option requests parallel copies. // The gsutil -R option is used for recursive file copy. const cacheControl = `-h "Cache-Control:public,max-age=${cacheTtl}"`; const gsutilCacheCmd = `gsutil -m setmeta ${cacheControl} ${dest}/**`; const gsutilCpCmd = `gsutil -m cp -r -z html,css,js,svg ${src} ${dest}`; gulp.src('').pipe($.shell([gsutilCpCmd, gsutilCacheCmd])); } // Push the local build of the MDL microsite and release artifacts to the // production Google Cloud Storage bucket for general serving to the web. // Public-read objects in GCS are served by a Google provided and supported // global, high performance caching/content delivery network (CDN) service. // This task requires gsutil to be installed and configured. // For info on gsutil: https://cloud.google.com/storage/docs/gsutil. // gulp.task('publish:prod', () => { mdlPublish('prod'); }); // Promote the staging version of the MDL microsite and release artifacts // to the production Google Cloud Storage bucket for general serving. // Public-read objects in GCS are served by a Google provided and supported // global, high performance caching/content delivery network (CDN) service. // This task requires gsutil to be installed and configured. // For info on gsutil: https://cloud.google.com/storage/docs/gsutil. // gulp.task('publish:promote', () => { mdlPublish('promote'); }); // Push the staged version of the MDL microsite and release artifacts // to a production Google Cloud Storage bucket for staging and pre-production testing. // // This task requires gsutil to be installed and configured. // For info on gsutil: https://cloud.google.com/storage/docs/gsutil. // gulp.task('publish:staging', () => { mdlPublish('staging'); }); gulp.task('_release', () => { return gulp.src([ 'dist/material?(.min)@(.js|.css)?(.map)', 'LICENSE', 'README.md', 'bower.json', 'package.json', '.jscsrc', '.jshintrc', './sr?/**/*', 'gulpfile.babel.js', './util?/**/*' ]) .pipe(gulp.dest('_release')); }); gulp.task('publish:release', ['_release'], () => { return gulp.src('_release') .pipe($.subtree({ remote: 'origin', branch: 'release' })) .pipe(vinylPaths(del)); }); gulp.task('templates:styles', () => { return gulp.src('templates/**/*.css') .pipe($.autoprefixer(AUTOPREFIXER_BROWSERS)) // FIXME: This crashes. It's a bug in gulp-csso, // not csso itself. //.pipe($.csso()) .pipe(gulp.dest('dist/templates')); }); gulp.task('templates:static', () => { return gulp.src('templates/**/*.html') .pipe($.replace('$$version$$', pkg.version)) .pipe($.replace('$$hosted_libs_prefix$$', hostedLibsUrlPrefix)) .pipe(gulp.dest('dist/templates')); }); // This task can be used if you want to test the templates against locally // built version of the MDL libraries. gulp.task('templates:localtestingoverride', () => { return gulp.src('templates/**/*.html') .pipe($.replace('$$version$$', '.')) .pipe($.replace('$$hosted_libs_prefix$$', '')) .pipe(gulp.dest('dist/templates')); }); gulp.task('templates:images', () => { return gulp.src('templates/*/images/**/*') .pipe($.imagemin({ progressive: true, interlaced: true })) .pipe(gulp.dest('dist/templates')); }); gulp.task('templates:fonts', () => { return gulp.src('templates/*/fonts/**/*') .pipe(gulp.dest('dist/templates/')); }); gulp.task('templates', [ 'templates:static', 'templates:images', 'templates:fonts', 'templates:styles' ]); gulp.task('styles:gen', ['styles'], () => { const MaterialCustomizer = require('./docs/_assets/customizer.js'); const templatePath = path.join(__dirname, 'dist', 'material.min.css.template'); // TODO: This task needs refactoring once we turn MaterialCustomizer // into a proper Node module. const mc = new MaterialCustomizer(); mc.template = fs.readFileSync(templatePath).toString(); let stream = gulp.src(''); mc.paletteIndices.forEach(primary => { mc.paletteIndices.forEach(accent => { if (primary === accent) { return; } if (mc.forbiddenAccents.indexOf(accent) !== -1) { return; } const primaryName = primary.toLowerCase().replace(' ', '_'); const accentName = accent.toLowerCase().replace(' ', '_'); stream = stream.pipe($.file( `material.${primaryName}-${accentName}.min.css`, mc.processTemplate(primary, accent) )); }); }); stream.pipe(gulp.dest('dist')); });