UNPKG

@mapbox/batfish

Version:

The React-powered static-site generator you didn't know you wanted

189 lines (167 loc) 6.39 kB
// 'use strict'; const got = require('got'); const path = require('path'); const chalk = require('chalk'); const fs = require('fs'); const _ = require('lodash'); const pify = require('pify'); const globby = require('globby'); const postcss = require('postcss'); const isAbsoluteUrl = require('is-absolute-url'); const mkdirp = require('mkdirp'); const pTry = require('p-try'); const hasha = require('hasha'); const Concat = require('concat-with-sourcemaps'); const constants = require('./constants'); const postcssAbsoluteUrls = require('./postcss-absolute-urls'); const rethrowPostcssError = require('./rethrow-postcss-error'); const errorTypes = require('./error-types'); const getPostcssPlugins = require('./get-postcss-plugins'); const urlCache = new Map(); // Fetch and concatenate stylesheets, with source maps. // Return Promise resolves with the absolute path to the written file. function compileStylesheets( batfishConfig , // Defaults to batfishConfig.outputDirectory. specialCssOutputDirectory ) { const cssOutputDirectory = specialCssOutputDirectory || batfishConfig.outputDirectory; return pTry(() => { if (_.isEmpty(batfishConfig.stylesheets)) return; const cssTarget = path.join(cssOutputDirectory, 'batfish-styles.css'); // Ensure the stylesheets are concatenated in the order specified. // Each item will be an object with locator, css, and map properties. // After all are accumulated, we'll concatenate them together in that order. const stylesheetContents = []; // Get a CSS from a URL and insert it into stylesheetContents // at the specified index. const getStylesheetFromUrl = ( url , index ) => { const cached = urlCache.get(url); if (cached) { stylesheetContents[index] = cached; return Promise.resolve(); } return got(url) .catch(() => { throw new errorTypes.ConfigValidationError( `Stylesheet at ${chalk.yellow(url)} could not be downloaded.` ); }) .then((response ) => { return ( postcss() // Make all the URLs in the stylesheet absolute so the new local // stylesheet can fetch assets like fonts and images. .use(postcssAbsoluteUrls({ stylesheetUrl: url })) .process(response.body, { from: url, to: cssTarget }) .catch(rethrowPostcssError) ); }) .then((result ) => { const contentsItem = { locator: url, css: result.css, map: undefined }; urlCache.set(url, contentsItem); stylesheetContents[index] = contentsItem; }); }; const processSingleStylesheetFromFs = ( filename ) => { return pify(fs.readFile)(filename, 'utf8').then((css) => { return postcss(getPostcssPlugins(batfishConfig)) .process(css, { from: filename, to: cssTarget, map: { inline: false, sourcesContent: true, annotation: false } }) .catch(rethrowPostcssError) .then((result) => { return { locator: filename, css: result.css, map: result.map.toString() }; }); }); }; // input could be a filename or a glob or an array of those. // Get the CSS from each file and insert it into stylesheetContents // at the specified index. If input is array, that item in stylesheetContents // will be an array. const getStylesheetFromFs = ( input , index ) => { return globby(input, { absolute: true }) .then((filenames) => { return Promise.all(filenames.map(processSingleStylesheetFromFs)); }) .then((contents) => { stylesheetContents[index] = contents; }); }; // Runs after stylesheetContents has been populated. Concatenates the CSS // in that order, with source maps. Outputs new CSS and and a new source map. const concatStylesheetContents = () => { const concatenator = new Concat( true, path.join(cssOutputDirectory, constants.BATFISH_CSS_BASENAME), '\n' ); // Since items in stylesheets can be an array (fancy glob), // we need to be recursive here. const addItems = (list) => { list.forEach((item) => { if (Array.isArray(item)) return addItems(item); concatenator.add(item.locator, item.css, item.map); }); }; addItems(stylesheetContents); return { css: concatenator.content, map: concatenator.sourceMap }; }; let cssFilePath; return Promise.all( batfishConfig.stylesheets.map((locator, index) => { if (!Array.isArray(locator) && isAbsoluteUrl(locator)) { return getStylesheetFromUrl(locator, index); } return getStylesheetFromFs(locator, index); }) ) .then(() => pify(mkdirp)(cssOutputDirectory)) .then(() => { const concatenated = concatStylesheetContents(); const finalCss = concatenated.css; const finalMap = concatenated.map; // Add a md5 hash to the filename for production. let basename = constants.BATFISH_CSS_BASENAME; if (batfishConfig.production) { const hash = hasha(finalCss, { algorithm: 'md5' }); basename = basename.replace(/\.css$/, `-${hash}.css`); } const finalCssWithSourcemapAnnotation = `${finalCss}/*# sourceMappingURL=${basename}.map */`; cssFilePath = path.join(cssOutputDirectory, basename); const mapFilePath = `${cssFilePath}.map`; return Promise.all([ pify(fs.writeFile)(cssFilePath, finalCssWithSourcemapAnnotation), pify(fs.writeFile)(mapFilePath, finalMap) ]).then(() => cssFilePath); }); }); } module.exports = compileStylesheets;