<commit_before>#!/usr/bin/env node

'use strict';

/**
 * Validate that a given component can actually server-side render
 * with the given properties (specified via a fixture file).
 *
 * Not all react components can be server-side rendered: they may
 * use globals, or access the DOM, or what have you.  This script
 * can be used to validate that the component *can* be server-side
 * rendered.
 *
 * The input is a bit difficult to construct: it's a json-encoded
 * list of objects like so:
 *    {
 *      jsPackages: [[pkg_name, pkg_contents], [pkg_name, pkg_contents], ...],
 *      pathToReactComponent: "./in/require/format.js",
 *      fixtureFile: "/ideally/an/absolute/path",
 *    }
 * passed in via sttdin.
 *
 * We require() the fixture file and try to server-side render the
 * component using each fixture found therein.  jsPackages are the
 * packages needed to render the component (including the package
 * that defines the component!) topologically sorted so no package
 * require()s anything from a package that comes after it.
 */

/* eslint-disable no-console */

const path = require("path");

const render = require("../src/render.js");

// Cribbed from
// https://github.com/facebook/react/blob/92530b4ddcc77be49a50ce4cbffb32ee38a98247/src/renderers/dom/client/ReactMount.js
function firstDifferenceIndex(string1, string2) {
    const minLen = Math.min(string1.length, string2.length);
    for (let i = 0; i < minLen; i++) {
        if (string1.charAt(i) !== string2.charAt(i)) {
            return i;
        }
    }
    return string1.length === string2.length ? -1 : minLen;
}

// Given some html, remove the reactid and react-checksum, which are
// auto-generated and not necessarily consistent between runs.
const normalizeReactOutput = function(html) {
    html = html.replace(/data-reactid="[^"]*"/g,
                        'data-reactid="..."');
    html = html.replace(/data-react-checksum="[^"]*"/g,
                        'data-react-checksum="..."');
    return html;
};

const mismatchMsg = (
    "There was a mismatch between the server and client rendered" +
    " HTML. Server-side rendered components must not do feature" +
    " feature detection, because the features detected will be" +
    " different on the client and server!\n\n" +
    "Here's where the server and client may differ");

// Returns the number of errors found trying to render the component.
const validate = function(jsPackages, pathToReactComponent, fixtureFile) {
    let allProps;
    try {
        const relativeFixturePath = path.relative(__dirname, fixtureFile);
        allProps = require(relativeFixturePath).instances;
    } catch (err) {
        console.log(`Error reading fixtures from ${fixtureFile}: ${err}`);
        // NOTE(jeresig): We should really be returning 1 here as it is an
        // error, however we now have some fixture tests that do more than
        // just return an object (they build React components and wrappers).
        // The only way to make these run correctly is to bring in the full
        // dev package and run it with all of the dev dependencies. For
        // example right now a fixture that does `require("react")` will fail
        // with "require.withVars not found". Note that this is only an issue
        // with validate_components.js. The code normally runs fine server-
        // side and also runs fine in the normal fixture tests.
        return Promise.resolve(0);
    }

    return Promise.all(allProps.map((props, i) => {
        // For each fixture file, we'll try rendering the component three
        // times:
        //
        //  1. Emulating a server-side render
        //  2. Emulating a mobile browser client-side render
        //  3. Emulating a desktop browser client-side render
        //
        // Then we compare the outputs from (1) and (2), and the outputs from
        // (1) and (3). If they differ, it suggests there the client and server
        // would render different things, causing a checksum difference. When
        // the React checksum differs, React will throw away everything in the
        // root container. This causes nasty reflows and would throw away any
        // user input in the server-rendered components.
        //
        // This also helps us find non-determinism in the rendering output
        // (e.g. using random numbers as IDs).
        return Promise.all([
            render(jsPackages, pathToReactComponent, props, {}, 'ignore'),
            render(jsPackages, pathToReactComponent, props, {
                width: 2000,
                height: 900,
                innerWidth: 2000,
                innerHeight: 900,
                outerWidth: 2000,
                outerHeight: 900,
            }, 'ignore'),
            render(jsPackages, pathToReactComponent, props, {
                width: 400,
                height: 600,
                innerWidth: 400,
                innerHeight: 600,
                outerWidth: 400,
                outerHeight: 600,
                ontouchstart: function() {},
                onorientationchange: function() {},
                orientation: 'portrait',
            }, 'ignore'),
        ]).then((results) => {
            const serverHtml = normalizeReactOutput(results[0].html);
            const desktopHtml = normalizeReactOutput(results[1].html);
            const mobileHtml = normalizeReactOutput(results[2].html);

            if (serverHtml !== desktopHtml) {
                const diffIndex = firstDifferenceIndex(serverHtml,
                                                       desktopHtml);
                return Promise.reject(new Error(
                    mismatchMsg +
                    "\n(server) " + serverHtml.substring(diffIndex - 20,
                                                         diffIndex + 40) +
                    "\n(desktop) " + desktopHtml.substring(diffIndex - 20,
                                                           diffIndex + 40)));
            } else if (serverHtml !== mobileHtml) {
                const diffIndex = firstDifferenceIndex(serverHtml,
                                                       mobileHtml);
                return Promise.reject(new Error(
                    mismatchMsg +
                    "\n(server) " + serverHtml.substring(diffIndex - 20,
                                                         diffIndex + 40) +
                    "\n(mobile) " + mobileHtml.substring(diffIndex - 20,
                                                         diffIndex + 40)));
            }
        }).then(() => {
            console.log(`${pathToReactComponent} #${i}: OK`);
            return 0;
        }).catch((err) => {
            console.log(`${pathToReactComponent} #${i}: ERROR ${err.stack}`);
            return 1;
        });
    })).then((results) => {
        // Total up all of the errors
        return results.reduce((a, b) => a + b, 0);
    }).catch((err) => {
        // Some uncaught error!? Should hopefully never happen!
        console.log(`${pathToReactComponent}: ERROR ${err.stack}`);
        return 1;
    });
};


const main = function(inputJson) {
    // Run all the validation calls as async promises
    const promises = inputJson.map(
        (oneInput) => validate(oneInput.jsPackages,
            oneInput.pathToReactComponent, oneInput.fixtureFile));

    // Return a promise that has a total number of errors generated
    return Promise.all(promises)
        .then((errors) => errors.reduce((a, b) => a + b, 0));
};


let inputText = '';
process.stdin.on('data', function(chunk) {
    inputText = inputText + chunk;
});
process.stdin.on('end', function() {
    const inputJson = JSON.parse(inputText);
    main(inputJson).then((numErrors) => {
        console.log('DONE');
        process.exit(numErrors);
    }).catch((err) => {
        console.log('UNCAUGHT ERROR', err, err.stack);
        process.exit(1);
    });
});
<commit_msg>Add some debugging to help catch broken validation<commit_after>#!/usr/bin/env node

'use strict';

/**
 * Validate that a given component can actually server-side render
 * with the given properties (specified via a fixture file).
 *
 * Not all react components can be server-side rendered: they may
 * use globals, or access the DOM, or what have you.  This script
 * can be used to validate that the component *can* be server-side
 * rendered.
 *
 * The input is a bit difficult to construct: it's a json-encoded
 * list of objects like so:
 *    {
 *      jsPackages: [[pkg_name, pkg_contents], [pkg_name, pkg_contents], ...],
 *      pathToReactComponent: "./in/require/format.js",
 *      fixtureFile: "/ideally/an/absolute/path",
 *    }
 * passed in via sttdin.
 *
 * We require() the fixture file and try to server-side render the
 * component using each fixture found therein.  jsPackages are the
 * packages needed to render the component (including the package
 * that defines the component!) topologically sorted so no package
 * require()s anything from a package that comes after it.
 */

/* eslint-disable no-console */

const path = require("path");

const render = require("../src/render.js");

// Cribbed from
// https://github.com/facebook/react/blob/92530b4ddcc77be49a50ce4cbffb32ee38a98247/src/renderers/dom/client/ReactMount.js
function firstDifferenceIndex(string1, string2) {
    const minLen = Math.min(string1.length, string2.length);
    for (let i = 0; i < minLen; i++) {
        if (string1.charAt(i) !== string2.charAt(i)) {
            return i;
        }
    }
    return string1.length === string2.length ? -1 : minLen;
}

// Given some html, remove the reactid and react-checksum, which are
// auto-generated and not necessarily consistent between runs.
const normalizeReactOutput = function(html) {
    html = html.replace(/data-reactid="[^"]*"/g,
                        'data-reactid="..."');
    html = html.replace(/data-react-checksum="[^"]*"/g,
                        'data-react-checksum="..."');
    return html;
};

const mismatchMsg = (
    "There was a mismatch between the server and client rendered" +
    " HTML. Server-side rendered components must not do feature" +
    " feature detection, because the features detected will be" +
    " different on the client and server!\n\n" +
    "Here's where the server and client may differ");

// Returns the number of errors found trying to render the component.
const validate = function(jsPackages, pathToReactComponent, fixtureFile) {
    let allProps;
    try {
        const relativeFixturePath = path.relative(__dirname, fixtureFile);
        allProps = require(relativeFixturePath).instances;
    } catch (err) {
        console.log(`Error reading fixtures from ${fixtureFile}: ${err}`);
        // NOTE(jeresig): We should really be returning 1 here as it is an
        // error, however we now have some fixture tests that do more than
        // just return an object (they build React components and wrappers).
        // The only way to make these run correctly is to bring in the full
        // dev package and run it with all of the dev dependencies. For
        // example right now a fixture that does `require("react")` will fail
        // with "require.withVars not found". Note that this is only an issue
        // with validate_components.js. The code normally runs fine server-
        // side and also runs fine in the normal fixture tests.
        return Promise.resolve(0);
    }

    if (!allProps) {
        console.log(`Error: fixture has no instances: ${fixtureFile}` +
            ` (you may need to use module.exports = {instances: ...};)`);
        return Promise.resolve(1);
    }

    return Promise.all(allProps.map((props, i) => {
        // For each fixture file, we'll try rendering the component three
        // times:
        //
        //  1. Emulating a server-side render
        //  2. Emulating a mobile browser client-side render
        //  3. Emulating a desktop browser client-side render
        //
        // Then we compare the outputs from (1) and (2), and the outputs from
        // (1) and (3). If they differ, it suggests there the client and server
        // would render different things, causing a checksum difference. When
        // the React checksum differs, React will throw away everything in the
        // root container. This causes nasty reflows and would throw away any
        // user input in the server-rendered components.
        //
        // This also helps us find non-determinism in the rendering output
        // (e.g. using random numbers as IDs).
        return Promise.all([
            render(jsPackages, pathToReactComponent, props, {}, 'ignore'),
            render(jsPackages, pathToReactComponent, props, {
                width: 2000,
                height: 900,
                innerWidth: 2000,
                innerHeight: 900,
                outerWidth: 2000,
                outerHeight: 900,
            }, 'ignore'),
            render(jsPackages, pathToReactComponent, props, {
                width: 400,
                height: 600,
                innerWidth: 400,
                innerHeight: 600,
                outerWidth: 400,
                outerHeight: 600,
                ontouchstart: function() {},
                onorientationchange: function() {},
                orientation: 'portrait',
            }, 'ignore'),
        ]).then((results) => {
            const serverHtml = normalizeReactOutput(results[0].html);
            const desktopHtml = normalizeReactOutput(results[1].html);
            const mobileHtml = normalizeReactOutput(results[2].html);

            if (serverHtml !== desktopHtml) {
                const diffIndex = firstDifferenceIndex(serverHtml,
                                                       desktopHtml);
                return Promise.reject(new Error(
                    mismatchMsg +
                    "\n(server) " + serverHtml.substring(diffIndex - 20,
                                                         diffIndex + 40) +
                    "\n(desktop) " + desktopHtml.substring(diffIndex - 20,
                                                           diffIndex + 40)));
            } else if (serverHtml !== mobileHtml) {
                const diffIndex = firstDifferenceIndex(serverHtml,
                                                       mobileHtml);
                return Promise.reject(new Error(
                    mismatchMsg +
                    "\n(server) " + serverHtml.substring(diffIndex - 20,
                                                         diffIndex + 40) +
                    "\n(mobile) " + mobileHtml.substring(diffIndex - 20,
                                                         diffIndex + 40)));
            }
        }).then(() => {
            console.log(`${pathToReactComponent} #${i}: OK`);
            return 0;
        }).catch((err) => {
            console.log(`${pathToReactComponent} #${i}: ERROR ${err.stack}`);
            return 1;
        });
    })).then((results) => {
        // Total up all of the errors
        return results.reduce((a, b) => a + b, 0);
    }).catch((err) => {
        // Some uncaught error!? Should hopefully never happen!
        console.log(`${pathToReactComponent}: ERROR ${err.stack}`);
        return 1;
    });
};


const main = function(inputJson) {
    // Run all the validation calls as async promises
    const promises = inputJson.map(
        (oneInput) => validate(oneInput.jsPackages,
            oneInput.pathToReactComponent, oneInput.fixtureFile));

    // Return a promise that has a total number of errors generated
    return Promise.all(promises)
        .then((errors) => errors.reduce((a, b) => a + b, 0));
};


let inputText = '';
process.stdin.on('data', function(chunk) {
    inputText = inputText + chunk;
});
process.stdin.on('end', function() {
    const inputJson = JSON.parse(inputText);
    main(inputJson).then((numErrors) => {
        console.log('DONE');
        process.exit(numErrors);
    }).catch((err) => {
        console.log('UNCAUGHT ERROR', err, err.stack);
        process.exit(1);
    });
});

{'fraction_non_alphanumeric': 0.11028500619578686, 'fraction_numerical': 0.013073110285006196, 'mean_word_length': 1.9968436687708875, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 27, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 24, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5033377', 'n_tokens_mistral': 4326, 'n_tokens_neox': 4113, 'n_words': 1378}