public/external/postinstall.js

Summary

Maintainability
A
0 mins
Test Coverage
const fs = require('node:fs');
const path = require('node:path');

const walkFilesSync = function (f, callback) {
    if (fs.lstatSync(f).isDirectory()) {
        return fs.readdirSync(f).sort().flatMap((f2) => walkFilesSync(path.join(f, f2), callback));
    }

    return [callback(f)];
};

const updateFileSync = function (f, callback) {
    const dataOrig = fs.readFileSync(f, 'binary');
    const dataNew = callback(dataOrig);
    if (dataNew !== undefined && dataNew !== dataOrig) {
        fs.writeFileSync(f, dataNew, { encoding: 'binary' });
    }
};

// move node_modules/ files to parent directory
for (const f of [
    '@highlightjs',
    '@shopify',
    'chart.js',
    'flatpickr',
    'fomantic-ui',
    'jquery',
    'twemoji',
]) {
    fs.cpSync(
        path.join(path.join(__dirname, 'node_modules'), f),
        path.join(__dirname, f),
        { recursive: true }
    );
}

const cssUrlPattern = '((?<!\\w)url\\([\'"]?(?!data:))((?:[^(){}\\\\\'"]|\\\\.)*)([\'"]?\\))';

// use native font stack in Fomantic-UI
// https://github.com/fomantic/Fomantic-UI/issues/2355
walkFilesSync(path.join(__dirname, 'fomantic-ui'), (f) => {
    updateFileSync(f, (data) => {
        if (!f.endsWith('.css')) {
            return;
        }

        data = data.replaceAll(new RegExp('\\s*@font-face\\s*\\{[^{}]*' + cssUrlPattern + '[^{}]+\\}', 'g'), (m, m1, m2, m3) => {
            if (m2.includes('/assets/fonts/Lato')) {
                return '';
            }

            return m;
        });

        data = data.replaceAll(/(font-family: *)([^;{}]*)(;?)/g, (m, m1, m2, m3) => {
            // based on https://github.com/twbs/bootstrap/blob/v5.1.3/scss/_variables.scss#L577
            const fontFamilySansSerif = [
                'system-ui',
                '-apple-system',
                '\'Segoe UI\'',
                'Roboto',
                '\'Helvetica Neue\'',
                'Arial',
                '\'Noto Sans\'',
                '\'Liberation Sans\'',
                'sans-serif',
                '\'Apple Color Emoji\'',
                '\'Segoe UI Emoji\'',
                '\'Segoe UI Symbol\'',
                '\'Noto Color Emoji\'',
            ].join(/\.min\./.test(f) ? ',' : ', ');
            // based on https://github.com/twbs/bootstrap/blob/v5.1.3/scss/_variables.scss#L578
            const fontFamilySansMonospace = [
                'SFMono-Regular',
                'Menlo',
                'Monaco',
                'Consolas',
                '\'Liberation Mono\'',
                '\'Courier New\'',
                'monospace',
            ].join(/\.min\./.test(f) ? ',' : ', ');

            if (/(?<!\w)lato(?!\w)/i.test(m2)) {
                return m1 + fontFamilySansSerif + m3;
            }
            if (/(?<!\w)monospace(?!\w)/i.test(m2)) {
                return m1 + fontFamilySansMonospace + m3;
            }
            if (m2 === 'inherit' || !m2.includes(',') || m2 === fontFamilySansSerif) {
                return m;
            }

            throw new Error('Font-family "' + m2 + '" has no mapping');
        });

        // change bold (700) font weight to 600 to match the original Lato font weight better
        // see https://github.com/fomantic/Fomantic-UI/pull/2359#discussion_r867457881 discussion
        data = data.replaceAll(/(font-weight: *)([^;{}]*)(;?)/g, (m, m1, m2, m3) => {
            if (m2 === 'bold' || m2 === '700') {
                return m1 + '600' + m3;
            }

            return m;
        });

        return data;
    });
});

// remove links to fonts with format other than woff2 from Fomantic-UI
walkFilesSync(path.join(__dirname, 'fomantic-ui'), (f) => {
    updateFileSync(f, (data) => {
        if (!f.endsWith('.css')) {
            return;
        }

        data = data.replaceAll(new RegExp('(src:\\s*(?!\\s))[^{};]*((?=[^{};,]+\\.woff2(?!\\w))' + cssUrlPattern + ')[^{};]*(;)', 'g'), '$1$2 format(\'woff2\')$6');

        return data;
    });
});

// remove twemoji images from Fomantic-UI, reduce total size by about 3500 files and 25 MB
// wait until https://github.com/fomantic/Fomantic-UI/issues/2363 is implemented or pack all images in one phar
walkFilesSync(path.join(__dirname, 'fomantic-ui'), (f) => {
    updateFileSync(f, (data) => {
        if (!f.endsWith('.css')) {
            return;
        }

        data = data.replaceAll(/\s*((?<!\w)em\[data-emoji=[^[\\\]{}]+]::before,?\s*)+{[^{}]*background-image:[^{}]+}/g, '');

        return data;
    });
});

// replace absolute URLs with relative paths
walkFilesSync(__dirname, (f) => {
    updateFileSync(f, (data) => {
        if (f.startsWith(path.join(__dirname, 'node_modules/'))
            || !f.endsWith('.css')
            || f.startsWith(path.join(__dirname, 'chart.js/dist/docs/'))
        ) {
            return;
        }

        data = data.replaceAll(new RegExp(cssUrlPattern, 'g'), (m, m1, m2, m3) => {
            let pathRel = null;
            if (m2.startsWith('http://') || m2.startsWith('https://') || m2.startsWith('//')) {
                const pathMap = {
                    'https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/': path.join(__dirname, 'twemoji/assets/svg/'),
                };

                const pathMapKeys = Object.keys(pathMap);
                for (const k of pathMapKeys) {
                    if (m2.startsWith(k)) {
                        const kRel = m2.slice(k.length);
                        const pathLocal = path.join(pathMap[k], kRel);
                        pathRel = path.relative(path.dirname(f), pathLocal);

                        break;
                    }
                }

                if (pathRel === null) {
                    throw new Error('URL "' + m2 + '" linked from "' + f + '"  has no local file mapping');
                }
            } else {
                pathRel = m2;
            }

            pathRel = pathRel.replaceAll('\\', '/');
            if (!pathRel.startsWith('.')) {
                pathRel = './' + pathRel;
            }

            if (!fs.existsSync(path.join(path.dirname(f), pathRel))) {
                throw new Error('File "' + pathRel + '" linked from "' + f + '" does not exist');
            }

            return m1 + pathRel + m3;
        });

        return data;
    });
});

// remove repeated Fomantic-UI version comments for easier diff
// https://github.com/fomantic/Fomantic-UI/issues/2468
walkFilesSync(path.join(__dirname, 'fomantic-ui'), (f) => {
    updateFileSync(f, (data) => {
        if (!f.endsWith('.css') && !f.endsWith('.js')) {
            return;
        }

        data = data.replaceAll(/(?<!^)\/\*!(?:(?!\/\*).)*# Fomantic-UI \d+\.\d+\.(?:(?!\/\*).)*MIT license(?:(?!\/\*).)*\*\/\n?/gs, '');

        return data;
    });
});

// replace Fomantic-UI modal module hideAll function
// https://github.com/fomantic/Fomantic-UI/issues/2526
walkFilesSync(path.join(__dirname, 'fomantic-ui'), (f) => {
    updateFileSync(f, (data) => {
        if (!f.endsWith('.js')) {
            return;
        }

        data = data.replaceAll(/(!\w+\.hide)All(\(\))/gs, '$1$2');

        return data;
    });
});

const cssTokenSelectorPattern = '(?:(?:[^(){}\'",+>~;/\\s]|\'[^\'\\\\{};]*\'|"[^"\\\\{};]*")+)';
const cssSimpleSelectorPattern = '(?:' + cssTokenSelectorPattern + '(?:\\(\\s*' + cssTokenSelectorPattern + '\\s*\\)' + cssTokenSelectorPattern + '?)*)';
const cssSingleSelectorPattern = '(?:' + cssSimpleSelectorPattern + '(?:\\s*[ +>~]\\s*' + cssSimpleSelectorPattern + ')*)';

// update Fomantic-UI ":first-child" selectors to work with immediately closed form tag
// https://github.com/atk4/ui/issues/1970
walkFilesSync(path.join(__dirname, 'fomantic-ui'), (f) => {
    updateFileSync(f, (data) => {
        if (!f.endsWith('.css')) {
            return;
        }

        data = data.replaceAll(new RegExp(cssSingleSelectorPattern + '(?=\\s*(,\\s*' + cssSingleSelectorPattern + '\\s*)*\\{)', 'g'), (mSingle) => (mSingle.includes(':first-child')
            ? mSingle.replaceAll(new RegExp('^(.*?)(' + cssSimpleSelectorPattern + '):first-child(' + cssSimpleSelectorPattern + '?)(.*)$', 'g'), (m, m1, m2, m3, m4) => (m1 === '' || /\.form(?!\w|.*[ +>~])/g.test(m1.trimEnd())
                ? m + ', '
                    + m1.trimEnd() + (m1 !== '' ? ' ' : '')
                    + 'form:first-child + ' + m2 + m3
                    + (m4 !== '' ? ' ' : '') + m4.trimStart()
                : m))
            : mSingle));

        return data;
    });
});

// normalize EOL of text files
walkFilesSync(__dirname, (f) => {
    updateFileSync(f, (data) => {
        if (data.includes('\0') || /\.min\./i.test(f)) {
            return;
        }

        data = data.replaceAll(/\r?\n|\r/g, '\n');
        if (data.slice(-1) !== '\n') {
            data += '\n';
        }

        return data;
    });
});