import { Analysis , Analyzer as AnalyzerType , Block , BlockCompiler , Options as CSSBlocksOptions , StyleMapping , resolveConfiguration , } from "@css-blocks/core" ; import { TemplateTypes } from "@opticss/template-api" ; import { ObjectDictionary , flatten , objectValues } from "@opticss/util" ; import * as debugGenerator from "debug" ; import { LegacyRawSourceMap , adaptToLegacySourceMap , postcss } from "opticss" ; import { Actions , DEFAULT_OPTIONS , OptiCSSOptions , OptimizationResult , Optimizer , } from "opticss" ; import * as path from "path" ; import { RawSourceMap } from "source-map" ; import * as Tapable from "tapable" ; import { Compiler as WebpackCompiler , Plugin as WebpackPlugin } from "webpack" ; import { RawSource , Source , SourceMapSource } from "webpack-sources" ; import { LoaderContext } from "./context" ; export type TmpType = keyof TemplateTypes ; export type Analyzer = AnalyzerType < TmpType > ; export type PendingResult = Promise < StyleMapping < TmpType > | void > ; export interface CssBlocksWebpackOptions { name ? : string ; analyzer : Analyzer ; outputCssFile ? : string ; compilationOptions ? : CSSBlocksOptions ; optimization ? : OptiCSSOptions ; } export type WebpackAny = any ; export interface BlockCompilationError { compilation : WebpackAny ; assetPath : string ; error : Error ; mapping ? : StyleMapping < TmpType > ; optimizerActions ? : Actions ; } export interface BlockCompilationComplete { compilation : WebpackAny ; assetPath : string ; mapping : StyleMapping < TmpType > ; optimizerActions : Actions ; } type Assets = ObjectDictionary < Source > ; type EntryTypes = string | string [ ] | ObjectDictionary < string > ; interface CompilationResult { optimizationResult : OptimizationResult ; blocks : Set < Block > ; analyses : Array < Analysis < TmpType > > ; } export class CssBlocksPlugin extends Tapable implements WebpackPlugin { optimizationOptions : OptiCSSOptions ; name : string ; analyzer : Analyzer ; projectDir : string ; outputCssFile : string ; compilationOptions : CSSBlocksOptions ; pendingResult ? : PendingResult ; debug : debugGenerator . IDebugger ; constructor ( options ) { super ( ) ; this . debug = debugGenerator ( "css-blocks:webpack" ) ; this . analyzer = options . analyzer ; this . outputCssFile = options . outputCssFile || "css-blocks.css" ; this . name = options . name || this . outputCssFile ; this . compilationOptions = options . compilationOptions || { } ; this . projectDir = process . cwd ( ) ; this . optimizationOptions = Object . assign ( { } , DEFAULT_OPTIONS , options . optimization ) ; } private async handleMake ( outputPath , assets , compilation , cb ) { this . trace ( `starting analysis.` ) ; this . analyzer . reset ( ) ; let webpackEntry = compilation . options . entry as EntryTypes ; let entries = [ ] ; if ( typeof webpackEntry === "string" ) { entries = [ webpackEntry ] ; } else if ( Array . isArray ( webpackEntry ) ) { entries = webpackEntry ; } else if ( typeof webpackEntry === "object" ) { entries = flatten < string > ( objectValues ( webpackEntry ) ) ; } entries = entries . filter ( entry => ! /\/webpack-dev-server\/|^webpack\/hot\/dev-server$/ . test ( entry ) ) ; let pending = this . analyzer . analyze ( "" , entries ) . catch ( ( err ) => { this . trace ( `Error during analysis. Draining queue.` ) ; return this . analyzer . blockFactory . prepareForExit ( ) . then ( ( ) => { this . trace ( `Drained. Raising error.` ) ; throw err ; } ) ; } ) . then ( ( analysis ) => { return this . compileBlocks ( analysis , path . join ( outputPath , this . outputCssFile ) ) ; } ) . then ( ( result ) => { this . trace ( `setting css asset: ${ this . outputCssFile }` ) ; let source ; if ( result . optimizationResult . output . sourceMap ) { let resultMap = result . optimizationResult . output . sourceMap ; let rawSourceMap ; if ( typeof resultMap === "string" ) { rawSourceMap = JSON . parse ( resultMap ) ; } else { rawSourceMap = resultMap ; } source = new SourceMapSource ( result . optimizationResult . output . content . toString ( ) , "optimized css" , adaptToLegacySourceMap ( rawSourceMap ) ) ; } else { source = new RawSource ( result . optimizationResult . output . content . toString ( ) ) ; } assets [ `${ this . outputCssFile }.log` ] = new RawSource ( result . optimizationResult . actions . performed . map ( a => a . logString ( ) ) . join ( "\n" ) ) ; assets [ this . outputCssFile ] = source ; let completion = { compilation : compilation , assetPath : this . outputCssFile , mapping : new StyleMapping ( result . optimizationResult . styleMapping , result . blocks , resolveConfiguration ( this . compilationOptions ) , result . analyses ) , optimizerActions : result . optimizationResult . actions , } ; return completion ; } ) . then ( ( completion ) => { this . trace ( `notifying of completion` ) ; this . notifyComplete ( completion , cb ) ; this . trace ( `notified of completion` ) ; return completion ; } ) . then ( ( compilationResult ) : StyleMapping < TmpType > => { return compilationResult . mapping ; } ) . catch ( ( error ) => { this . trace ( `notifying of compilation failure` ) ; compilation . errors . push ( error ) ; this . notifyComplete ( { error , compilation , assetPath : this . outputCssFile , } , cb ) ; this . trace ( `notified of compilation failure` ) ; } ) ; this . trace ( `notifying of pending compilation` ) ; this . notifyPendingCompilation ( pending ) ; this . trace ( `notified of pending compilation` ) ; } apply ( compiler ) { this . projectDir = compiler . options . context || this . projectDir ; let outputPath = compiler . options . output && compiler . options . output . path || this . projectDir ; let assets = { } ; compiler . plugin ( "this-compilation" , ( compilation ) => { this . notifyCompilationExpiration ( ) ; compilation . plugin ( "additional-assets" , ( cb ) => { Object . assign ( compilation . assets , assets ) ; cb ( ) ; } ) ; } ) ; compiler . plugin ( "make" , this . handleMake . bind ( this , outputPath , assets ) ) ; compiler . plugin ( "emit" , ( compilation , callback ) => { let discoveredFiles = [ ... this . analyzer . transitiveBlockDependencies ( ) ] . map ( ( b ) => b . identifier ) ; compilation . fileDependencies . push ( ... discoveredFiles ) ; callback ( ) ; } ) ; this . onCompilationExpiration ( ( ) => { this . trace ( `resetting pending compilation.` ) ; this . pendingResult = undefined ; } ) ; this . onPendingCompilation ( ( pendingResult ) => { this . trace ( `received pending compilation.` ) ; this . pendingResult = pendingResult ; } ) ; compiler . plugin ( "compilation" , ( compilation ) => { compilation . plugin ( "normal-module-loader" , ( context , mod ) => { this . trace ( `preparing normal-module-loader for ${ mod . resource }` ) ; context . cssBlocks = context . cssBlocks || { mappings : { } , compilationOptions : this . compilationOptions } ; if ( context . cssBlocks . mappings [ this . outputCssFile ] ) { throw new Error ( `css conflict detected. Multiple compiles writing to ${ this . outputCssFile }?` ) ; } if ( this . pendingResult === undefined ) { throw new Error ( `No pending result is available yet.` ) ; } context . cssBlocks . mappings [ this . outputCssFile ] = this . pendingResult ; } ) ; } ) ; } private compileBlocks ( analyzer , cssOutputName ) { let options = resolveConfiguration ( this . compilationOptions ) ; let blockCompiler = new BlockCompiler ( postcss , options ) ; let numBlocks = 0 ; let optimizer = new Optimizer ( this . optimizationOptions , analyzer . optimizationOptions ) ; let blocks = analyzer . transitiveBlockDependencies ( ) ; for ( let block of blocks ) { if ( block . stylesheet && block . identifier ) { blocks . add ( block ) ; this . trace ( `compiling ${ block . identifier }.` ) ; let root = blockCompiler . compile ( block , block . stylesheet , analyzer . reservedClassNames ( ) ) ; let result = root . toResult ( { to : cssOutputName , map : { inline : false , annotation : false } } ) ; let filename = options . importer . filesystemPath ( block . identifier , options ) || options . importer . debugIdentifier ( block . identifier , options ) ; optimizer . addSource ( { content : result . css , filename , sourceMap : result . map . toJSON ( ) , } ) ; numBlocks ++ ; } } let analyses = analyzer . analyses ( ) ; for ( let a of analyses ) { this . trace ( `Adding analysis for ${ a . template . identifier } to optimizer.` ) ; this . trace ( `Analysis for ${ a . template . identifier } has ${ a . elementCount ( ) } elements.` ) ; optimizer . addAnalysis ( a . forOptimizer ( options ) ) ; } this . trace ( `compiled ${ numBlocks } blocks.` ) ; this . debug ( "optimization starting." ) ; return optimizer . optimize ( cssOutputName ) . then ( optimizationResult => { this . debug ( "optimization complete." ) ; return { optimizationResult , blocks , analyses , } ; } ) ; } trace ( message ) { message = message . replace ( this . projectDir + "/" , "" ) ; this . debug ( `[${ this . name }] ${ message }` ) ; } /**
     * Fires when the compilation promise is available.
     */ onPendingCompilation ( handler ) { this . plugin ( "block-compilation-pending" , handler ) ; } private notifyPendingCompilation ( pendingResult ) { this . applyPlugins ( "block-compilation-pending" , pendingResult ) ; } /**
     * Fires when the compilation is first started to let any listeners know that
     * their current promise is no longer valid.
     */ onCompilationExpiration ( handler ) { this . plugin ( "block-compilation-expired" , handler ) ; } private notifyCompilationExpiration ( ) { this . applyPlugins ( "block-compilation-expired" ) ; } /**
     * Fires when the compilation is done.
     */ onComplete ( handler ) { this . plugin ( "block-compilation-complete" , handler ) ; } private notifyComplete ( result , cb ) { this . applyPluginsAsync ( "block-compilation-complete" , result , cb ) ; } }