node.jswebpackwebpack-plugin

Make sure webpack plugin finishes before compilation


I'm trying to write a webpack plugin (my first! so bear with me please) that downloads fontello icon fonts, puts them in the dist folder and then generates an icons.scss file containing SASS variables for every icon.

I guess this is a little unconventional because the icons.scss file isn't supposed to go in the dist/ folder but rather the src/ folder. It doesn't seem impossible to put files in src/ though, and it needs to be there in order for my app.scss to import it.

The problem I'm having is that my generated icons.scss file (which is included by my main entry point for SASS (app.scss)) doesn't have time to generate before SASS is being compiled.

Is there any way of telling webpack to wait until my plugin is finished before going ahead with the build?

To be clear I have everything working relatively well, fonts are downloaded, icons.scss is generated etc, the only problem is that SASS compiles before icons.scss exists. I have no idea which compiler/compilation hook is best to use either, so would love some input on that too.

Please see this excerpt of the code for the relevant parts. I've left comments where I feel like things can improve too:

// Fontello API endpoint
let url = 'http://fontello.com';

// Read our fontello config JSON
let fontelloConfig = fs.createReadStream('icons.json', 'utf8');

// Send fontello config to API
request.post({url: url, formData: {config: fontelloConfig}}, (err, res, body) => {
    if (err) {
        return console.error(err);
    }

    // Fetch ZIP using the session we got back from the previous call
    request.get(`http://fontello.com/${body}/get`)
        // Unzip it
        .pipe(unzipper.Parse())

        // For each file
        .on('entry', (entry) => {
            // Get basename and extension
            const basename = path.basename(entry.path);
            const ext = path.extname(basename);

            // Copy the fontello.css to the sass path
            // NOTE: All of this is a mess, I'm writing the fontello.css file to src/icons.scss, then reading src/icons.scss
            // only so that I can make my changes to its contents, finally I write the changed contents back to src/icons.scss
            // please help me improve this part, I'm a node and webpack noob
            if (basename === 'fontello.css') {
                // Write fontello.css to icons.scss
                entry.pipe(fs.createWriteStream('src/icons.scss')).on('finish', () => {
                    // Read the file...
                    fs.readFile('src/icons.scss', 'utf8', (err, data) => {
                        // NOTE: Not until this file is created should webpack continue with compilation, 
                        // if this file doesn't exist when SASS is compiled it will fail because my app.scss is trying to import this file
                        fs.writeFile('src/icons.scss', makeMyChangesToTheFontelloCssFileContent(data), 'utf8', (err) => {});
                    });
                });
            }
            // Copy fonts and config.json to dist
            // NOTE: I'm a noob so I didn't know you're supposed to use compilation.assets[filename] = filecontent;
            // I'm working on it, but please let me know if it's an easy change?
            else if (entry.type === 'File' && (basename === 'config.json' || entry.path.indexOf('/font/') !== -1)) {
                entry.pipe(fs.createWriteStream('dist/' + basename));
            }
            // Otherwise clean up(?): https://github.com/ZJONSSON/node-unzipper#parse-zip-file-contents
            else {
                entry.autodrain();
            }
        });
});

Edit: I've studied the docs but find it hard to know what to do without examples. I've set up callbacks on pretty much every compiler and compilation hook to understand when and how they are being run but it didn't really help me an awful lot.


Solution

  • I'm not happy with this solution, but I ended up just running my fontello script before webpack by changing my package.json:

    "dev": "node fontello.js && webpack --mode development",
    "build": "node fontello.js && webpack --mode production",
    

    And the final fontello.js looks like this:

    // Utils
    const path = require('path');
    const fs = require('fs');
    const request = require('request');
    const unzipper = require('unzipper');
    const css = require('css');
    
    // Fontello Plugin
    class FontelloSassWebpackPlugin {
        constructor (config) {
            this.config = Object.assign({
                src: path.resolve(__dirname, 'src/icons.json'),
                dest: path.resolve(__dirname, 'src/assets/fontello'),
                sass: path.resolve(__dirname, 'src/sass/icons.scss'),
                url: 'http://fontello.com'
            }, config);
        }
    
        // Converts the fontello.css to what we need
        convertFontelloCss (code) {
            var obj = css.parse(code);
            var newRules = [];
            var sassVars = '';
            var sassMixin = '';
    
            obj.stylesheet.rules.forEach(rule => {
                const selector = (rule.selectors && rule.selectors.length) ? rule.selectors[0] : null;
    
                if (selector) {
                    // [class] rule
                    if (selector.indexOf('[class^="icon-"]:before') !== -1) {
                        rule.selectors.push(...['[class^="icon-"]:after', '[class*=" icon-"]:after']);
    
                        rule.declarations.forEach(d => {
                            if (d.type === 'declaration') {
                                sassMixin += `${d.property}: ${d.value};\n`;
                            }
                        });
    
                        sassMixin = `@mixin icon ($icon-code: "[NOICO]") {\n${sassMixin}\ncontent: $icon-code;\n}`;
                    }
                    // Icon rule
                    if (selector.indexOf('.icon-') !== -1) {
                        const iconName = selector.match(/\.icon-(.*?):before/)[1];
                        var iconVal = '[NO-ICON]';
    
                        rule.declarations.forEach(d => {
                            if (d.property === 'content') {
                                iconVal = d.value;
                            }
                        });
    
                        newRules.push({
                            type: 'rule',
                            selectors: [`.icon-${iconName}.icon--after:before`],
                            declarations: [{
                                type: 'declaration',
                                property: 'content',
                                value: 'normal'
                            }]
                        });
    
                        newRules.push({
                            type: 'rule',
                            selectors: [`.icon-${iconName}.icon--after:after`],
                            declarations: rule.declarations
                        });
    
                        sassVars += `$icon-${iconName}: ${iconVal};\n`;
                    }
                }
            });
    
            obj.stylesheet.rules.push(...newRules);
    
            return css.stringify(obj, {compress: false}).replace(/\.\.\/font\//g, 'assets/fontello/') + sassMixin + sassVars;
        }
    
        apply () {
            const fontelloConfig = fs.createReadStream(this.config.src, 'utf8');
    
            // Make sure folder exists
            if (!fs.existsSync(this.config.dest)) {
                fs.mkdirSync(this.config.dest, {recursive: true});
            }
    
            // Fetch session
            request.post({url: this.config.url, formData: {config: fontelloConfig}}, (err, res, body) => {
                if (err) {
                    return console.error(err);
                }
    
                // Fetch ZIP
                request.get(`${this.config.url}/${body}/get`)
                    // Unzip it
                    .pipe(unzipper.Parse())
    
                    // For each file
                    .on('entry', (entry) => {
                        const basename = path.basename(entry.path);
                        const ext = path.extname(basename);
    
                        // Copy the fontello.css to the sass path
                        if (basename === 'fontello.css') {
                            entry.pipe(fs.createWriteStream(this.config.sass)).on('finish', () => {
                                fs.readFile(this.config.sass, 'utf8', (err, data) => {
                                    fs.writeFile(this.config.sass, this.convertFontelloCss(data), 'utf8', (err) => {});
                                });
                            });
                        }
                        // Copy fonts and config.json to dist
                        else if (entry.type === 'File' && (basename === 'config.json' || entry.path.indexOf('/font/') !== -1)) {
                            entry.pipe(fs.createWriteStream(this.config.dest + '/' + basename));
                        }
                        // Otherwise clean up: https://github.com/ZJONSSON/node-unzipper#parse-zip-file-contents
                        else {
                            entry.autodrain();
                        }
                    });
            });
        }
    }
    
    const fswp = new FontelloSassWebpackPlugin();
    
    fswp.apply();