From ada3f553fac39b9aadb0b09219920f2cb3e28d71 Mon Sep 17 00:00:00 2001 From: Gabriel Schulhof Date: Tue, 30 Dec 2014 13:51:27 +0200 Subject: [PATCH 1/3] Build: Use esprima to construct modules list --- Gruntfile.js | 73 +++++++++++++++++++++++++--------------------------- package.json | 1 + 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 0026039a816..193a511de02 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,6 +2,7 @@ module.exports = function( grunt ) { "use strict"; var _ = require( "underscore" ), + esprima = require( "esprima" ), cheerio = require( "cheerio" ), replaceCombinedCssReference = function( href, processedName ) { @@ -16,53 +17,49 @@ module.exports = function( grunt ) { // this, we parse js/jquery.mobile.js and reconstruct the array of // dependencies listed therein. makeModulesList = function( modules ) { - var start, end, index, - modulesHash = {}, + 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 + // 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 ) { - 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( /^.\//, "" ); + 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 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 ] ); - } + // 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 re-create the comma- - // separated list and return it. - if ( fixedModules.length === modules.length ) { - modules = fixedModules; - } + // 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; } } } diff --git a/package.json b/package.json index ee4825b1e21..ecd8561f5e4 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "casperjs": "1.1.0-beta3", "cheerio": "0.12.4", "commitplease": "2.0.0", + "esprima": "1.2.2", "grunt": "0.4.2", "grunt-bowercopy": "0.5.0", "grunt-casper": "0.3.2", From f33dc017c9c77bf876cf4b2d4da896856e17be99 Mon Sep 17 00:00:00 2001 From: Gabriel Schulhof Date: Tue, 30 Dec 2014 14:29:14 +0200 Subject: [PATCH 2/3] Build: Create custom CSS for the modules listed in the "modules" option --- Gruntfile.js | 181 ++++++++++++++++++++++++++++++++++++++++++++++++++- package.json | 1 + 2 files changed, 181 insertions(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 193a511de02..8a23e7b1767 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -164,6 +164,11 @@ module.exports = function( grunt ) { } return content; }, + css = require( "css" ), + cssFiles = { + theme: { present: {}, list: [] }, + structure: { present: {}, list: [] } + }, path = require( "path" ), httpPort = Math.floor( 9000 + Math.random()*1000 ), phpPort = Math.floor( 8000 + Math.random()*1000 ), @@ -385,7 +390,180 @@ module.exports = function( grunt ) { endFile: "build/wrap.end" }, + // 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" + onModuleBundleComplete: function() { + var cssFileContents, allFiles, structure, theme, all, + 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; + }; + + // We do nothing unless the "modules" option is defined + if ( grunt.option( "modules" ) ) { + + allFiles = grunt.config( "cssbuild.all.files" ); + + // 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 ); + } + }, + onBuildWrite: function (moduleName, path, contents) { + var index, match, parsedFile, + 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 the "modules" option was used, 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 + if ( grunt.option( "modules" ) ) { + parsedFile = esprima.parse( contents, { comment: 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 contents.replace(/__version__/g, grunt.config.process( "\"<%= version %>\"" )); @@ -1075,7 +1253,8 @@ module.exports = function( grunt ) { "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/package.json b/package.json index ecd8561f5e4..4f5866284b7 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "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", From ba068aa1477c285b6641e30ddd4c5437151d0596 Mon Sep 17 00:00:00 2001 From: Gabriel Schulhof Date: Tue, 30 Dec 2014 21:34:12 +0200 Subject: [PATCH 3/3] Build: Move processing of "modules" option to a task --- Gruntfile.js | 239 +------------------------------------ build/tasks/modules.js | 259 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+), 237 deletions(-) create mode 100644 build/tasks/modules.js diff --git a/Gruntfile.js b/Gruntfile.js index 8a23e7b1767..9d14f141cd9 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,7 +2,6 @@ module.exports = function( grunt ) { "use strict"; var _ = require( "underscore" ), - esprima = require( "esprima" ), cheerio = require( "cheerio" ), replaceCombinedCssReference = function( href, processedName ) { @@ -11,61 +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 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; - }, processDemos = function( content, srcPath ) { var processedName, $; @@ -164,11 +108,6 @@ module.exports = function( grunt ) { } return content; }, - css = require( "css" ), - cssFiles = { - theme: { present: {}, list: [] }, - structure: { present: {}, list: [] } - }, path = require( "path" ), httpPort = Math.floor( 9000 + Math.random()*1000 ), phpPort = Math.floor( 8000 + Math.random()*1000 ), @@ -367,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", @@ -390,180 +327,7 @@ module.exports = function( grunt ) { endFile: "build/wrap.end" }, - // 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" - onModuleBundleComplete: function() { - var cssFileContents, allFiles, structure, theme, all, - 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; - }; - - // We do nothing unless the "modules" option is defined - if ( grunt.option( "modules" ) ) { - - allFiles = grunt.config( "cssbuild.all.files" ); - - // 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 ); - } - }, - onBuildWrite: function (moduleName, path, contents) { - var index, match, parsedFile, - 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 the "modules" option was used, 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 - if ( grunt.option( "modules" ) ) { - parsedFile = esprima.parse( contents, { comment: 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 contents.replace(/__version__/g, grunt.config.process( "\"<%= version %>\"" )); @@ -1247,6 +1011,7 @@ module.exports = function( grunt ) { ]); grunt.registerTask( "dist", [ + "modules", "clean:dist", "config:fetchHeadHash", "js:release", 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 ); + } + }); + }); +};