@@ -8,10 +8,12 @@ import Plugin = require("broccoli-plugin");
8
8
import type { PluginOptions } from "broccoli-plugin/dist/interfaces" ;
9
9
import debugGenerator from "debug" ;
10
10
import * as FSTree from "fs-tree-diff" ;
11
- import { OptiCSSOptions , Optimizer , postcss } from "opticss" ;
11
+ 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" ;
16
+ import { AddonEnvironment } from "./utils/interfaces" ;
15
17
16
18
const debug = debugGenerator ( "css-blocks:ember-app" ) ;
17
19
@@ -83,7 +85,7 @@ export class CSSBlocksApplicationPlugin extends Filter {
83
85
}
84
86
debug ( `Loaded ${ blocksUsed . size } blocks.` ) ;
85
87
debug ( `Loaded ${ optimizer . analyses . length } analyses.` ) ;
86
- let cssFileName = cssBlocksOutputFilename ( this . cssBlocksOptions ) ;
88
+ let cssFileName = cssBlocksPreprocessFilename ( this . cssBlocksOptions ) ;
87
89
let optLogFileName = `${ cssFileName } .optimization.log` ;
88
90
let optimizationResult = await optimizer . optimize ( cssFileName ) ;
89
91
debug ( `Optimized CSS. There were ${ optimizationResult . actions . performed . length } optimizations performed.` ) ;
@@ -164,7 +166,7 @@ export class CSSBlocksStylesPreprocessorPlugin extends Plugin {
164
166
}
165
167
async build ( ) {
166
168
// Are there any changes to make? If not, bail out early.
167
- let stylesheetPath = cssBlocksOutputFilename ( this . cssBlocksOptions ) ;
169
+ let stylesheetPath = cssBlocksPreprocessFilename ( this . cssBlocksOptions ) ;
168
170
let entries = this . input . entries ( "." , { globs : [ stylesheetPath ] } ) ;
169
171
let currentFSTree = FSTree . fromEntries ( entries ) ;
170
172
let patch = this . previousSourceTree . calculatePatch ( currentFSTree ) ;
@@ -190,6 +192,79 @@ export class CSSBlocksStylesPreprocessorPlugin extends Plugin {
190
192
}
191
193
}
192
194
195
+ /**
196
+ * Plugin for the CSS postprocess tree. This plugin scans for classes declared
197
+ * in application CSS (outside of CSS Blocks) and checks if there are any
198
+ * duplicates between the app code and the classes generated by the optimizer.
199
+ *
200
+ * This plugin is only run for builds where the optimizer is enabled.
201
+ */
202
+ export class CSSBlocksStylesPostprocessorPlugin extends Filter {
203
+ env : AddonEnvironment ;
204
+
205
+ constructor ( env : AddonEnvironment , inputNodes : InputNode [ ] ) {
206
+ super ( mergeTrees ( inputNodes ) , { } ) ;
207
+ this . env = env ;
208
+ }
209
+
210
+ processString ( contents : string , _relativePath : string ) : string {
211
+ return contents ;
212
+ }
213
+
214
+ async build ( ) {
215
+ await super . build ( ) ;
216
+
217
+ // Look up all the application style content that's already present.
218
+ const blocksCssFile = cssBlocksPostprocessFilename ( this . env . config ) ;
219
+ const appStyles : string [ ] = [ ] ;
220
+ const foundFiles : string [ ] = [ ] ;
221
+
222
+ const walkEntries = this . input . entries ( undefined , {
223
+ globs : [ "**/*.css" ] ,
224
+ } ) ;
225
+ walkEntries . forEach ( entry => {
226
+ if ( entry . relativePath === blocksCssFile ) return ;
227
+ try {
228
+ appStyles . push ( this . input . readFileSync ( entry . relativePath ) . toString ( "utf8" ) ) ;
229
+ foundFiles . push ( entry . relativePath ) ;
230
+ } catch ( e ) {
231
+ // broccoli-concat will complain about this later. let's move on.
232
+ }
233
+ } ) ;
234
+
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 => {
239
+ try {
240
+ const parsed = postcss . parse ( content ) ;
241
+ parsed . walkRules ( rule => {
242
+ const selectors = parseSelector ( rule . selector ) ;
243
+ selectors . forEach ( sel => {
244
+ sel . eachSelectorNode ( node => {
245
+ if ( node . type === "class" ) {
246
+ foundClasses . add ( node . value ) ;
247
+ }
248
+ } ) ;
249
+ } ) ;
250
+ } ) ;
251
+ } catch ( e ) {
252
+ // TODO: Can't parse CSS? Gracefully fail or crash and burn?
253
+ errorLog . push ( e . toString ( ) ) ;
254
+ }
255
+ } ) ;
256
+
257
+ // Build a logfile for the output tree, for debugging.
258
+ let logfile = "FOUND CLASSES:\n" ;
259
+ foundClasses . forEach ( cssClass => { logfile += `${ cssClass } \n` ; } ) ;
260
+ logfile += "\nFOUND FILES:\n" ;
261
+ foundFiles . forEach ( file => { logfile += `${ file } \n` ; } ) ;
262
+ logfile += "\nERRORS:\n" ;
263
+ errorLog . forEach ( err => { logfile += `${ err } \n` ; } ) ;
264
+ this . output . writeFileSync ( "assets/app-classes.log" , logfile ) ;
265
+ }
266
+ }
267
+
193
268
/**
194
269
* Given CSS and a sourcemap, append an embedded sourcemap (base64 encoded)
195
270
* to the end of the css.
@@ -205,14 +280,3 @@ function addSourcemapInfoToOptimizedCss(css: string, sourcemap?: string) {
205
280
const encodedSourcemap = Buffer . from ( sourcemap ) . toString ( "base64" ) ;
206
281
return `${ css } \n/*# sourceMappingURL=data:application/json;base64,${ encodedSourcemap } */` ;
207
282
}
208
-
209
- /**
210
- * Generate the output path for the compiled CSS Blocks content, using the
211
- * preferred filename given by the user. If none is given, the default
212
- * path is "app/styles/css-blocks.css".
213
- * @param options - The options passed to the Ember plugin.
214
- * @returns - The path for the CSS Blocks compiled content.
215
- */
216
- function cssBlocksOutputFilename ( options : ResolvedCSSBlocksEmberOptions ) {
217
- return `app/styles/${ options . output } ` ;
218
- }
0 commit comments