import * as fse from 'fs-extra' ; import * as upath from 'upath' ; import chalk = require ( 'chalk' ) ; import sass = require ( 'sass' ) ; import { watch } from 'chokidar' ; import postcss , { ProcessOptions } from 'postcss' ; import autoprefixer = require ( 'autoprefixer' ) ; import CleanCSS = require ( 'clean-css' ) ; import { RawSourceMap } from 'source-map' ; import mergeSourceMap = require ( 'merge-source-map' ) ; import { prettyHrTime } from './PrettyUnits' ; import { Shout } from './Shout' ; import { BuildVariables } from './variables-factory/BuildVariables' ; import { PathFinder } from './variables-factory/PathFinder' ; import { sassImport } from './SassImportResolver' ; import { VoiceAssistant } from './VoiceAssistant' ; /**
 * Contains items returned by a CSS build sub-process.
 */ interface CssBuildResult { css : string ; map ? : RawSourceMap ; } /**
 * Contains methods for compiling a Sass project.
 */ export class SassBuildTool { private variables : BuildVariables ; private finder : PathFinder ; private va : VoiceAssistant ; param settings param flags constructor ( variables ) { this . variables = variables ; this . finder = new PathFinder ( variables ) ; this . va = new VoiceAssistant ( variables . mute ) ; } /**
     * Gets the full file path of the virtual Sass-Compiled CSS file.
     */ get virtualSassOutputFilePath ( ) : string { return upath . join ( this . finder . root , '(intermediate)' , '(sass-output).css' ) ; } /**
     * Gets the full file path of the virtual Sass-Compiled CSS file.
     */ get virtualPostcssOutputFilePath ( ) : string { return upath . join ( this . finder . root , '(intermediate)' , '(postcss-output).css' ) ; } param sm fixSassGeneratedSourceMap ( sm ) { const folder = upath . basename ( this . virtualSassOutputFilePath ) ; sm . sources = sm . sources . map ( s => { const absolute = upath . join ( folder , s ) ; return '/' + upath . relative ( this . finder . root , absolute ) ; } ) ; } param virtualSassOutputPath async compileSassProject ( virtualSassOutputPath ) { const cssInput = this . finder . cssEntry ; const sassResult = sass . renderSync ( { file : cssInput , outFile : virtualSassOutputPath , data : await fse . readFile ( cssInput , 'utf8' ) , sourceMap : this . variables . sourceMap , sourceMapContents : this . variables . sourceMap , importer : ( request , source ) => { return { file : sassImport ( source , request ) } ; } , } ) ; const result = { css : sassResult . css . toString ( 'utf8' ) } ; if ( this . variables . sourceMap && sassResult . map ) { const sms = sassResult . map . toString ( 'utf8' ) ; const sm = JSON . parse ( sms ) ; this . fixSassGeneratedSourceMap ( sm ) ; result . map = sm ; } return result ; } param virtualSassOutputPath param virtualPostcssOutputPath param sassResult async runPostCSS ( virtualSassOutputPath , virtualPostcssOutputPath , sassResult ) { const postcssOptions = { from : virtualSassOutputPath , to : virtualPostcssOutputPath } ; if ( this . variables . sourceMap ) { postcssOptions . map = { inline : false , prev : false } ; } const postcssResult = await postcss ( [ autoprefixer ] ) . process ( sassResult . css , postcssOptions ) ; const result = { css : postcssResult . css } ; if ( this . variables . sourceMap && sassResult . map && postcssResult . map ) { const sm2 = postcssResult . map . toJSON ( ) ; const abs = upath . resolve ( upath . dirname ( virtualPostcssOutputPath ) , sm2 . sources [ 0 ] ) ; sm2 . sources [ 0 ] = '/' + upath . relative ( this . variables . root , abs ) ; result . map = mergeSourceMap ( sassResult . map , sm2 ) ; } return result ; } param cssOutputPath param postcssResult runCleanCSS ( cssOutputPath , postcssResult ) { const cleanCssOptions = { level : { 1 : { specialComments : false } } , inline : false , sourceMap : this . variables . sourceMap , sourceMapInlineSources : this . variables . sourceMap } ; const cleanResult = new CleanCSS ( cleanCssOptions ) . minify ( postcssResult . css ) ; const errors = cleanResult . errors ; if ( errors . length ) { const errorMessage = "Error when minifying CSS:\n" + errors . map ( Q => '- ' + Q . toString ( ) ) . join ( "\n" ) ; throw new Error ( errorMessage ) ; } const result = { css : cleanResult . styles } ; if ( this . variables . sourceMap && postcssResult . map && cleanResult . sourceMap ) { const sm3 = cleanResult . sourceMap . toJSON ( ) ; sm3 . sources [ 0 ] = '/(intermediate)/(postcss-output).css' ; sm3 . file = upath . basename ( cssOutputPath ) ; result . map = mergeSourceMap ( postcssResult . map , sm3 ) ; const sourceMapFileName = upath . basename ( cssOutputPath ) + '.map' ; result . css += `\n/*# sourceMappingURL=${ sourceMapFileName } */` ; } return result ; } /**
     * Builds the CSS project asynchronously.
     */ async build ( ) { const sassOutputPath = this . virtualSassOutputFilePath ; const sassResult = await this . compileSassProject ( sassOutputPath ) ; const cssOutputPath = this . finder . cssOutputFilePath ; let postcssOutputPath = cssOutputPath ; if ( this . variables . production ) { postcssOutputPath = this . virtualPostcssOutputFilePath ; } let cssResult = await this . runPostCSS ( sassOutputPath , postcssOutputPath , sassResult ) ; if ( this . variables . production ) { cssResult = this . runCleanCSS ( cssOutputPath , cssResult ) ; } const cssOutputTask = Shout . fileOutput ( cssOutputPath , cssResult . css ) ; if ( cssResult . map ) { cssResult . map . sourceRoot = 'instapack://' ; const s = JSON . stringify ( cssResult . map ) ; await Shout . fileOutput ( cssOutputPath + '.map' , s ) ; } await cssOutputTask ; this . va . rewind ( ) ; } /**
     * Executes build method with a formatted error and stopwatch wrapper.
     */ async buildWithStopwatch ( ) { Shout . timed ( 'Compiling' , chalk . cyanBright ( 'index.scss' ) , chalk . grey ( 'in ' + this . finder . cssInputFolder + '/' ) ) ; const start = process . hrtime ( ) ; try { await this . build ( ) ; } catch ( error ) { let render ; this . va . speak ( 'CSS COMPILE ERROR!' ) ; if ( error [ 'formatted' ] ) { const formatted = 'Sass Compile' + ( error [ 'formatted' ] as string ) . trim ( ) ; render = chalk . redBright ( formatted ) ; console . error ( '\n' + render + '\n' ) ; } else { Shout . error ( 'during CSS build:' , error ) ; } } finally { const time = prettyHrTime ( process . hrtime ( start ) ) ; Shout . timed ( 'Finished CSS build after' , chalk . greenBright ( time ) ) ; } } /**
     * Executes build when any *.scss files on the CSS project folder is modified.
     */ watch ( ) { let debounced ; const debounce = ( ) : void => { clearTimeout ( debounced ) ; debounced = setTimeout ( ( ) => { this . buildWithStopwatch ( ) ; } , 300 ) ; } ; watch ( this . finder . scssGlob , { ignoreInitial : true } ) . on ( 'add' , file => { Shout . sass ( chalk . grey ( 'tracking new file:' , file ) ) ; debounce ( ) ; } ) . on ( 'change' , file => { Shout . sass ( chalk . grey ( 'updating file:' , file ) ) ; debounce ( ) ; } ) . on ( 'unlink' , file => { Shout . sass ( chalk . grey ( 'removing file:' , file ) ) ; debounce ( ) ; } ) ; } }