Skip to content

Commit f38f22d

Browse files
author
Gabriel Schulhof
committed
Build: Create custom CSS for the modules listed in the "modules" option
Closes jquery-archivegh-7892
1 parent 780f0e4 commit f38f22d

File tree

3 files changed

+265
-63
lines changed

3 files changed

+265
-63
lines changed

Gruntfile.js

Lines changed: 4 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -10,65 +10,6 @@ module.exports = function( grunt ) {
1010
.replace( /\.\.\/css/, "css" )
1111
.replace( /jquery\.mobile\.css/, processedName + ".min.css" );
1212
},
13-
14-
// Ensure that modules specified via the --modules option are in the same
15-
// order as the one in which they appear in js/jquery.mobile.js. To achieve
16-
// this, we parse js/jquery.mobile.js and reconstruct the array of
17-
// dependencies listed therein.
18-
makeModulesList = function( modules ) {
19-
var start, end, index,
20-
modulesHash = {},
21-
fixedModules = [],
22-
jsFile = grunt.file.read( path.join( "js", "jquery.mobile.js" ) );
23-
24-
modules = modules.split( "," );
25-
26-
// This is highly dependent on the contents of js/jquery.mobile.js
27-
if ( jsFile ) {
28-
start = jsFile.indexOf( "[" );
29-
if ( start > -1 ) {
30-
start++;
31-
end = jsFile.indexOf( "]" );
32-
if ( start < jsFile.length &&
33-
end > -1 && end < jsFile.length && end > start ) {
34-
35-
// Convert list of desired modules to a hash
36-
for ( index = 0 ; index < modules.length ; index++ ) {
37-
modulesHash[ modules[ index ] ] = true;
38-
}
39-
40-
// Split list of modules from js/jquery.mobile.js into an array
41-
jsFile = jsFile
42-
.slice( start, end )
43-
.match( /"[^"]*"/gm );
44-
45-
// Add each desired module to the fixed list of modules in the
46-
// correct order
47-
for ( index = 0 ; index < jsFile.length ; index++ ) {
48-
49-
// First we need to touch up each module from js/jquery.mobile.js
50-
jsFile[ index ] = jsFile[ index ]
51-
.replace( /"/g, "" )
52-
.replace( /^.\//, "" );
53-
54-
// Then, if it's in the hash of desired modules, add it to the
55-
// list containing the desired modules in the correct order
56-
if ( modulesHash[ jsFile[ index ] ] ) {
57-
fixedModules.push( jsFile[ index ] );
58-
}
59-
}
60-
61-
// If we've found all the desired modules, we re-create the comma-
62-
// separated list and return it.
63-
if ( fixedModules.length === modules.length ) {
64-
modules = fixedModules;
65-
}
66-
}
67-
}
68-
}
69-
70-
return modules;
71-
},
7213
processDemos = function( content, srcPath ) {
7314
var processedName, $;
7415

@@ -365,9 +306,7 @@ module.exports = function( grunt ) {
365306

366307
mainConfigFile: "js/requirejs.config.js",
367308

368-
include: ( grunt.option( "modules" ) ?
369-
makeModulesList( grunt.option( "modules" ) ) :
370-
[ "jquery.mobile" ] ),
309+
include: [ "jquery.mobile" ],
371310

372311
exclude: [
373312
"jquery",
@@ -1072,13 +1011,15 @@ module.exports = function( grunt ) {
10721011
]);
10731012

10741013
grunt.registerTask( "dist", [
1014+
"modules",
10751015
"clean:dist",
10761016
"config:fetchHeadHash",
10771017
"js:release",
10781018
"css:release",
10791019
"demos",
10801020
"compress:dist",
1081-
"compress:images"
1021+
"compress:images",
1022+
"clean:tmp"
10821023
]);
10831024
grunt.registerTask( "dist:release", [ "release:init", "dist", "cdn" ] );
10841025
grunt.registerTask( "dist:git", [ "dist", "clean:git", "config:copy:git:-git", "copy:git" ] );

build/tasks/modules.js

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
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+
};

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
"casperjs": "1.1.0-beta3",
2828
"cheerio": "0.12.4",
2929
"commitplease": "2.0.0",
30+
"css": "2.1.0",
31+
"esprima": "1.2.2",
3032
"grunt": "0.4.2",
3133
"grunt-bowercopy": "0.5.0",
3234
"grunt-casper": "0.3.2",

0 commit comments

Comments
 (0)