diff --git a/Gruntfile.js b/Gruntfile.js index 0026039a816..9d14f141cd9 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -10,65 +10,6 @@ module.exports = function( grunt ) { .replace( /\.\.\/css/, "css" ) .replace( /jquery\.mobile\.css/, processedName + ".min.css" ); }, - - // Ensure that modules specified via the --modules option are in the same - // order as the one in which they appear in js/jquery.mobile.js. To achieve - // this, we parse js/jquery.mobile.js and reconstruct the array of - // dependencies listed therein. - makeModulesList = function( modules ) { - var start, end, index, - modulesHash = {}, - fixedModules = [], - jsFile = grunt.file.read( path.join( "js", "jquery.mobile.js" ) ); - - modules = modules.split( "," ); - - // This is highly dependent on the contents of js/jquery.mobile.js - if ( jsFile ) { - start = jsFile.indexOf( "[" ); - if ( start > -1 ) { - start++; - end = jsFile.indexOf( "]" ); - if ( start < jsFile.length && - end > -1 && end < jsFile.length && end > start ) { - - // Convert list of desired modules to a hash - for ( index = 0 ; index < modules.length ; index++ ) { - modulesHash[ modules[ index ] ] = true; - } - - // Split list of modules from js/jquery.mobile.js into an array - jsFile = jsFile - .slice( start, end ) - .match( /"[^"]*"/gm ); - - // Add each desired module to the fixed list of modules in the - // correct order - for ( index = 0 ; index < jsFile.length ; index++ ) { - - // First we need to touch up each module from js/jquery.mobile.js - jsFile[ index ] = jsFile[ index ] - .replace( /"/g, "" ) - .replace( /^.\//, "" ); - - // Then, if it's in the hash of desired modules, add it to the - // list containing the desired modules in the correct order - if ( modulesHash[ jsFile[ index ] ] ) { - fixedModules.push( jsFile[ index ] ); - } - } - - // If we've found all the desired modules, we re-create the comma- - // separated list and return it. - if ( fixedModules.length === modules.length ) { - modules = fixedModules; - } - } - } - } - - return modules; - }, processDemos = function( content, srcPath ) { var processedName, $; @@ -365,9 +306,7 @@ module.exports = function( grunt ) { mainConfigFile: "js/requirejs.config.js", - include: ( grunt.option( "modules" ) ? - makeModulesList( grunt.option( "modules" ) ) : - [ "jquery.mobile" ] ), + include: [ "jquery.mobile" ], exclude: [ "jquery", @@ -1072,13 +1011,15 @@ module.exports = function( grunt ) { ]); grunt.registerTask( "dist", [ + "modules", "clean:dist", "config:fetchHeadHash", "js:release", "css:release", "demos", "compress:dist", - "compress:images" + "compress:images", + "clean:tmp" ]); grunt.registerTask( "dist:release", [ "release:init", "dist", "cdn" ] ); grunt.registerTask( "dist:git", [ "dist", "clean:git", "config:copy:git:-git", "copy:git" ] ); diff --git a/build/tasks/modules.js b/build/tasks/modules.js new file mode 100644 index 00000000000..ff86e454b9b --- /dev/null +++ b/build/tasks/modules.js @@ -0,0 +1,259 @@ +#!/usr/bin/env node +module.exports = function( grunt ) { + "use strict"; + + var css = require( "css" ), + esprima = require( "esprima" ), + path = require( "path" ), + cssFiles = { + theme: { present: {}, list: [] }, + structure: { present: {}, list: [] } + }; + + // Ensure that modules specified via the --modules option are in the same + // order as the one in which they appear in js/jquery.mobile.js. To achieve + // this, we parse js/jquery.mobile.js and reconstruct the array of + // dependencies listed therein. + function makeModulesList( modules ) { + var parsedFile, desiredModulesHash, listedModules, index, singleListedModule, + fixedModules = [], + jsFile = grunt.file.read( path.join( "js", "jquery.mobile.js" ) ); + + modules = modules.split( "," ); + + // This is highly dependent on the contents of js/jquery.mobile.js. It assumes that all + // dependencies are listed flatly in the first argument of the first expression in the + // file. + if ( jsFile ) { + parsedFile = esprima.parse( jsFile, { raw: true, comment: true } ); + + // Descend into the parsed file to grab the array of deps + if ( parsedFile && parsedFile.body && parsedFile.body.length > 0 && + parsedFile.body[ 0 ] && parsedFile.body[ 0 ].expression && + parsedFile.body[ 0 ].expression.arguments && + parsedFile.body[ 0 ].expression.arguments.length && + parsedFile.body[ 0 ].expression.arguments.length > 0 && + parsedFile.body[ 0 ].expression.arguments[ 0 ] && + parsedFile.body[ 0 ].expression.arguments[ 0 ].elements && + parsedFile.body[ 0 ].expression.arguments[ 0 ].elements.length > 0 ) { + + listedModules = parsedFile.body[ 0 ].expression.arguments[ 0 ].elements; + desiredModulesHash = {}; + + // Convert list of desired modules to a hash + for ( index = 0 ; index < modules.length ; index++ ) { + desiredModulesHash[ modules[ index ] ] = true; + } + + // Then, if a listed module is in the hash of desired modules, add it to the + // list containing the desired modules in the correct order + for ( index = 0 ; index < listedModules.length ; index++ ) { + singleListedModule = listedModules[ index ].value.replace( /^\.\//, "" ); + if ( desiredModulesHash[ singleListedModule ] ) { + fixedModules.push( singleListedModule ); + } + } + + // If we've found all the desired modules we can return the list of modules + // assembled, because that list contains the modules in the correct order. + if ( fixedModules.length === modules.length ) { + modules = fixedModules; + } + } + } + + return modules; + }; + + grunt.registerTask( "modules", function() { + var modulesList = grunt.option( "modules" ), + requirejsModules = grunt.config( "requirejs.js.options.include" ), + onBuildWrite = grunt.config( "requirejs.js.options.onBuildWrite" ), + onModuleBundleComplete = grunt.config( "requirejs.js.options.onModuleBundleComplete" ); + + if ( !modulesList ) { + return; + } + + if ( !requirejsModules ) { + throw( new Error( "Missing configuration key 'requirejs.js.options.include" ) ); + } + + grunt.config( "requirejs.js.options.include", makeModulesList( modulesList ) ); + + grunt.config( "requirejs.js.options.onBuildWrite", function( moduleName, path, contents ) { + var index, match, + + // We parse the file for the special comments in order to assemble a list of + // structure and theme CSS files to serve as the basis for custom theme and + // structure files which we then feed to the optimizer + parsedFile = esprima.parse( contents, { comment: true } ), + addCSSFile = function( file ) { + file = file.trim(); + if ( !cssFiles[ match[ 1 ] ].present[ file ] ) { + cssFiles[ match[ 1 ] ].list.push( file ); + cssFiles[ match[ 1 ] ].present[ file ] = true; + } + }; + + if ( parsedFile.comments && parsedFile.comments.length > 0 ) { + for ( index = 0 ; index < parsedFile.comments.length ; index++ ) { + match = parsedFile.comments[ index ].value + .match( /^>>css\.(theme|structure): (.*)/ ); + + // Parse the special comment and add the files listed on the right hand + // side of the flag to the appropriate list of CSS files + if ( match && match.length > 2 ) { + match[ 2 ].split( "," ).forEach( addCSSFile ); + } + } + } + + return onBuildWrite ? onBuildWrite.apply( this, arguments ) : contents; + }); + + + grunt.config( "requirejs.js.options.onModuleBundleComplete", function() { + + // We assume that the source for the structure file is called + // "jquery.mobile.structure.css", that the source for the theme file is called + // "jquery.mobile.theme.css", and that the source for the combined theme+structure file + // is called "jquery.mobile.css" + var cssFileContents, structure, theme, all, + allFiles = grunt.config( "cssbuild.all.files" ), + destinationPath = grunt.config.process( "<%= dirs.tmp %>" ), + + // Traverse the tree produced by the CSS parser and update import paths + updateImportUrl = function( cssFilePath, cssRoot ) { + var index, item, match, filename; + + for ( index in cssRoot ) { + item = cssRoot[ index ]; + + if ( item && item.type === "import" ) { + + // NB: The regex below assumes there's no whitespace in the + // @import reference, i.e. url("path/to/filename"); + match = item.import.match( /(url\()(.*)(\))$/ ); + if ( match ) { + + // Strip the quotes from around the filename + filename = match[ 2 ] + .substr( 1, match[ 2 ].length - 2 ); + + // Replace theme and structure with our custom + // reference + if ( path.basename( filename ) === + "jquery.mobile.theme.css" ) { + item.import = + "url(\"jquery.mobile.custom.theme.css\")"; + } else if ( path.basename( filename ) === + "jquery.mobile.structure.css" ) { + item.import = + "url(\"jquery.mobile.custom.structure.css\")"; + + // Adjust the relative path for all other imports + } else { + item.import = + + // url( + match[ 1 ] + + + // quotation mark + match[ 2 ].charAt( 0 ) + + + // path adjusted to be relative to the + // temporary directory + path.relative( destinationPath, + path.normalize( path.join( cssFilePath, + filename ) ) ) + + + // quotation mark + match[ 2 ].charAt( 0 ) + + + // ) + match[ 3 ]; + } + } + } else if ( typeof item === "object" ) { + updateImportUrl( cssFilePath, item ); + } + } + + return cssRoot; + }; + + // Find the entries for the structure, the theme, and the combined + // theme+structure file, because we want to update them to point to our + // custom-built version + allFiles.forEach( function( singleCSSFile ) { + if ( path.basename( singleCSSFile.src ) === + "jquery.mobile.structure.css" ) { + structure = singleCSSFile; + } else if ( path.basename( singleCSSFile.src ) === + "jquery.mobile.theme.css" ) { + theme = singleCSSFile; + } else if ( path.basename( singleCSSFile.src ) === + "jquery.mobile.css" ) { + all = singleCSSFile; + } + }); + + // Create temporary structure file and update the grunt config + // reference + cssFileContents = ""; + if ( cssFiles.structure.list.length > 0 ) { + cssFiles.structure.list.forEach( function( file ) { + + // Recalculate relative path from destination in the temporary + // directory + file = path.relative( destinationPath, + + // css files are originally relative to "js/" + path.join( "js", file ) ); + cssFileContents += "@import url(\"" + file + "\");\n"; + }); + structure.src = path.join( destinationPath, + "jquery.mobile.custom.structure.css" ); + grunt.file.write( structure.src, cssFileContents, + { encoding: "utf8" } ); + } + + // Create temporary theme file and update the grunt config reference + cssFileContents = ""; + if ( cssFiles.theme.list.length > 0 ) { + cssFiles.theme.list.forEach( function( file ) { + + // Recalculate relative path from destination in the temporary + // directory + file = path.relative( destinationPath, + + // css files are originally relative to "js/" + path.join( "js", file ) ); + cssFileContents += "@import url(\"" + file + "\");\n"; + }); + theme.src = path.join( destinationPath, + "jquery.mobile.custom.theme.css" ); + grunt.file.write( theme.src, cssFileContents, + { encoding: "utf8" } ); + } + + // Create temporary theme+structure file by replacing references to the + // standard theme and structure files with references to the custom + // theme and structure files created above, and update the grunt config + // reference + cssFileContents = css.stringify( updateImportUrl( + path.dirname( all.src ), + css.parse( grunt.file.read( all.src, { encoding: "utf8" } ) ) ) ); + all.src = path.join( destinationPath, "jquery.mobile.custom.css" ); + grunt.file.write( all.src, cssFileContents, { encoding: "utf8" } ); + + // Update grunt configuration + grunt.config( "cssbuild.all.files", allFiles ); + + if ( onModuleBundleComplete ) { + return onModuleBundleComplete.apply( this, arguments ); + } + }); + }); +}; diff --git a/package.json b/package.json index ee4825b1e21..4f5866284b7 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "casperjs": "1.1.0-beta3", "cheerio": "0.12.4", "commitplease": "2.0.0", + "css": "2.1.0", + "esprima": "1.2.2", "grunt": "0.4.2", "grunt-bowercopy": "0.5.0", "grunt-casper": "0.3.2",