'use strict';

const rollup = require('rollup');
const babel = require('@rollup/plugin-babel').babel;
const closure = require('./plugins/closure-plugin');
const flowRemoveTypes = require('flow-remove-types');
const prettier = require('rollup-plugin-prettier');
const replace = require('@rollup/plugin-replace');
const stripBanner = require('rollup-plugin-strip-banner');
const chalk = require('chalk');
const resolve = require('@rollup/plugin-node-resolve').nodeResolve;
const fs = require('fs');
const argv = require('minimist')(process.argv.slice(2));
const Modules = require('./modules');
const Bundles = require('./bundles');
const Stats = require('./stats');
const Sync = require('./sync');
const sizes = require('./plugins/sizes-plugin');
const useForks = require('./plugins/use-forks-plugin');
const dynamicImports = require('./plugins/dynamic-imports');
const Packaging = require('./packaging');
const {asyncRimRaf} = require('./utils');
const codeFrame = require('@babel/code-frame');
const Wrappers = require('./wrappers');

const RELEASE_CHANNEL = process.env.RELEASE_CHANNEL;

// Default to building in experimental mode. If the release channel is set via
// an environment variable, then check if it's "experimental".
const __EXPERIMENTAL__ =
  typeof RELEASE_CHANNEL === 'string'
    ? RELEASE_CHANNEL === 'experimental'
    : true;

// Errors in promises should be fatal.
let loggedErrors = new Set();
process.on('unhandledRejection', err => {
  if (loggedErrors.has(err)) {
    // No need to print it twice.
    process.exit(1);
  }
  throw err;
});

const {
  NODE_ES2015,
  ESM_DEV,
  ESM_PROD,
  NODE_DEV,
  NODE_PROD,
  NODE_PROFILING,
  BUN_DEV,
  BUN_PROD,
  FB_WWW_DEV,
  FB_WWW_PROD,
  FB_WWW_PROFILING,
  RN_OSS_DEV,
  RN_OSS_PROD,
  RN_OSS_PROFILING,
  RN_FB_DEV,
  RN_FB_PROD,
  RN_FB_PROFILING,
  BROWSER_SCRIPT,
} = Bundles.bundleTypes;

const {getFilename} = Bundles;

function parseRequestedNames(names, toCase) {
  let result = [];
  for (let i = 0; i < names.length; i++) {
    let splitNames = names[i].split(',');
    for (let j = 0; j < splitNames.length; j++) {
      let name = splitNames[j].trim();
      if (!name) {
        continue;
      }
      if (toCase === 'uppercase') {
        name = name.toUpperCase();
      } else if (toCase === 'lowercase') {
        name = name.toLowerCase();
      }
      result.push(name);
    }
  }
  return result;
}
const argvType = Array.isArray(argv.type) ? argv.type : [argv.type];
const requestedBundleTypes = parseRequestedNames(
  argv.type ? argvType : [],
  'uppercase'
);

const names = argv._;
const requestedBundleNames = parseRequestedNames(
  names ? names : [],
  'lowercase'
);
const forcePrettyOutput = argv.pretty;
const isWatchMode = argv.watch;
const syncFBSourcePath = argv['sync-fbsource'];
const syncWWWPath = argv['sync-www'];

// Non-ES2015 stuff applied before closure compiler.
const babelPlugins = [
  // These plugins filter out non-ES2015.
  ['@babel/plugin-proposal-class-properties', {loose: true}],
  'syntax-trailing-function-commas',
  // These use loose mode which avoids embedding a runtime.
  // TODO: Remove object spread from the source. Prefer Object.assign instead.
  [
    '@babel/plugin-proposal-object-rest-spread',
    {loose: true, useBuiltIns: true},
  ],
  ['@babel/plugin-transform-template-literals', {loose: true}],
  // TODO: Remove for...of from the source. It requires a runtime to be embedded.
  '@babel/plugin-transform-for-of',
  // TODO: Remove array spread from the source. Prefer .apply instead.
  ['@babel/plugin-transform-spread', {loose: true, useBuiltIns: true}],
  '@babel/plugin-transform-parameters',
  // TODO: Remove array destructuring from the source. Requires runtime.
  ['@babel/plugin-transform-destructuring', {loose: true, useBuiltIns: true}],
  // Transform Object spread to shared/assign
  require('../babel/transform-object-assign'),
];

const babelToES5Plugins = [
  // These plugins transform DEV mode. Closure compiler deals with these in PROD.
  '@babel/plugin-transform-literals',
  '@babel/plugin-transform-arrow-functions',
  '@babel/plugin-transform-block-scoped-functions',
  '@babel/plugin-transform-shorthand-properties',
  '@babel/plugin-transform-computed-properties',
  ['@babel/plugin-transform-block-scoping', {throwIfClosureRequired: true}],
];

function getBabelConfig(
  updateBabelOptions,
  bundleType,
  packageName,
  externals,
  isDevelopment,
  bundle
) {
  const canAccessReactObject =
    packageName === 'react' || externals.indexOf('react') !== -1;
  let options = {
    exclude: '/**/node_modules/**',
    babelrc: false,
    configFile: false,
    presets: [],
    plugins: [...babelPlugins],
    babelHelpers: 'bundled',
    sourcemap: false,
  };
  if (isDevelopment) {
    options.plugins.push(...babelToES5Plugins);
    if (
      bundleType === FB_WWW_DEV ||
      bundleType === RN_OSS_DEV ||
      bundleType === RN_FB_DEV
    ) {
      options.plugins.push(
        // Turn console.error/warn() into a custom wrapper
        [
          require('../babel/transform-replace-console-calls'),
          {
            shouldError: !canAccessReactObject,
          },
        ]
      );
    }
  }
  if (updateBabelOptions) {
    options = updateBabelOptions(options);
  }
  // Controls whether to replace error messages with error codes in production.
  // By default, error messages are replaced in production.
  if (!isDevelopment && bundle.minifyWithProdErrorCodes !== false) {
    options.plugins.push(require('../error-codes/transform-error-messages'));
  }

  return options;
}

let getRollupInteropValue = id => {
  // We're setting Rollup to assume that imports are ES modules unless otherwise specified.
  // However, we also compile ES import syntax to `require()` using Babel.
  // This causes Rollup to turn uses of `import SomeDefaultImport from 'some-module' into
  // references to `SomeDefaultImport.default` due to CJS/ESM interop.
  // Some CJS modules don't have a `.default` export, and the rewritten import is incorrect.
  // Specifying `interop: 'default'` instead will have Rollup use the imported variable as-is,
  // without adding a `.default` to the reference.
  const modulesWithCommonJsExports = [
    'art/core/transform',
    'art/modes/current',
    'art/modes/fast-noSideEffects',
    'art/modes/svg',
    'JSResourceReferenceImpl',
    'error-stack-parser',
    'neo-async',
    'webpack/lib/dependencies/ModuleDependency',
    'webpack/lib/dependencies/NullDependency',
    'webpack/lib/Template',
  ];

  if (modulesWithCommonJsExports.includes(id)) {
    return 'default';
  }

  // For all other modules, handle imports without any import helper utils
  return 'esModule';
};

function getRollupOutputOptions(
  outputPath,
  format,
  globals,
  globalName,
  bundleType
) {
  const isProduction = isProductionBundleType(bundleType);

  return {
    file: outputPath,
    format,
    globals,
    freeze: !isProduction,
    interop: getRollupInteropValue,
    name: globalName,
    sourcemap: false,
    esModule: false,
    exports: 'auto',
  };
}

function getFormat(bundleType) {
  switch (bundleType) {
    case NODE_ES2015:
    case NODE_DEV:
    case NODE_PROD:
    case NODE_PROFILING:
    case BUN_DEV:
    case BUN_PROD:
    case FB_WWW_DEV:
    case FB_WWW_PROD:
    case FB_WWW_PROFILING:
    case RN_OSS_DEV:
    case RN_OSS_PROD:
    case RN_OSS_PROFILING:
    case RN_FB_DEV:
    case RN_FB_PROD:
    case RN_FB_PROFILING:
      return `cjs`;
    case ESM_DEV:
    case ESM_PROD:
      return `es`;
    case BROWSER_SCRIPT:
      return `iife`;
  }
}

function isProductionBundleType(bundleType) {
  switch (bundleType) {
    case NODE_ES2015:
      return true;
    case ESM_DEV:
    case NODE_DEV:
    case BUN_DEV:
    case FB_WWW_DEV:
    case RN_OSS_DEV:
    case RN_FB_DEV:
      return false;
    case ESM_PROD:
    case NODE_PROD:
    case BUN_PROD:
    case NODE_PROFILING:
    case FB_WWW_PROD:
    case FB_WWW_PROFILING:
    case RN_OSS_PROD:
    case RN_OSS_PROFILING:
    case RN_FB_PROD:
    case RN_FB_PROFILING:
    case BROWSER_SCRIPT:
      return true;
    default:
      throw new Error(`Unknown type: ${bundleType}`);
  }
}

function isProfilingBundleType(bundleType) {
  switch (bundleType) {
    case NODE_ES2015:
    case FB_WWW_DEV:
    case FB_WWW_PROD:
    case NODE_DEV:
    case NODE_PROD:
    case BUN_DEV:
    case BUN_PROD:
    case RN_FB_DEV:
    case RN_FB_PROD:
    case RN_OSS_DEV:
    case RN_OSS_PROD:
    case ESM_DEV:
    case ESM_PROD:
    case BROWSER_SCRIPT:
      return false;
    case FB_WWW_PROFILING:
    case NODE_PROFILING:
    case RN_FB_PROFILING:
    case RN_OSS_PROFILING:
      return true;
    default:
      throw new Error(`Unknown type: ${bundleType}`);
  }
}

function getBundleTypeFlags(bundleType) {
  const isFBWWWBundle =
    bundleType === FB_WWW_DEV ||
    bundleType === FB_WWW_PROD ||
    bundleType === FB_WWW_PROFILING;
  const isRNBundle =
    bundleType === RN_OSS_DEV ||
    bundleType === RN_OSS_PROD ||
    bundleType === RN_OSS_PROFILING ||
    bundleType === RN_FB_DEV ||
    bundleType === RN_FB_PROD ||
    bundleType === RN_FB_PROFILING;

  const isFBRNBundle =
    bundleType === RN_FB_DEV ||
    bundleType === RN_FB_PROD ||
    bundleType === RN_FB_PROFILING;

  const shouldStayReadable = isFBWWWBundle || isRNBundle || forcePrettyOutput;

  return {
    isFBWWWBundle,
    isRNBundle,
    isFBRNBundle,
    shouldStayReadable,
  };
}

function forbidFBJSImports() {
  return {
    name: 'forbidFBJSImports',
    resolveId(importee, importer) {
      if (/^fbjs\//.test(importee)) {
        throw new Error(
          `Don't import ${importee} (found in ${importer}). ` +
            `Use the utilities in packages/shared/ instead.`
        );
      }
    },
  };
}

function getPlugins(
  entry,
  externals,
  updateBabelOptions,
  filename,
  packageName,
  bundleType,
  globalName,
  moduleType,
  pureExternalModules,
  bundle
) {
  try {
    const forks = Modules.getForks(bundleType, entry, moduleType, bundle);
    const isProduction = isProductionBundleType(bundleType);
    const isProfiling = isProfilingBundleType(bundleType);

    const needsMinifiedByClosure =
      bundleType !== ESM_PROD && bundleType !== ESM_DEV;

    return [
      // Keep dynamic imports as externals
      dynamicImports(),
      {
        name: 'rollup-plugin-flow-remove-types',
        transform(code) {
          const transformed = flowRemoveTypes(code);
          return {
            code: transformed.toString(),
            map: null,
          };
        },
      },
      // Shim any modules that need forking in this environment.
      useForks(forks),
      // Ensure we don't try to bundle any fbjs modules.
      forbidFBJSImports(),
      // Use Node resolution mechanism.
      resolve({
        // skip: externals, // TODO: options.skip was removed in @rollup/plugin-node-resolve 3.0.0
      }),
      // Remove license headers from individual modules
      stripBanner({
        exclude: 'node_modules/**/*',
      }),
      // Compile to ES2015.
      babel(
        getBabelConfig(
          updateBabelOptions,
          bundleType,
          packageName,
          externals,
          !isProduction,
          bundle
        )
      ),
      // Remove 'use strict' from individual source files.
      {
        name: "remove 'use strict'",
        transform(source) {
          return source.replace(/['"]use strict["']/g, '');
        },
      },
      // Turn __DEV__ and process.env checks into constants.
      replace({
        preventAssignment: true,
        values: {
          __DEV__: isProduction ? 'false' : 'true',
          __PROFILE__: isProfiling || !isProduction ? 'true' : 'false',
          'process.env.NODE_ENV': isProduction
            ? "'production'"
            : "'development'",
          __EXPERIMENTAL__,
        },
      }),
      {
        name: 'top-level-definitions',
        renderChunk(source) {
          return Wrappers.wrapWithTopLevelDefinitions(
            source,
            bundleType,
            globalName,
            filename,
            moduleType,
            bundle.wrapWithModuleBoundaries
          );
        },
      },
      // For production builds, compile with Closure. We do this even for the
      // "non-minified" production builds because Closure is much better at
      // minification than what most applications use. During this step, we do
      // preserve the original symbol names, though, so the resulting code is
      // relatively readable.
      //
      // For the minified builds, the names will be mangled later.
      //
      // We don't bother with sourcemaps at this step. The sourcemaps we publish
      // are only for whitespace and symbol renaming; they don't map back to
      // before Closure was applied.
      needsMinifiedByClosure &&
        closure({
          compilation_level: 'SIMPLE',
          language_in: 'ECMASCRIPT_2020',
          language_out:
            bundleType === NODE_ES2015
              ? 'ECMASCRIPT_2020'
              : bundleType === BROWSER_SCRIPT
                ? 'ECMASCRIPT5'
                : 'ECMASCRIPT5_STRICT',
          emit_use_strict:
            bundleType !== BROWSER_SCRIPT &&
            bundleType !== ESM_PROD &&
            bundleType !== ESM_DEV,
          env: 'CUSTOM',
          warning_level: 'QUIET',
          source_map_include_content: true,
          use_types_for_optimization: false,
          process_common_js_modules: false,
          rewrite_polyfills: false,
          inject_libraries: false,
          allow_dynamic_import: true,

          // Don't let it create global variables in the browser.
          // https://github.com/facebook/react/issues/10909
          assume_function_wrapper: true,

          // Don't rename symbols (variable names, functions, etc). We leave
          // this up to the application to handle, if they want. Otherwise gzip
          // takes care of it.
          renaming: false,
        }),
      needsMinifiedByClosure &&
        // Add the whitespace back
        prettier({
          parser: 'flow',
          singleQuote: false,
          trailingComma: 'none',
          bracketSpacing: true,
        }),
      {
        name: 'license-and-signature-header',
        renderChunk(source) {
          return Wrappers.wrapWithLicenseHeader(
            source,
            bundleType,
            globalName,
            filename,
            moduleType
          );
        },
      },
      // Record bundle size.
      sizes({
        getSize: (size, gzip) => {
          const currentSizes = Stats.currentBuildResults.bundleSizes;
          const recordIndex = currentSizes.findIndex(
            record =>
              record.filename === filename && record.bundleType === bundleType
          );
          const index = recordIndex !== -1 ? recordIndex : currentSizes.length;
          currentSizes[index] = {
            filename,
            bundleType,
            packageName,
            size,
            gzip,
          };
        },
      }),
    ].filter(Boolean);
  } catch (error) {
    console.error(
      chalk.red(`There was an error preparing plugins for entry "${entry}"`)
    );
    throw error;
  }
}

function shouldSkipBundle(bundle, bundleType) {
  const shouldSkipBundleType = bundle.bundleTypes.indexOf(bundleType) === -1;
  if (shouldSkipBundleType) {
    return true;
  }
  if (requestedBundleTypes.length > 0) {
    const hasRequestedBundleType = requestedBundleTypes.some(requestedType =>
      bundleType.includes(requestedType)
    );
    if (!hasRequestedBundleType) {
      return true;
    }
  }
  if (requestedBundleNames.length > 0) {
    // If the name ends with `something/index` we only match if the
    // entry ends in something. Such as `react-dom/index` only matches
    // `react-dom` but not `react-dom/server`. Everything else is fuzzy
    // search.
    const entryLowerCase = bundle.entry.toLowerCase() + '/index.js';
    const isAskingForDifferentNames = requestedBundleNames.every(
      requestedName => {
        const matchEntry = entryLowerCase.indexOf(requestedName) !== -1;
        if (!bundle.name) {
          return !matchEntry;
        }
        const matchName =
          bundle.name.toLowerCase().indexOf(requestedName) !== -1;
        return !matchEntry && !matchName;
      }
    );
    if (isAskingForDifferentNames) {
      return true;
    }
  }
  return false;
}

function resolveEntryFork(resolvedEntry, isFBBundle) {
  // Pick which entry point fork to use:
  // .modern.fb.js
  // .classic.fb.js
  // .fb.js
  // .stable.js
  // .experimental.js
  // .js
  // or any of those plus .development.js

  if (isFBBundle) {
    const resolvedFBEntry = resolvedEntry.replace(
      '.js',
      __EXPERIMENTAL__ ? '.modern.fb.js' : '.classic.fb.js'
    );
    const developmentFBEntry = resolvedFBEntry.replace(
      '.js',
      '.development.js'
    );
    if (fs.existsSync(developmentFBEntry)) {
      return developmentFBEntry;
    }
    if (fs.existsSync(resolvedFBEntry)) {
      return resolvedFBEntry;
    }
    const resolvedGenericFBEntry = resolvedEntry.replace('.js', '.fb.js');
    const developmentGenericFBEntry = resolvedGenericFBEntry.replace(
      '.js',
      '.development.js'
    );
    if (fs.existsSync(developmentGenericFBEntry)) {
      return developmentGenericFBEntry;
    }
    if (fs.existsSync(resolvedGenericFBEntry)) {
      return resolvedGenericFBEntry;
    }
    // Even if it's a FB bundle we fallthrough to pick stable or experimental if we don't have an FB fork.
  }
  const resolvedForkedEntry = resolvedEntry.replace(
    '.js',
    __EXPERIMENTAL__ ? '.experimental.js' : '.stable.js'
  );
  const devForkedEntry = resolvedForkedEntry.replace('.js', '.development.js');
  if (fs.existsSync(devForkedEntry)) {
    return devForkedEntry;
  }
  if (fs.existsSync(resolvedForkedEntry)) {
    return resolvedForkedEntry;
  }
  // Just use the plain .js one.
  return resolvedEntry;
}

async function createBundle(bundle, bundleType) {
  const filename = getFilename(bundle, bundleType);
  const logKey =
    chalk.white.bold(filename) + chalk.dim(` (${bundleType.toLowerCase()})`);
  const format = getFormat(bundleType);
  const packageName = Packaging.getPackageName(bundle.entry);

  const {isFBWWWBundle, isFBRNBundle} = getBundleTypeFlags(bundleType);

  let resolvedEntry = resolveEntryFork(
    require.resolve(bundle.entry),
    isFBWWWBundle || isFBRNBundle,
    !isProductionBundleType(bundleType)
  );

  const peerGlobals = Modules.getPeerGlobals(bundle.externals, bundleType);
  let externals = Object.keys(peerGlobals);

  const deps = Modules.getDependencies(bundleType, bundle.entry);
  externals = externals.concat(deps);

  const importSideEffects = Modules.getImportSideEffects();
  const pureExternalModules = Object.keys(importSideEffects).filter(
    module => !importSideEffects[module]
  );

  const rollupConfig = {
    input: resolvedEntry,
    treeshake: {
      moduleSideEffects: (id, external) =>
        !(external && pureExternalModules.includes(id)),
      propertyReadSideEffects: false,
    },
    external(id) {
      const containsThisModule = pkg => id === pkg || id.startsWith(pkg + '/');
      const isProvidedByDependency = externals.some(containsThisModule);
      if (isProvidedByDependency) {
        if (id.indexOf('/src/') !== -1) {
          throw Error(
            'You are trying to import ' +
              id +
              ' but ' +
              externals.find(containsThisModule) +
              ' is one of npm dependencies, ' +
              'so it will not contain that source file. You probably want ' +
              'to create a new bundle entry point for it instead.'
          );
        }
        return true;
      }
      return !!peerGlobals[id];
    },
    onwarn: handleRollupWarning,
    plugins: getPlugins(
      bundle.entry,
      externals,
      bundle.babel,
      filename,
      packageName,
      bundleType,
      bundle.global,
      bundle.moduleType,
      pureExternalModules,
      bundle
    ),
    output: {
      externalLiveBindings: false,
      freeze: false,
      interop: getRollupInteropValue,
      esModule: false,
    },
  };
  const mainOutputPath = Packaging.getBundleOutputPath(
    bundle,
    bundleType,
    filename,
    packageName
  );

  const rollupOutputOptions = getRollupOutputOptions(
    mainOutputPath,
    format,
    peerGlobals,
    bundle.global,
    bundleType
  );

  if (isWatchMode) {
    rollupConfig.output = [rollupOutputOptions];
    const watcher = rollup.watch(rollupConfig);
    watcher.on('event', async event => {
      switch (event.code) {
        case 'BUNDLE_START':
          console.log(`${chalk.bgYellow.black(' BUILDING ')} ${logKey}`);
          break;
        case 'BUNDLE_END':
          console.log(`${chalk.bgGreen.black(' COMPLETE ')} ${logKey}\n`);
          break;
        case 'ERROR':
        case 'FATAL':
          console.log(`${chalk.bgRed.black(' OH NOES! ')} ${logKey}\n`);
          handleRollupError(event.error);
          break;
      }
    });
  } else {
    console.log(`${chalk.bgYellow.black(' BUILDING ')} ${logKey}`);
    try {
      const result = await rollup.rollup(rollupConfig);
      await result.write(rollupOutputOptions);
    } catch (error) {
      console.log(`${chalk.bgRed.black(' OH NOES! ')} ${logKey}\n`);
      handleRollupError(error);
      throw error;
    }
    console.log(`${chalk.bgGreen.black(' COMPLETE ')} ${logKey}\n`);
  }
}

function handleRollupWarning(warning) {
  if (warning.code === 'UNUSED_EXTERNAL_IMPORT') {
    const match = warning.message.match(/external module "([^"]+)"/);
    if (!match || typeof match[1] !== 'string') {
      throw new Error(
        'Could not parse a Rollup warning. ' + 'Fix this method.'
      );
    }
    const importSideEffects = Modules.getImportSideEffects();
    const externalModule = match[1];
    if (typeof importSideEffects[externalModule] !== 'boolean') {
      throw new Error(
        'An external module "' +
          externalModule +
          '" is used in a DEV-only code path ' +
          'but we do not know if it is safe to omit an unused require() to it in production. ' +
          'Please add it to the `importSideEffects` list in `scripts/rollup/modules.js`.'
      );
    }
    // Don't warn. We will remove side effectless require() in a later pass.
    return;
  }

  if (warning.code === 'CIRCULAR_DEPENDENCY') {
    // Ignored
  } else if (typeof warning.code === 'string') {
    // This is a warning coming from Rollup itself.
    // These tend to be important (e.g. clashes in namespaced exports)
    // so we'll fail the build on any of them.
    console.error();
    console.error(warning.message || warning);
    console.error();
    process.exit(1);
  } else {
    // The warning is from one of the plugins.
    // Maybe it's not important, so just print it.
    console.warn(warning.message || warning);
  }
}

function handleRollupError(error) {
  loggedErrors.add(error);
  if (!error.code) {
    console.error(error);
    return;
  }
  console.error(
    `\x1b[31m-- ${error.code}${error.plugin ? ` (${error.plugin})` : ''} --`
  );
  console.error(error.stack);
  if (error.loc && error.loc.file) {
    const {file, line, column} = error.loc;
    // This looks like an error from Rollup, e.g. missing export.
    // We'll use the accurate line numbers provided by Rollup but
    // use Babel code frame because it looks nicer.
    const rawLines = fs.readFileSync(file, 'utf-8');
    // column + 1 is required due to rollup counting column start position from 0
    // whereas babel-code-frame counts from 1
    const frame = codeFrame(rawLines, line, column + 1, {
      highlightCode: true,
    });
    console.error(frame);
  } else if (error.codeFrame) {
    // This looks like an error from a plugin (e.g. Babel).
    // In this case we'll resort to displaying the provided code frame
    // because we can't be sure the reported location is accurate.
    console.error(error.codeFrame);
  }
}

async function buildEverything() {
  if (!argv['unsafe-partial']) {
    await asyncRimRaf('build');
  }

  // Run them serially for better console output
  // and to avoid any potential race conditions.

  let bundles = [];
  // eslint-disable-next-line no-for-of-loops/no-for-of-loops
  for (const bundle of Bundles.bundles) {
    bundles.push(
      [bundle, NODE_ES2015],
      [bundle, ESM_DEV],
      [bundle, ESM_PROD],
      [bundle, NODE_DEV],
      [bundle, NODE_PROD],
      [bundle, NODE_PROFILING],
      [bundle, BUN_DEV],
      [bundle, BUN_PROD],
      [bundle, FB_WWW_DEV],
      [bundle, FB_WWW_PROD],
      [bundle, FB_WWW_PROFILING],
      [bundle, RN_OSS_DEV],
      [bundle, RN_OSS_PROD],
      [bundle, RN_OSS_PROFILING],
      [bundle, RN_FB_DEV],
      [bundle, RN_FB_PROD],
      [bundle, RN_FB_PROFILING],
      [bundle, BROWSER_SCRIPT]
    );
  }

  bundles = bundles.filter(([bundle, bundleType]) => {
    return !shouldSkipBundle(bundle, bundleType);
  });

  if (process.env.CI_TOTAL && process.env.CI_INDEX) {
    const nodeTotal = parseInt(process.env.CI_TOTAL, 10);
    const nodeIndex = parseInt(process.env.CI_INDEX, 10);
    bundles = bundles.filter((_, i) => i % nodeTotal === nodeIndex);
  }

  // eslint-disable-next-line no-for-of-loops/no-for-of-loops
  for (const [bundle, bundleType] of bundles) {
    await createBundle(bundle, bundleType);
  }

  await Packaging.copyAllShims();
  await Packaging.prepareNpmPackages();

  if (syncFBSourcePath) {
    await Sync.syncReactNative(syncFBSourcePath);
  } else if (syncWWWPath) {
    await Sync.syncReactDom('build/facebook-www', syncWWWPath);
  }

  console.log(Stats.printResults());
  if (!forcePrettyOutput) {
    Stats.saveResults();
  }
}

buildEverything();