diff --git a/Gruntfile.js b/Gruntfile.js index 7e64c2a9..84d2becf 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -413,6 +413,11 @@ function buildPackages( folder, callback ) { // (a) Build jquery-ui-[VERSION].zip; function( callback ) { + if ( semver.gte( jqueryUi.pkg.version, "1.13.0-a" ) ) { + packagerZip( "./lib/package-1-13", "jquery-ui-" + jqueryUi.pkg.version, + new ThemeGallery( jqueryUi )[ 0 ].vars, folder, jqueryUi, callback ); + return; + } if ( semver.gte( jqueryUi.pkg.version, "1.12.0-a" ) ) { packagerZip( "./lib/package-1-12", "jquery-ui-" + jqueryUi.pkg.version, new ThemeGallery( jqueryUi )[ 0 ].vars, folder, jqueryUi, callback ); @@ -438,6 +443,11 @@ function buildPackages( folder, callback ) { // (b) Build themes package jquery-ui-themes-[VERSION].zip; function( callback ) { + if ( semver.gte( jqueryUi.pkg.version, "1.13.0-a" ) ) { + packagerZip( "./lib/package-1-13-themes", "jquery-ui-themes-" + jqueryUi.pkg.version, + null, folder, jqueryUi, callback ); + return; + } if ( semver.gte( jqueryUi.pkg.version, "1.12.0-a" ) ) { packagerZip( "./lib/package-1-12-themes", "jquery-ui-themes-" + jqueryUi.pkg.version, null, folder, jqueryUi, callback ); diff --git a/download.js b/download.js index 49bd2d77..cb8bb5a8 100644 --- a/download.js +++ b/download.js @@ -151,7 +151,11 @@ Frontend.prototype = { // The new way to generate a package. } else { - Package = require( "./lib/package-1-12" ); + if ( semver.gte( jqueryUi.pkg.version, "1.13.0-a" ) ) { + Package = require( "./lib/package-1-13" ); + } else { + Package = require( "./lib/package-1-12" ); + } packager = new Packager( jqueryUi.files().cache, Package, { components: components, themeVars: themeVars, diff --git a/lib/package-1-13-themes.js b/lib/package-1-13-themes.js new file mode 100644 index 00000000..77e9ae01 --- /dev/null +++ b/lib/package-1-13-themes.js @@ -0,0 +1,83 @@ +"use strict"; + +var async = require( "async" ); +var extend = require( "util" )._extend; +var banner = require( "./banner" ); +var sqwish = require( "sqwish" ); +var Package1_13 = require( "./package-1-13" ); +var path = require( "path" ); +var ThemeGallery = require( "./themeroller-themegallery" ); + +function stripBanner( data ) { + if ( data instanceof Buffer ) { + data = data.toString( "utf8" ); + } + return data.replace( /^\s*\/\*[\s\S]*?\*\/\s*/g, "" ); +} + +function Package( files, runtime ) { + this.themeGallery = ThemeGallery( runtime.jqueryUi ); + if ( !runtime.themeVars ) { + runtime.themeVars = this.themeGallery[ 0 ].vars; + } + Package1_13.apply( this, arguments ); +} + +extend( Package.prototype, { + "AUTHORS.txt": Package1_13.prototype[ "AUTHORS.txt" ], + "LICENSE.txt": Package1_13.prototype[ "LICENSE.txt" ], + "images": Package1_13.prototype.images, + "jquery-ui.css": Package1_13.prototype[ "jquery-ui.css" ], + "jquery-ui.structure.css": Package1_13.prototype[ "jquery-ui.structure.css" ], + "jquery-ui.theme.css": Package1_13.prototype[ "jquery-ui.theme.css" ], + "jquery-ui.min.css": Package1_13.prototype[ "jquery-ui.min.css" ], + "jquery-ui.structure.min.css": Package1_13.prototype[ "jquery-ui.structure.min.css" ], + "jquery-ui.theme.min.css": Package1_13.prototype[ "jquery-ui.theme.min.css" ], + + "themes": function( callback ) { + var files = {}; + var structureCssFileNames = this.structureCssFileNames; + var themeCssFileNames = this.themeCssFileNames; + var pkgJson = this.pkgJson; + var themeGallery = this.themeGallery; + this.structureCss.promise.then( function( structureCss ) { + async.mapSeries( themeGallery, function( theme, callback ) { + var themeCss = theme.css(); + + files[ path.join( theme.folderName(), "theme.css" ) ] = themeCss; + + var _banner = banner( pkgJson, structureCssFileNames.concat( themeCssFileNames ), { + customThemeUrl: theme.url() + } ); + var _minBanner = banner( pkgJson, structureCssFileNames.concat( themeCssFileNames ), { + minify: true, + customThemeUrl: theme.url() + } ); + var allCss = structureCss + stripBanner( themeCss ); + + // Bundle CSS (and minified) + files[ path.join( theme.folderName(), "jquery-ui.css" ) ] = _banner + allCss; + files[ path.join( theme.folderName(), "jquery-ui.min.css" ) ] = _minBanner + sqwish.minify( allCss ); + + // Custom theme image files + theme.generateImages( function( error, imageFiles ) { + if ( error ) { + return callback( error, null ); + } + imageFiles.forEach( function( imageFile ) { + files[ path.join( theme.folderName(), "images", imageFile.path ) ] = imageFile.data; + } ); + callback(); + } ); + }, function( error ) { + if ( error ) { + console.log( "mapSeries( themeGallery ) failed", error ); + return callback( error ); + } + callback( null, files ); + } ); + } ); + } +} ); + +module.exports = Package; diff --git a/lib/package-1-13.js b/lib/package-1-13.js new file mode 100644 index 00000000..9f9307f8 --- /dev/null +++ b/lib/package-1-13.js @@ -0,0 +1,337 @@ +"use strict"; + +var indexTemplate, jsBundleIntro, jsBundleOutro, + amdBuilder = require( "builder-amd" ), + banner = require( "./banner" ), + extend = require( "util" )._extend, + fs = require( "fs" ), + handlebars = require( "handlebars" ), + jqueryCssBuilder = require( "builder-jquery-css" ), + path = require( "path" ), + Q = require( "q" ), + sqwish = require( "sqwish" ), + ThemeRoller = require( "jquery-ui-themeroller" ), + UglifyJS = require( "uglify-js" ); + +// We're using the same template as 1.12 for now. +indexTemplate = handlebars.compile( fs.readFileSync( __dirname + "/../template/zip/index-1-12.html", "utf8" ) ); +Q.longStackSupport = true; + +jsBundleIntro = "( function( factory ) {\n" + + " \"use strict\";\n" + + " \n" + + " if ( typeof define === \"function\" && define.amd ) {\n" + + "\n" + + " // AMD. Register as an anonymous module.\n" + + " define( [ \"jquery\" ], factory );\n" + + " } else {\n" + + "\n" + + " // Browser globals\n" + + " factory( jQuery );\n" + + " }\n" + + "} )( function( $ ) {" + + " \"use strict\";"; + +jsBundleOutro = "} );"; + +function camelCase( input ) { + return input.toLowerCase().replace( /[-/](.)/g, function( match, group1 ) { + return group1.toUpperCase(); + } ); +} + +function stripBanner( data ) { + if ( data instanceof Buffer ) { + data = data.toString( "utf8" ); + } + return data.replace( /^\s*\/\*[\s\S]*?\*\/\s*/g, "" ); +} + +/** + * scope( css, scope ) + * - css [ String ]: CSS content. + * - scopeString [ String ]: The scope-string that will be added before each css ".ui*" selector. + * + * Returns the scoped css. + */ +function scope( css, scopeString ) { + return css.replace( /(\.ui[^\n,}]*)/g, scopeString + " $1" ); +} + +function Package( files, runtime ) { + this.jsBundle = Q.defer(); + this.jsFileNames = runtime.components.map( function( component ) { + return component + ".js"; + } ); + this.pkgJson = JSON.parse( files[ "package.json" ].toString( "utf-8" ) ); + this.structureCss = Q.defer(); + this.structureCssFileNames = []; + this.structureMinCss = Q.defer(); + this.themeCss = Q.defer(); + this.themeCssFileNames = []; + this.themeMinCss = Q.defer(); + this.zipBasedir = "jquery-ui-" + this.pkgJson.version + ".custom"; + this.zipFilename = this.zipBasedir + ".zip"; +} + +extend( Package.prototype, { + "AUTHORS.txt": "AUTHORS.txt", + "LICENSE.txt": "LICENSE.txt", + "package.json": "package.json", + "external/jquery/jquery.js": function() { + if ( !this.runtime.components.length ) { + return null; + } + return this.files[ "external/jquery/jquery.js" ]; + }, + + "images": function( callback ) { + var self = this; + + this.themeCss.promise.then( function( css ) { + if ( css === null ) { + return callback( null, null ); + } + self.themeroller.generateImages( callback ); + } ).catch( callback ); + }, + + "index.html": function() { + var version = this.pkgJson.version; + if ( !this.runtime.components.length ) { + return null; + } + return indexTemplate( { + ui: this.runtime.components.reduce( function( sum, component ) { + sum[ component.replace( /^.+\//, "" ) ] = true; + return sum; + }, {} ), + version: version + } ); + }, + + "jquery-ui.css": function( callback ) { + var self = this, + pkgJson = this.pkgJson, + structureCssFileNames = this.structureCssFileNames, + themeCssFileNames = this.themeCssFileNames; + + Q.all( [ this.structureCss.promise, this.themeCss.promise ] ).spread( function( structureCss, themeCss ) { + var _banner = banner( pkgJson, structureCssFileNames.concat( themeCssFileNames ), { + customThemeUrl: self.customThemeUrl + } ); + themeCss = themeCss ? "\n" + themeCss : ""; + callback( null, _banner + structureCss + themeCss ); + } ).catch( callback ); + }, + + "jquery-ui.js": function( callback ) { + var jsBundle = this.jsBundle, + jsFileNames = this.jsFileNames, + pkgJson = this.pkgJson; + + if ( !this.runtime.components.length ) { + return callback( null, null ); + } + + amdBuilder( this.files, { + appDir: "ui", + exclude: [ "jquery" ], + include: this.runtime.components, + onBuildWrite: function( id, path, contents ) { + var name; + + if ( id === "jquery" ) { + return contents; + } + + name = camelCase( id.replace( /ui\//, "" ).replace( /\.js$/, "" ) ); + return contents + + // Remove UMD wrappers of UI & jQuery Color. + .replace( /\( ?function\( ?(?:root, ?)?factory\b[\s\S]*?\( ?"(?:this, ?)?function\( ?[^\)]* ?\) ?\{/, "" ) + .replace( /\} ?\);\s*?$/, "" ) + + // Replace return exports for var =. + .replace( /\nreturn/, "\nvar " + name + " =" ); + }, + optimize: "none", + paths: { + jquery: "../external/jquery/jquery" + }, + wrap: { + start: jsBundleIntro, + end: jsBundleOutro + } + }, function( error, js ) { + var _banner; + if ( error ) { + jsBundle.reject( error ); + return callback( error ); + } + + // Remove leftover define created during rjs build + js = js.replace( /define\(".*/, "" ); + + jsBundle.resolve( js ); + _banner = banner( pkgJson, jsFileNames ); + callback( null, _banner + js ); + } ); + }, + + "jquery-ui.structure.css": function( callback ) { + var self = this, + structureCssFileNames = this.structureCssFileNames, + runtime = this.runtime, + structureCss = this.structureCss, + themeCss = this.themeCss; + + if ( !this.runtime.components.length ) { + structureCss.resolve( "" ); + return callback( null, null ); + } + + jqueryCssBuilder( this.files, "structure", { + appDir: "ui", + include: this.runtime.components, + onCssBuildWrite: function( _path, data ) { + if ( data === undefined ) { + throw new Error( "onCssBuildWrite failed (data is undefined) for path " + _path ); + } + structureCssFileNames.push( path.basename( _path ) ); + return stripBanner( data ); + }, + paths: { + jquery: "../external/jquery/jquery" + } + }, function( error, css ) { + if ( error ) { + structureCss.reject( error ); + return callback( error ); + } + + // Scope all rules due to specificity issue with tabs (see #87) + if ( runtime.scope ) { + css = scope( css, runtime.scope ); + } + + structureCss.resolve( css ); + + // Add Banner + themeCss.promise.then( function() { + var banner = self.baseThemeCss + .replace( /\*\/[\s\S]*/, "*/" ) + .replace( /\n.*\n.*themeroller.*/, "" ); + + banner = banner ? banner + "\n" : ""; + callback( null, banner + css ); + } ); + } ); + }, + + "jquery-ui.theme.css": function() { + var css; + + if ( this.runtime.themeVars === null ) { + this.baseThemeCss = ""; + this.themeCss.resolve( null ); + return null; + } + + this.baseThemeCss = this.files[ "themes/base/theme.css" ]; + this.themeCssFileNames.push( "theme.css" ); + + this.themeroller = new ThemeRoller( this.baseThemeCss, this.runtime.themeVars ); + this.customThemeUrl = this.themeroller.url(); + css = this.themeroller.css(); + + if ( this.runtime.scope ) { + css = scope( css, this.runtime.scope ); + } + + this.themeCss.resolve( stripBanner( css ) ); + + return css; + }, + + "jquery-ui.min.css": function( callback ) { + var self = this, + pkgJson = this.pkgJson, + structureCssFileNames = this.structureCssFileNames, + themeCssFileNames = this.themeCssFileNames; + + Q.all( [ this.structureMinCss.promise, this.themeMinCss.promise ] ).spread( + function( structureMinCss, themeMinCss ) { + var _banner = banner( pkgJson, structureCssFileNames.concat( themeCssFileNames ), { + customThemeUrl: self.customThemeUrl, + minify: true + } ); + themeMinCss = themeMinCss || ""; + callback( null, _banner + structureMinCss + themeMinCss ); + } ).catch( callback ); + }, + + "jquery-ui.min.js": function( callback ) { + var jsFileNames = this.jsFileNames, + pkgJson = this.pkgJson; + + if ( !this.runtime.components.length ) { + return callback( null, null ); + } + + this.jsBundle.promise.then( function( js ) { + var minJs; + var _banner = banner( pkgJson, jsFileNames, { minify: true } ); + var uglifyResult = UglifyJS.minify( js ); + if ( uglifyResult.error ) { + throw uglifyResult.error; + } + minJs = uglifyResult.code; + callback( null, _banner + minJs ); + } ).catch( callback ); + }, + + "jquery-ui.structure.min.css": function( callback ) { + var pkgJson = this.pkgJson, + structureMinCss = this.structureMinCss; + + if ( !this.runtime.components.length ) { + structureMinCss.resolve(); + return callback( null, null ); + } + + this.structureCss.promise.then( function( css ) { + var _banner = banner( pkgJson, null, { minify: true } ), + minCss = sqwish.minify( css ); + structureMinCss.resolve( minCss ); + callback( null, _banner + minCss ); + }, function( error ) { + structureMinCss.reject( error ); + callback( error ); + } ); + }, + + "jquery-ui.theme.min.css": function( callback ) { + var pkgJson = this.pkgJson, + themeMinCss = this.themeMinCss; + + this.themeCss.promise.then( function( css ) { + var _banner, minCss; + + if ( css === null ) { + themeMinCss.resolve( null ); + return callback( null, null ); + } + + _banner = banner( pkgJson, null, { minify: true } ); + minCss = sqwish.minify( css ); + themeMinCss.resolve( minCss ); + callback( null, _banner + minCss ); + }, function( error ) { + themeMinCss.reject( error ); + callback( error ); + } ); + } +} ); + +module.exports = Package; diff --git a/test/package-1-12.0.js b/test/package-1-12.0.js index 315eca2a..922af530 100644 --- a/test/package-1-12.0.js +++ b/test/package-1-12.0.js @@ -196,7 +196,8 @@ tests = { JqueryUi.all().filter( function( jqueryUi ) { // Filter supported releases only - return semver.gte( jqueryUi.pkg.version, "1.12.0-a" ); + return semver.gte( jqueryUi.pkg.version, "1.12.0-a" ) && + semver.lt( jqueryUi.pkg.version, "1.13.0-a" ); } ).forEach( function( jqueryUi ) { function deepTestBuild( obj, tests ) { var allComponents = jqueryUi.components().map( function( component ) { diff --git a/test/package-1-13.0.js b/test/package-1-13.0.js new file mode 100644 index 00000000..b322a0e2 --- /dev/null +++ b/test/package-1-13.0.js @@ -0,0 +1,240 @@ +"use strict"; + +var commonFiles, COMMON_FILES_TESTCASES, defaultTheme, newPackage, someWidgets1, someWidgets2, tests, themeFiles, THEME_FILES_TESTCASES, + async = require( "async" ), + JqueryUi = require( "../lib/jquery-ui" ), + Package = require( "../lib/package-1-13" ), + Packager = require( "node-packager" ), + semver = require( "semver" ), + themeGallery = require( "../lib/themeroller-themegallery" )(); + +function filePresent( files, filepath ) { + var filepathRe = filepath instanceof RegExp ? filepath : new RegExp( filepath.replace( /\*/g, "[^\/]*" ).replace( /\./g, "\\." ).replace( /(.*)/, "^$1$" ) ); + return Object.keys( files ).some( function( filepath ) { + return filepathRe.test( filepath ); + } ); +} + +defaultTheme = themeGallery[ 0 ].vars; +someWidgets1 = "widget core position widgets/autocomplete widgets/button widgets/menu widgets/progressbar widgets/spinner widgets/tabs".split( " " ); +someWidgets2 = "widget core widgets/mouse position widgets/draggable widgets/resizable widgets/button widgets/datepicker widgets/dialog widgets/slider widgets/tooltip".split( " " ); + +commonFiles = [ + "external/jquery/jquery.js", + "index.html", + "jquery-ui.css", + "jquery-ui.js", + "jquery-ui.min.css", + "jquery-ui.min.js", + "jquery-ui.structure.css", + "jquery-ui.structure.min.css" +]; +COMMON_FILES_TESTCASES = commonFiles.length; +function commonFilesCheck( test, files ) { + commonFiles.forEach( function( filepath ) { + test.ok( filePresent( files, filepath ), "Missing a common file \"" + filepath + "\"." ); + } ); +} + +themeFiles = [ + "jquery-ui.theme.css", + "jquery-ui.theme.min.css", + "images/ui-icons*png" +]; +THEME_FILES_TESTCASES = themeFiles.length; +function themeFilesCheck( test, files, theme ) { + themeFiles.forEach( function( filepath ) { + if ( theme ) { + test.ok( filePresent( files, filepath ), "Missing a theme file \"" + filepath + "\"." ); + } else { + test.ok( !filePresent( files, filepath ), "Should not include the theme file \"" + filepath + "\"." ); + } + } ); +} + +tests = { + "test: select all components": { + "with the default theme": function( test ) { + var pkg = new Packager( this.files, Package, { + components: this.allComponents, + themeVars: defaultTheme + } ); + test.expect( COMMON_FILES_TESTCASES + THEME_FILES_TESTCASES ); + pkg.toJson( function( error, files ) { + if ( error ) { + return test.done( error ); + } + commonFilesCheck( test, files ); + themeFilesCheck( test, files, true ); + test.done(); + } ); + }, + "with a different theme": function( test ) { + var pkg = new Packager( this.files, Package, { + components: this.allComponents, + themeVars: themeGallery[ 1 ].vars + } ); + test.expect( COMMON_FILES_TESTCASES + THEME_FILES_TESTCASES ); + pkg.toJson( function( error, files ) { + if ( error ) { + return test.done( error ); + } + commonFilesCheck( test, files ); + themeFilesCheck( test, files, true ); + test.done(); + } ); + } + }, + "test: select all widgets": function( test ) { + var allWidgets = this.allWidgets; + var pkg = new Packager( this.files, Package, { + components: allWidgets, + themeVars: defaultTheme + } ); + test.expect( COMMON_FILES_TESTCASES + THEME_FILES_TESTCASES + 2 ); + test.equal( allWidgets.length, 15 ); + pkg.toJson( function( error, files ) { + if ( error ) { + return test.done( error ); + } + commonFilesCheck( test, files ); + themeFilesCheck( test, files, true ); + + // 15 widgets, 14 have CSS, plus core, theme, draggable, resizable + var includes = files[ "jquery-ui.min.css" ].match( /\* Includes: (.+)/ ); + test.equal( includes[ 1 ].split( "," ).length, 18, allWidgets + " -> " + includes[ 1 ] ); + + test.done(); + } ); + }, + "test: select all effects": function( test ) { + var pkg = new Packager( this.files, Package, { + components: this.allEffects, + themeVars: null + } ); + test.expect( COMMON_FILES_TESTCASES + THEME_FILES_TESTCASES + 1 ); + test.equal( this.allEffects.length, 16 ); + pkg.toJson( function( error, files ) { + if ( error ) { + return test.done( error ); + } + commonFilesCheck( test, files ); + themeFilesCheck( test, files, false ); + test.done(); + } ); + }, + "test: select some widgets (1)": function( test ) { + var pkg = new Packager( this.files, Package, { + components: someWidgets1, + themeVars: defaultTheme + } ); + test.expect( COMMON_FILES_TESTCASES + THEME_FILES_TESTCASES + 2 ); + test.equal( someWidgets1.length, 9 ); + pkg.toJson( function( error, files ) { + if ( error ) { + return test.done( error ); + } + commonFilesCheck( test, files ); + themeFilesCheck( test, files, true ); + + // 9 components selected, 6 have CSS, plus core, theme, + // checkboxradio, controlgroup (tmp button dependencies) + var includes = files[ "jquery-ui.min.css" ].match( /\* Includes: (.+)/ ); + test.equal( includes[ 1 ].split( "," ).length, 10, someWidgets1 + " -> " + includes[ 1 ] ); + + test.done(); + } ); + }, + "test: select some widgets (2)": function( test ) { + var pkg = new Packager( this.files, Package, { + components: someWidgets2, + themeVars: defaultTheme + } ); + test.expect( COMMON_FILES_TESTCASES + THEME_FILES_TESTCASES + 2 ); + test.equal( someWidgets2.length, 11 ); + pkg.toJson( function( error, files ) { + if ( error ) { + return test.done( error ); + } + commonFilesCheck( test, files ); + themeFilesCheck( test, files, true ); + + // 11 components selected, 7 have CSS, plus core, theme, + // checkboxradio, controlgroup (tmp button dependencies) + var includes = files[ "jquery-ui.min.css" ].match( /\* Includes: (.+)/ ); + test.equal( includes[ 1 ].split( "," ).length, 11, someWidgets2 + " -> " + includes[ 1 ] ); + + test.done(); + } ); + }, + "test: scope widget CSS": function( test ) { + var pkg, + filesToCheck = [ + "jquery-ui.css", + "jquery-ui.min.css" + ], + scope = "#wrapper", + scopeRe = new RegExp( scope ); + pkg = new Packager( this.files, Package, { + components: this.allComponents, + themeVars: defaultTheme, + scope: scope + } ); + test.expect( filesToCheck.length ); + pkg.toJson( function( error, files ) { + if ( error ) { + return test.done( error ); + } + filesToCheck.forEach( function( filepath ) { + test.ok( scopeRe.test( files[ filepath ] ), "Missing scope selector on \"" + filepath + "\"." ); + } ); + test.done(); + } ); + } +}; + +JqueryUi.all().filter( function( jqueryUi ) { + + // Filter supported releases only + return semver.gte( jqueryUi.pkg.version, "1.13.0-a" ); +} ).forEach( function( jqueryUi ) { + function deepTestBuild( obj, tests ) { + var allComponents = jqueryUi.components().map( function( component ) { + return component.name; + } ), + allEffects = jqueryUi.components().filter( function( component ) { + return component.category === "Effects"; + } ).map( function( component ) { + return component.name; + } ), + allWidgets = jqueryUi.components().filter( function( component ) { + return component.category === "Widgets"; + } ).map( function( component ) { + return [ component.name ]; + } ).reduce( function( flat, arr ) { + return flat.concat( arr ); + }, [] ).sort().filter( function( element, i, arr ) { + + // unique + return i === arr.indexOf( element ); + } ), + files = jqueryUi.files().cache; + Object.keys( tests ).forEach( function( i ) { + if ( typeof tests[ i ] === "object" ) { + obj[ i ] = {}; + deepTestBuild( obj[ i ], tests[ i ] ); + } else { + obj[ i ] = function( test ) { + tests[ i ].call( { + allComponents: allComponents, + allEffects: allEffects, + allWidgets: allWidgets, + files: files + }, test ); + }; + } + } ); + } + module.exports[ jqueryUi.pkg.version ] = {}; + deepTestBuild( module.exports[ jqueryUi.pkg.version ], tests ); +} );