import { StyleMapping , TemplateAnalysis , TemplateIntegrationOptions , TemplateTypes , normalizeTemplateOptions , } from "@opticss/template-api" ; import Concat = require ( "concat-with-sourcemaps" ) ; import * as postcss from "postcss" ; import { Actions } from "./Actions" ; import { CssFile , ParsedCssFile } from "./CssFile" ; import { Initializers , initializers } from "./initializers" ; import { DEFAULT_OPTIONS , OptiCSSOptions } from "./OpticssOptions" ; import { OptimizationPass } from "./OptimizationPass" ; import { Optimization , isMultiFileOptimization , isSingleFileOptimization , optimizations , } from "./optimizations" ; import { adaptFromLegacySourceMap } from "./util/adaptSourceMap" ; export interface OptimizationResult { output : CssFile ; styleMapping : StyleMapping ; actions : Actions ; } export interface TimingData { [ timingName ] : { start : number ; end : number ; } ; } type PostcssToResultOptions = Parameters < postcss . Root [ "toResult" ] > [ 0 ] ; type PostcssProcessOptions = NonNullable < Parameters < postcss . Processor [ "process" ] > [ 1 ] > ; export class Optimizer { sources : Array < CssFile > ; analyses : Array < TemplateAnalysis < keyof TemplateTypes > > ; options : OptiCSSOptions ; templateOptions : TemplateIntegrationOptions ; timings : TimingData ; private initializers : Set < keyof Initializers > ; private optimizations : Set < Optimization > ; param sources constructor ( options , templateOptions ) { if ( parseInt ( process . versions . node ) <= 6 ) { throw new Error ( "Opticss does not support Node.js <= 6" ) ; } this . sources = [ ] ; this . analyses = [ ] ; this . options = Object . assign ( { } , DEFAULT_OPTIONS , options ) ; this . templateOptions = normalizeTemplateOptions ( templateOptions ) ; this . optimizations = new Set ( ) ; this . initializers = new Set ( [ "initKnownIdents" ] ) ; this . timings = { } ; if ( ! this . options . enabled ) { return ; } let only = this . options . only || [ ] ; let except = this . options . except || [ ] ; Object . keys ( optimizations ) . forEach ( opt => { if ( ( only . length && ! only . includes ( opt ) ) || ( except . length && except . includes ( opt ) ) ) { return ; } if ( this . options [ opt ] ) { let optimization = new optimizations [ opt ] ( this . options , this . templateOptions ) ; this . optimizations . add ( optimization ) ; for ( let initializerName of optimization . initializers ) { this . initializers . add ( initializerName ) ; } } } ) ; } param CSS addSource ( file ) { this . sources . push ( file ) ; } param TemplateAnalysis addAnalysis ( analysis ) { this . analyses . push ( analysis ) ; } param Timing param Timing param Timing private logTiming ( name , start , end ) { this . timings [ name ] = { start : start . getUTCMilliseconds ( ) , end : end . getUTCMilliseconds ( ) , } ; } param All returns private parseFiles ( sources ) { let start = new Date ( ) ; let promises = new Array < Promise < ParsedCssFile > > ( ) ; for ( let source of sources ) { promises . push ( parseCss ( source ) ) ; } return Promise . all ( promises ) . then ( ( res ) => { this . logTiming ( "parse" , start , new Date ( ) ) ; return res ; } ) ; } param This param All private initialize ( pass , files ) { let start = new Date ( ) ; this . initializers . forEach ( initializerName => { initializers [ initializerName ] ( pass , this . analyses , files , this . options , this . templateOptions ) ; } ) ; this . logTiming ( "initialize" , start , new Date ( ) ) ; } param This param All returns private optimizeFiles ( pass , files ) { let begin = new Date ( ) ; this . optimizations . forEach ( ( optimization ) => { let start = new Date ( ) ; if ( isSingleFileOptimization ( optimization ) ) { for ( let file of files ) { optimization . optimizeSingleFile ( pass , this . analyses , file ) ; } } if ( isMultiFileOptimization ( optimization ) ) { optimization . optimizeAllFiles ( pass , this . analyses , files ) ; } this . logTiming ( optimization . name , start , new Date ( ) ) ; } ) ; this . logTiming ( "optimize" , begin , new Date ( ) ) ; return Promise . resolve ( files ) ; } param All param The returns private concatenateFiles ( files , outputFilename ) { let start = new Date ( ) ; let output = new Concat ( true , outputFilename , "\n" ) ; for ( let file of files ) { let resultOpts = { to : outputFilename , map : { inline : false , prev : file . content . map , sourcesContent : true , annotation : false , } , } ; let result = file . content . root ! . toResult ( resultOpts ) ; output . add ( file . filename || "optimized-input.css" , result . css , adaptFromLegacySourceMap ( result . map . toJSON ( ) ) ) ; } this . logTiming ( "concatenate" , start , new Date ( ) ) ; return output ; } param outputFilename returns async optimize ( outputFilename ) { let pass = new OptimizationPass ( this . options , this . templateOptions ) ; let start = new Date ( ) ; let files = await this . parseFiles ( this . sources ) ; this . initialize ( pass , files ) ; files = await this . optimizeFiles ( pass , files ) ; let output = this . concatenateFiles ( files , outputFilename ) ; this . logTiming ( "total" , start , new Date ( ) ) ; return { output : { filename : outputFilename , content : output . content . toString ( ) , sourceMap : output . sourceMap , } , styleMapping : pass . styleMapping , actions : pass . actions , } ; } } param Input returns function parseCss ( file ) { if ( typeof file . content === "string" ) { return new Promise < postcss . Result > ( ( resolve , reject ) => { let processOpts = { from : file . filename , map : { inline : false , prev : file . sourceMap , sourcesContent : true , annotation : false , } , } ; postcss ( [ POSTCSS_NOOP_PLUGIN ] ) . process ( file . content , processOpts ) . then ( resolve , reject ) ; } ) . then ( result => { return { content : result , filename : file . filename , } ; } ) ; } else { return Promise . resolve ( < ParsedCssFile > file ) ; } } export const POSTCSS_NOOP_PLUGIN = postcss . plugin ( "noop-plugin" , function ( _opts ) { return function ( _root , _result ) { return Promise . resolve ( ) ; } ; } ) ;