UNPKG

instapack

Version:

All-in-one TypeScript and Sass compiler for web applications!

186 lines (185 loc) 7.48 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SassBuildTool = void 0; const fse = require("fs-extra"); const upath = require("upath"); const chalk = require("chalk"); const sass = require("sass"); const chokidar_1 = require("chokidar"); const postcss_1 = require("postcss"); const autoprefixer = require("autoprefixer"); const CleanCSS = require("clean-css"); const mergeSourceMap = require("merge-source-map"); const PrettyUnits_1 = require("./PrettyUnits"); const Shout_1 = require("./Shout"); const PathFinder_1 = require("./variables-factory/PathFinder"); const SassImportResolver_1 = require("./SassImportResolver"); const VoiceAssistant_1 = require("./VoiceAssistant"); class SassBuildTool { constructor(variables) { this.variables = variables; this.finder = new PathFinder_1.PathFinder(variables); this.va = new VoiceAssistant_1.VoiceAssistant(variables.mute); } get virtualSassOutputFilePath() { return upath.join(this.finder.root, '(intermediate)', '(sass-output).css'); } get virtualPostcssOutputFilePath() { return upath.join(this.finder.root, '(intermediate)', '(postcss-output).css'); } 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); }); } 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: SassImportResolver_1.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; } 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_1.default([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; } 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; } 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_1.Shout.fileOutput(cssOutputPath, cssResult.css); if (cssResult.map) { cssResult.map.sourceRoot = 'instapack://'; const s = JSON.stringify(cssResult.map); await Shout_1.Shout.fileOutput(cssOutputPath + '.map', s); } await cssOutputTask; this.va.rewind(); } async buildWithStopwatch() { Shout_1.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'].trim(); render = chalk.redBright(formatted); console.error('\n' + render + '\n'); } else { Shout_1.Shout.error('during CSS build:', error); } } finally { const time = PrettyUnits_1.prettyHrTime(process.hrtime(start)); Shout_1.Shout.timed('Finished CSS build after', chalk.greenBright(time)); } } watch() { let debounced; const debounce = () => { clearTimeout(debounced); debounced = setTimeout(() => { this.buildWithStopwatch(); }, 300); }; chokidar_1.watch(this.finder.scssGlob, { ignoreInitial: true }) .on('add', file => { Shout_1.Shout.sass(chalk.grey('tracking new file:', file)); debounce(); }) .on('change', file => { Shout_1.Shout.sass(chalk.grey('updating file:', file)); debounce(); }) .on('unlink', file => { Shout_1.Shout.sass(chalk.grey('removing file:', file)); debounce(); }); } } exports.SassBuildTool = SassBuildTool;