|
| 1 | +#!/usr/bin/env node |
| 2 | +module.exports = function( grunt ) { |
| 3 | + "use strict"; |
| 4 | + |
| 5 | + var css = require( "css" ), |
| 6 | + esprima = require( "esprima" ), |
| 7 | + path = require( "path" ), |
| 8 | + cssFiles = { |
| 9 | + theme: { present: {}, list: [] }, |
| 10 | + structure: { present: {}, list: [] } |
| 11 | + }; |
| 12 | + |
| 13 | + // Ensure that modules specified via the --modules option are in the same |
| 14 | + // order as the one in which they appear in js/jquery.mobile.js. To achieve |
| 15 | + // this, we parse js/jquery.mobile.js and reconstruct the array of |
| 16 | + // dependencies listed therein. |
| 17 | + function makeModulesList( modules ) { |
| 18 | + var parsedFile, desiredModulesHash, listedModules, index, singleListedModule, |
| 19 | + fixedModules = [], |
| 20 | + jsFile = grunt.file.read( path.join( "js", "jquery.mobile.js" ) ); |
| 21 | + |
| 22 | + modules = modules.split( "," ); |
| 23 | + |
| 24 | + // This is highly dependent on the contents of js/jquery.mobile.js. It assumes that all |
| 25 | + // dependencies are listed flatly in the first argument of the first expression in the |
| 26 | + // file. |
| 27 | + if ( jsFile ) { |
| 28 | + parsedFile = esprima.parse( jsFile, { raw: true, comment: true } ); |
| 29 | + |
| 30 | + // Descend into the parsed file to grab the array of deps |
| 31 | + if ( parsedFile && parsedFile.body && parsedFile.body.length > 0 && |
| 32 | + parsedFile.body[ 0 ] && parsedFile.body[ 0 ].expression && |
| 33 | + parsedFile.body[ 0 ].expression.arguments && |
| 34 | + parsedFile.body[ 0 ].expression.arguments.length && |
| 35 | + parsedFile.body[ 0 ].expression.arguments.length > 0 && |
| 36 | + parsedFile.body[ 0 ].expression.arguments[ 0 ] && |
| 37 | + parsedFile.body[ 0 ].expression.arguments[ 0 ].elements && |
| 38 | + parsedFile.body[ 0 ].expression.arguments[ 0 ].elements.length > 0 ) { |
| 39 | + |
| 40 | + listedModules = parsedFile.body[ 0 ].expression.arguments[ 0 ].elements; |
| 41 | + desiredModulesHash = {}; |
| 42 | + |
| 43 | + // Convert list of desired modules to a hash |
| 44 | + for ( index = 0 ; index < modules.length ; index++ ) { |
| 45 | + desiredModulesHash[ modules[ index ] ] = true; |
| 46 | + } |
| 47 | + |
| 48 | + // Then, if a listed module is in the hash of desired modules, add it to the |
| 49 | + // list containing the desired modules in the correct order |
| 50 | + for ( index = 0 ; index < listedModules.length ; index++ ) { |
| 51 | + singleListedModule = listedModules[ index ].value.replace( /^\.\//, "" ); |
| 52 | + if ( desiredModulesHash[ singleListedModule ] ) { |
| 53 | + fixedModules.push( singleListedModule ); |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + // If we've found all the desired modules we can return the list of modules |
| 58 | + // assembled, because that list contains the modules in the correct order. |
| 59 | + if ( fixedModules.length === modules.length ) { |
| 60 | + modules = fixedModules; |
| 61 | + } |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + return modules; |
| 66 | + }; |
| 67 | + |
| 68 | + grunt.registerTask( "modules", function() { |
| 69 | + var modulesList = grunt.option( "modules" ), |
| 70 | + requirejsModules = grunt.config( "requirejs.js.options.include" ), |
| 71 | + onBuildWrite = grunt.config( "requirejs.js.options.onBuildWrite" ), |
| 72 | + onModuleBundleComplete = grunt.config( "requirejs.js.options.onModuleBundleComplete" ); |
| 73 | + |
| 74 | + if ( !modulesList ) { |
| 75 | + return; |
| 76 | + } |
| 77 | + |
| 78 | + if ( !requirejsModules ) { |
| 79 | + throw( new Error( "Missing configuration key 'requirejs.js.options.include" ) ); |
| 80 | + } |
| 81 | + |
| 82 | + grunt.config( "requirejs.js.options.include", makeModulesList( modulesList ) ); |
| 83 | + |
| 84 | + grunt.config( "requirejs.js.options.onBuildWrite", function( moduleName, path, contents ) { |
| 85 | + var index, match, |
| 86 | + |
| 87 | + // We parse the file for the special comments in order to assemble a list of |
| 88 | + // structure and theme CSS files to serve as the basis for custom theme and |
| 89 | + // structure files which we then feed to the optimizer |
| 90 | + parsedFile = esprima.parse( contents, { comment: true } ), |
| 91 | + addCSSFile = function( file ) { |
| 92 | + file = file.trim(); |
| 93 | + if ( !cssFiles[ match[ 1 ] ].present[ file ] ) { |
| 94 | + cssFiles[ match[ 1 ] ].list.push( file ); |
| 95 | + cssFiles[ match[ 1 ] ].present[ file ] = true; |
| 96 | + } |
| 97 | + }; |
| 98 | + |
| 99 | + if ( parsedFile.comments && parsedFile.comments.length > 0 ) { |
| 100 | + for ( index = 0 ; index < parsedFile.comments.length ; index++ ) { |
| 101 | + match = parsedFile.comments[ index ].value |
| 102 | + .match( /^>>css\.(theme|structure): (.*)/ ); |
| 103 | + |
| 104 | + // Parse the special comment and add the files listed on the right hand |
| 105 | + // side of the flag to the appropriate list of CSS files |
| 106 | + if ( match && match.length > 2 ) { |
| 107 | + match[ 2 ].split( "," ).forEach( addCSSFile ); |
| 108 | + } |
| 109 | + } |
| 110 | + } |
| 111 | + |
| 112 | + return onBuildWrite ? onBuildWrite.apply( this, arguments ) : contents; |
| 113 | + }); |
| 114 | + |
| 115 | + |
| 116 | + grunt.config( "requirejs.js.options.onModuleBundleComplete", function() { |
| 117 | + |
| 118 | + // We assume that the source for the structure file is called |
| 119 | + // "jquery.mobile.structure.css", that the source for the theme file is called |
| 120 | + // "jquery.mobile.theme.css", and that the source for the combined theme+structure file |
| 121 | + // is called "jquery.mobile.css" |
| 122 | + var cssFileContents, structure, theme, all, |
| 123 | + allFiles = grunt.config( "cssbuild.all.files" ), |
| 124 | + destinationPath = grunt.config.process( "<%= dirs.tmp %>" ), |
| 125 | + |
| 126 | + // Traverse the tree produced by the CSS parser and update import paths |
| 127 | + updateImportUrl = function( cssFilePath, cssRoot ) { |
| 128 | + var index, item, match, filename; |
| 129 | + |
| 130 | + for ( index in cssRoot ) { |
| 131 | + item = cssRoot[ index ]; |
| 132 | + |
| 133 | + if ( item && item.type === "import" ) { |
| 134 | + |
| 135 | + // NB: The regex below assumes there's no whitespace in the |
| 136 | + // @import reference, i.e. url("path/to/filename"); |
| 137 | + match = item.import.match( /(url\()(.*)(\))$/ ); |
| 138 | + if ( match ) { |
| 139 | + |
| 140 | + // Strip the quotes from around the filename |
| 141 | + filename = match[ 2 ] |
| 142 | + .substr( 1, match[ 2 ].length - 2 ); |
| 143 | + |
| 144 | + // Replace theme and structure with our custom |
| 145 | + // reference |
| 146 | + if ( path.basename( filename ) === |
| 147 | + "jquery.mobile.theme.css" ) { |
| 148 | + item.import = |
| 149 | + "url(\"jquery.mobile.custom.theme.css\")"; |
| 150 | + } else if ( path.basename( filename ) === |
| 151 | + "jquery.mobile.structure.css" ) { |
| 152 | + item.import = |
| 153 | + "url(\"jquery.mobile.custom.structure.css\")"; |
| 154 | + |
| 155 | + // Adjust the relative path for all other imports |
| 156 | + } else { |
| 157 | + item.import = |
| 158 | + |
| 159 | + // url( |
| 160 | + match[ 1 ] + |
| 161 | + |
| 162 | + // quotation mark |
| 163 | + match[ 2 ].charAt( 0 ) + |
| 164 | + |
| 165 | + // path adjusted to be relative to the |
| 166 | + // temporary directory |
| 167 | + path.relative( destinationPath, |
| 168 | + path.normalize( path.join( cssFilePath, |
| 169 | + filename ) ) ) + |
| 170 | + |
| 171 | + // quotation mark |
| 172 | + match[ 2 ].charAt( 0 ) + |
| 173 | + |
| 174 | + // ) |
| 175 | + match[ 3 ]; |
| 176 | + } |
| 177 | + } |
| 178 | + } else if ( typeof item === "object" ) { |
| 179 | + updateImportUrl( cssFilePath, item ); |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + return cssRoot; |
| 184 | + }; |
| 185 | + |
| 186 | + // Find the entries for the structure, the theme, and the combined |
| 187 | + // theme+structure file, because we want to update them to point to our |
| 188 | + // custom-built version |
| 189 | + allFiles.forEach( function( singleCSSFile ) { |
| 190 | + if ( path.basename( singleCSSFile.src ) === |
| 191 | + "jquery.mobile.structure.css" ) { |
| 192 | + structure = singleCSSFile; |
| 193 | + } else if ( path.basename( singleCSSFile.src ) === |
| 194 | + "jquery.mobile.theme.css" ) { |
| 195 | + theme = singleCSSFile; |
| 196 | + } else if ( path.basename( singleCSSFile.src ) === |
| 197 | + "jquery.mobile.css" ) { |
| 198 | + all = singleCSSFile; |
| 199 | + } |
| 200 | + }); |
| 201 | + |
| 202 | + // Create temporary structure file and update the grunt config |
| 203 | + // reference |
| 204 | + cssFileContents = ""; |
| 205 | + if ( cssFiles.structure.list.length > 0 ) { |
| 206 | + cssFiles.structure.list.forEach( function( file ) { |
| 207 | + |
| 208 | + // Recalculate relative path from destination in the temporary |
| 209 | + // directory |
| 210 | + file = path.relative( destinationPath, |
| 211 | + |
| 212 | + // css files are originally relative to "js/" |
| 213 | + path.join( "js", file ) ); |
| 214 | + cssFileContents += "@import url(\"" + file + "\");\n"; |
| 215 | + }); |
| 216 | + structure.src = path.join( destinationPath, |
| 217 | + "jquery.mobile.custom.structure.css" ); |
| 218 | + grunt.file.write( structure.src, cssFileContents, |
| 219 | + { encoding: "utf8" } ); |
| 220 | + } |
| 221 | + |
| 222 | + // Create temporary theme file and update the grunt config reference |
| 223 | + cssFileContents = ""; |
| 224 | + if ( cssFiles.theme.list.length > 0 ) { |
| 225 | + cssFiles.theme.list.forEach( function( file ) { |
| 226 | + |
| 227 | + // Recalculate relative path from destination in the temporary |
| 228 | + // directory |
| 229 | + file = path.relative( destinationPath, |
| 230 | + |
| 231 | + // css files are originally relative to "js/" |
| 232 | + path.join( "js", file ) ); |
| 233 | + cssFileContents += "@import url(\"" + file + "\");\n"; |
| 234 | + }); |
| 235 | + theme.src = path.join( destinationPath, |
| 236 | + "jquery.mobile.custom.theme.css" ); |
| 237 | + grunt.file.write( theme.src, cssFileContents, |
| 238 | + { encoding: "utf8" } ); |
| 239 | + } |
| 240 | + |
| 241 | + // Create temporary theme+structure file by replacing references to the |
| 242 | + // standard theme and structure files with references to the custom |
| 243 | + // theme and structure files created above, and update the grunt config |
| 244 | + // reference |
| 245 | + cssFileContents = css.stringify( updateImportUrl( |
| 246 | + path.dirname( all.src ), |
| 247 | + css.parse( grunt.file.read( all.src, { encoding: "utf8" } ) ) ) ); |
| 248 | + all.src = path.join( destinationPath, "jquery.mobile.custom.css" ); |
| 249 | + grunt.file.write( all.src, cssFileContents, { encoding: "utf8" } ); |
| 250 | + |
| 251 | + // Update grunt configuration |
| 252 | + grunt.config( "cssbuild.all.files", allFiles ); |
| 253 | + |
| 254 | + if ( onModuleBundleComplete ) { |
| 255 | + return onModuleBundleComplete.apply( this, arguments ); |
| 256 | + } |
| 257 | + }); |
| 258 | + }); |
| 259 | +}; |
0 commit comments