@@ -12,7 +12,7 @@ import { OptiCSSOptions, Optimizer, parseSelector, postcss } from "opticss";
12
12
import * as path from "path" ;
13
13
14
14
import { RuntimeDataGenerator } from "./RuntimeDataGenerator" ;
15
- import { cssBlocksPostprocessFilename , cssBlocksPreprocessFilename } from "./utils/filepaths" ;
15
+ import { cssBlocksPostprocessFilename , cssBlocksPreprocessFilename , optimizedStylesPostprocessFilepath , optimizedStylesPreprocessFilepath } from "./utils/filepaths" ;
16
16
import { AddonEnvironment } from "./utils/interfaces" ;
17
17
18
18
const debug = debugGenerator ( "css-blocks:ember-app" ) ;
@@ -47,7 +47,7 @@ export class CSSBlocksApplicationPlugin extends Filter {
47
47
let factory = new BlockFactory ( config , postcss ) ;
48
48
let analyzer = new EmberAnalyzer ( factory , this . cssBlocksOptions . analysisOpts ) ;
49
49
let optimizerOptions = this . cssBlocksOptions . optimization ;
50
- this . reserveClassnames ( optimizerOptions ) ;
50
+ this . reserveClassnames ( optimizerOptions , this . cssBlocksOptions . appClasses ) ;
51
51
let optimizer = new Optimizer ( optimizerOptions , analyzer . optimizationOptions ) ;
52
52
let blocksUsed = new Set < Block > ( ) ;
53
53
for ( let entry of entries ) {
@@ -98,6 +98,22 @@ export class CSSBlocksApplicationPlugin extends Filter {
98
98
this . output . writeFileSync ( optLogFileName , optimizationResult . actions . logStrings ( ) . join ( "\n" ) , "utf8" ) ;
99
99
debug ( "Wrote css, sourcemap, and optimization log." ) ;
100
100
101
+ // Also, write out a list of generated classes that we can use later
102
+ // for conflict detection during postprocess.
103
+ const classesUsed : Set < string > = new Set ( ) ;
104
+ optimizationResult . styleMapping . optimizedAttributes . forEach ( attr => {
105
+ classesUsed . add ( attr . value . valueOf ( ) ) ;
106
+ } ) ;
107
+ this . output . writeFileSync (
108
+ optimizedStylesPreprocessFilepath ,
109
+ JSON . stringify (
110
+ new Array ( ...classesUsed . values ( ) ) ,
111
+ undefined ,
112
+ " " ,
113
+ ) ,
114
+ ) ;
115
+ debug ( "Wrote list of generated classes." ) ;
116
+
101
117
let dataGenerator = new RuntimeDataGenerator ( [ ...blocksUsed ] , optimizationResult . styleMapping , analyzer , config , reservedClassnames ) ;
102
118
let data = dataGenerator . generate ( ) ;
103
119
let serializedData = JSON . stringify ( data , undefined , " " ) ;
@@ -116,7 +132,7 @@ export class CSSBlocksApplicationPlugin extends Filter {
116
132
* application to the the list of identifiers that should be omitted by the
117
133
* classname generator.
118
134
*/
119
- reserveClassnames ( optimizerOptions : Partial < OptiCSSOptions > ) : void {
135
+ reserveClassnames ( optimizerOptions : Partial < OptiCSSOptions > , appClassesAlias : string [ ] ) : void {
120
136
let rewriteIdents = optimizerOptions . rewriteIdents ;
121
137
let rewriteIdentsFlag : boolean ;
122
138
let omitIdents : Array < string > ;
@@ -131,7 +147,8 @@ export class CSSBlocksApplicationPlugin extends Filter {
131
147
omitIdents = rewriteIdents . omitIdents && rewriteIdents . omitIdents . class || [ ] ;
132
148
}
133
149
134
- // TODO: scan css files for other classes in use and add them to `omitIdents`.
150
+ // Add in any additional classes that were passed in using the appClasses alias.
151
+ omitIdents . push ( ...appClassesAlias ) ;
135
152
136
153
optimizerOptions . rewriteIdents = {
137
154
id : false ,
@@ -165,17 +182,28 @@ export class CSSBlocksStylesPreprocessorPlugin extends Plugin {
165
182
this . cssBlocksOptions = cssBlocksOptions ;
166
183
}
167
184
async build ( ) {
168
- // Are there any changes to make? If not, bail out early.
169
185
let stylesheetPath = cssBlocksPreprocessFilename ( this . cssBlocksOptions ) ;
170
- let entries = this . input . entries ( "." , { globs : [ stylesheetPath ] } ) ;
186
+
187
+ // Are there any changes to make? If not, bail out early.
188
+ let entries = this . input . entries (
189
+ "." ,
190
+ {
191
+ globs : [
192
+ stylesheetPath ,
193
+ "app/styles/css-blocks-style-mapping.css" ,
194
+ ] ,
195
+ } ,
196
+ ) ;
171
197
let currentFSTree = FSTree . fromEntries ( entries ) ;
172
198
let patch = this . previousSourceTree . calculatePatch ( currentFSTree ) ;
173
199
if ( patch . length === 0 ) {
174
200
return ;
175
201
} else {
176
202
this . previousSourceTree = currentFSTree ;
177
203
}
178
- // Read in the CSS Blocks compiled content that was created previously.
204
+
205
+ // Read in the CSS Blocks compiled content that was created previously
206
+ // from the template tree.
179
207
let blocksFileContents : string ;
180
208
if ( this . input . existsSync ( stylesheetPath ) ) {
181
209
blocksFileContents = this . input . readFileSync ( stylesheetPath , { encoding : "utf8" } ) ;
@@ -185,10 +213,16 @@ export class CSSBlocksStylesPreprocessorPlugin extends Plugin {
185
213
blocksFileContents = "" ;
186
214
}
187
215
188
- // Now, write out compiled content to its expected location.
216
+ // Now, write out compiled content to its expected location in the CSS tree .
189
217
// By default, this is app/styles/css-blocks.css.
190
218
this . output . mkdirSync ( path . dirname ( stylesheetPath ) , { recursive : true } ) ;
191
219
this . output . writeFileSync ( stylesheetPath , blocksFileContents ) ;
220
+
221
+ // Also, forward along the JSON list of optimizer-generated class names.
222
+ if ( this . input . existsSync ( optimizedStylesPreprocessFilepath ) ) {
223
+ const dataContent = this . input . readFileSync ( optimizedStylesPreprocessFilepath ) . toString ( "utf8" ) ;
224
+ this . output . writeFileSync ( optimizedStylesPreprocessFilepath , dataContent ) ;
225
+ }
192
226
}
193
227
}
194
228
@@ -201,10 +235,12 @@ export class CSSBlocksStylesPreprocessorPlugin extends Plugin {
201
235
*/
202
236
export class CSSBlocksStylesPostprocessorPlugin extends Filter {
203
237
env : AddonEnvironment ;
238
+ previousSourceTree : FSTree ;
204
239
205
240
constructor ( env : AddonEnvironment , inputNodes : InputNode [ ] ) {
206
241
super ( mergeTrees ( inputNodes ) , { } ) ;
207
242
this . env = env ;
243
+ this . previousSourceTree = new FSTree ( ) ;
208
244
}
209
245
210
246
processString ( contents : string , _relativePath : string ) : string {
@@ -214,28 +250,57 @@ export class CSSBlocksStylesPostprocessorPlugin extends Filter {
214
250
async build ( ) {
215
251
await super . build ( ) ;
216
252
217
- // Look up all the application style content that's already present.
218
253
const blocksCssFile = cssBlocksPostprocessFilename ( this . env . config ) ;
219
- const appStyles : string [ ] = [ ] ;
254
+ let optimizerClasses : string [ ] = [ ] ;
255
+ const appCss : string [ ] = [ ] ;
220
256
const foundFiles : string [ ] = [ ] ;
257
+ const foundClasses : Set < string > = new Set < string > ( ) ;
258
+ const errorLog : string [ ] = [ ] ;
221
259
260
+ // Are there any changes to make? If not, bail out early.
261
+ let entries = this . input . entries (
262
+ "." ,
263
+ {
264
+ globs : [
265
+ "**/*.css" ,
266
+ optimizedStylesPostprocessFilepath ,
267
+ ] ,
268
+ } ,
269
+ ) ;
270
+ let currentFSTree = FSTree . fromEntries ( entries ) ;
271
+ let patch = this . previousSourceTree . calculatePatch ( currentFSTree ) ;
272
+ if ( patch . length === 0 ) {
273
+ return ;
274
+ } else {
275
+ this . previousSourceTree = currentFSTree ;
276
+ }
277
+
278
+ // Read in the list of classes generated by the optimizer.
279
+ if ( this . input . existsSync ( optimizedStylesPostprocessFilepath ) ) {
280
+ optimizerClasses = JSON . parse ( this . input . readFileSync ( optimizedStylesPostprocessFilepath ) . toString ( "utf8" ) ) ;
281
+ } else {
282
+ // Welp, nothing to do if we don't have optimizer data.
283
+ debug ( "Skipping conflict analysis because there is no optimizer data." ) ;
284
+ return ;
285
+ }
286
+
287
+ // Look up all the application style content that's already present.
222
288
const walkEntries = this . input . entries ( undefined , {
223
289
globs : [ "**/*.css" ] ,
224
290
} ) ;
225
291
walkEntries . forEach ( entry => {
226
292
if ( entry . relativePath === blocksCssFile ) return ;
227
293
try {
228
- appStyles . push ( this . input . readFileSync ( entry . relativePath ) . toString ( "utf8" ) ) ;
294
+ appCss . push ( this . input . readFileSync ( entry . relativePath ) . toString ( "utf8" ) ) ;
229
295
foundFiles . push ( entry . relativePath ) ;
230
296
} catch ( e ) {
231
297
// broccoli-concat will complain about this later. let's move on.
232
298
}
233
299
} ) ;
300
+ debug ( "Done looking up app CSS." ) ;
234
301
235
- // Now, read in each of these sources and check there are no class name conflicts.
236
- const foundClasses : Set < string > = new Set < string > ( ) ;
237
- const errorLog : string [ ] = [ ] ;
238
- appStyles . forEach ( content => {
302
+ // Now, read in each of these sources and note all classes found.
303
+ appCss . forEach ( content => {
239
304
try {
240
305
const parsed = postcss . parse ( content ) ;
241
306
parsed . walkRules ( rule => {
@@ -249,19 +314,38 @@ export class CSSBlocksStylesPostprocessorPlugin extends Filter {
249
314
} ) ;
250
315
} ) ;
251
316
} catch ( e ) {
252
- // TODO: Can't parse CSS? Gracefully fail or crash and burn?
317
+ // Can't parse CSS? We'll skip it and add a warning to the log.
253
318
errorLog . push ( e . toString ( ) ) ;
319
+ debug ( `Ran into an error when parsing CSS content for conflict analysis! Review the error log for details.` ) ;
254
320
}
255
321
} ) ;
322
+ debug ( "Done finding app classes." ) ;
323
+
324
+ // Find collisions between the app styles and optimizer styles.
325
+ const collisions = optimizerClasses . filter ( val => foundClasses . has ( val ) ) ;
326
+ debug ( "Done identifying collisions." ) ;
256
327
257
328
// Build a logfile for the output tree, for debugging.
258
- let logfile = "FOUND CLASSES:\n" ;
329
+ let logfile = "COLLISIONS:\n" ;
330
+ collisions . forEach ( cssClass => { logfile += `${ cssClass } \n` ; } ) ;
331
+ logfile += "\nFOUND APP CLASSES:\n" ;
259
332
foundClasses . forEach ( cssClass => { logfile += `${ cssClass } \n` ; } ) ;
260
333
logfile += "\nFOUND FILES:\n" ;
261
334
foundFiles . forEach ( file => { logfile += `${ file } \n` ; } ) ;
262
335
logfile += "\nERRORS:\n" ;
263
336
errorLog . forEach ( err => { logfile += `${ err } \n` ; } ) ;
264
337
this . output . writeFileSync ( "assets/app-classes.log" , logfile ) ;
338
+ debug ( "Wrote log file to broccoli tree." ) ;
339
+
340
+ if ( collisions . length > 0 ) {
341
+ throw new Error (
342
+ "Your application CSS contains classes that are also generated by the CSS optimizer. This can cause style conflicts between your application's classes and those generated by CSS Blocks.\n" +
343
+ "To resolve this conflict, you should add any short class names in non-block CSS (~5 characters or less) to the list of disallowed classes in your build configuration.\n" +
344
+ "(You can do this by setting css-blocks.appClasses to an array of disallowed classes in ember-cli-build.js.)\n\n" +
345
+ "Conflicting classes:\n" +
346
+ collisions . reduce ( ( prev , curr ) => prev += `${ curr } \n` , "" ) ,
347
+ ) ;
348
+ }
265
349
}
266
350
}
267
351
0 commit comments