From ec463c53ea9daaa09c0b94e6265495ac8ac509e6 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Mon, 24 Jun 2024 16:20:58 -0400 Subject: [PATCH] [ci] Parallelize yarn build and yarn lint-build ghstack-source-id: fc61ba266b164d9bf3831d4d6e37da261481a916 Pull Request resolved: https://github.com/facebook/react/pull/30071 --- .circleci/config.yml | 2 +- .github/workflows/runtime_build.yml | 51 +- scripts/rollup/build-all-release-channels.js | 120 ++- scripts/rollup/build-ghaction.js | 871 +++++++++++++++++++ 4 files changed, 995 insertions(+), 49 deletions(-) create mode 100644 scripts/rollup/build-ghaction.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 18372084d3685..a4671f4f58a64 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -97,7 +97,7 @@ jobs: steps: - checkout - setup_node_modules - - run: yarn build + - run: yarn build --ci=circleci - persist_to_workspace: root: . paths: diff --git a/.github/workflows/runtime_build.yml b/.github/workflows/runtime_build.yml index 6753b083298f2..a380c2c85c722 100644 --- a/.github/workflows/runtime_build.yml +++ b/.github/workflows/runtime_build.yml @@ -8,9 +8,34 @@ on: - 'compiler/**' jobs: + define_build_params: + name: Build build params + runs-on: ubuntu-latest + outputs: + bundle_type: ${{ steps.define_bundle_types.outputs.result }} + release_channel: ${{ steps.define_release_channels.outputs.result }} + steps: + - uses: actions/checkout@v4 + - uses: actions/github-script@v7 + id: define_bundle_types + with: + script: | + const {bundleTypes} = require('./scripts/rollup/bundles'); + return Object.values(bundleTypes); + - uses: actions/github-script@v7 + id: define_release_channels + with: + script: | + return ['stable', 'experimental']; + build: name: yarn build runs-on: ubuntu-latest + needs: define_build_params + strategy: + matrix: + bundle_type: ${{ fromJSON(needs.define_build_params.outputs.bundle_type) }} + release_channel: ${{ fromJSON(needs.define_build_params.outputs.release_channel) }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -25,17 +50,21 @@ jobs: path: "**/node_modules" key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - run: yarn install --frozen-lockfile - - run: yarn build - - name: Cache build - uses: actions/cache@v4 - id: build_cache + - run: yarn build --b=${{ matrix.bundle_type }} --r=${{ matrix.release_channel }} --ci=github + - name: Archive build + uses: actions/upload-artifact@v4 with: - path: "build/**" - key: yarn-build-${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + name: ${{ matrix.bundle_type }}-${{ matrix.release_channel }} + path: | + build/** lint_build: name: yarn lint-build - needs: build + needs: [define_build_params, build] + strategy: + matrix: + bundle_type: ${{ fromJSON(needs.define_build_params.outputs.bundle_type) }} + release_channel: ${{ fromJSON(needs.define_build_params.outputs.release_channel) }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -50,11 +79,9 @@ jobs: with: path: "**/node_modules" key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - - name: Restore cached build - uses: actions/cache@v4 - id: build_cache + - name: Restore archived build + uses: actions/download-artifact@v4 with: - path: "build/**" - key: yarn-build-${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + name: ${{ matrix.bundle_type }}-${{ matrix.release_channel }} - run: yarn install --frozen-lockfile - run: yarn lint-build diff --git a/scripts/rollup/build-all-release-channels.js b/scripts/rollup/build-all-release-channels.js index 5e8cd27cf5e47..2dac6f7954d3f 100644 --- a/scripts/rollup/build-all-release-channels.js +++ b/scripts/rollup/build-all-release-channels.js @@ -15,6 +15,9 @@ const { canaryChannelLabel, rcNumber, } = require('../../ReactVersions'); +const yargs = require('yargs'); +const Bundles = require('./bundles'); +const {buildEverything} = require('./build-ghaction'); // Runs the build script for both stable and experimental release channels, // by configuring an environment variable. @@ -53,44 +56,87 @@ fs.writeFileSync( `export default '${PLACEHOLDER_REACT_VERSION}';\n` ); -if (process.env.CIRCLE_NODE_TOTAL) { - // In CI, we use multiple concurrent processes. Allocate half the processes to - // build the stable channel, and the other half for experimental. Override - // the environment variables to "trick" the underlying build script. - const total = parseInt(process.env.CIRCLE_NODE_TOTAL, 10); - const halfTotal = Math.floor(total / 2); - const index = parseInt(process.env.CIRCLE_NODE_INDEX, 10); - if (index < halfTotal) { - const nodeTotal = halfTotal; - const nodeIndex = index; - buildForChannel('stable', nodeTotal, nodeIndex); - processStable('./build'); +const argv = yargs.wrap(yargs.terminalWidth()).options({ + releaseChannel: { + alias: 'r', + describe: 'Build the given release channel.', + requiresArg: true, + type: 'string', + default: 'experimental', + choices: ['experimental', 'stable'], + }, + bundleType: { + alias: 'b', + describe: 'Build the given bundle type.', + requiresArg: true, + type: 'string', + choices: Object.values(Bundles.bundleTypes), + }, + ci: { + describe: 'Run tests in CI', + requiresArg: false, + type: 'choices', + choices: ['circleci', 'github'], + }, +}).argv; + +async function main() { + if (argv.ci === 'github') { + // ./scripts/rollup/build was being used by spawning a new process and passing via ENV variables + // so let's just preserve this for now and rewrite it later to just take a function arg + process.env.RELEASE_CHANNEL = argv.releaseChannel; + await buildEverything(argv.bundleType); + switch (argv.releaseChannel) { + case 'stable': { + processStable('./build'); + break; + } + case 'experimental': { + processExperimental('./build'); + break; + } + default: + throw new Error(`Unknown release channel ${argv.releaseChannel}`); + } + } else if (argv.ci === 'circleci') { + // In CI, we use multiple concurrent processes. Allocate half the processes to + // build the stable channel, and the other half for experimental. Override + // the environment variables to "trick" the underlying build script. + const total = parseInt(process.env.CIRCLE_NODE_TOTAL, 10); + const halfTotal = Math.floor(total / 2); + const index = parseInt(process.env.CIRCLE_NODE_INDEX, 10); + if (index < halfTotal) { + const nodeTotal = halfTotal; + const nodeIndex = index; + buildForChannel('stable', nodeTotal, nodeIndex); + processStable('./build'); + } else { + const nodeTotal = total - halfTotal; + const nodeIndex = index - halfTotal; + buildForChannel('experimental', nodeTotal, nodeIndex); + processExperimental('./build'); + } } else { - const nodeTotal = total - halfTotal; - const nodeIndex = index - halfTotal; - buildForChannel('experimental', nodeTotal, nodeIndex); - processExperimental('./build'); + // Running locally, no concurrency. Move each channel's build artifacts into + // a temporary directory so that they don't conflict. + buildForChannel('stable', '', ''); + const stableDir = tmp.dirSync().name; + crossDeviceRenameSync('./build', stableDir); + processStable(stableDir); + buildForChannel('experimental', '', ''); + const experimentalDir = tmp.dirSync().name; + crossDeviceRenameSync('./build', experimentalDir); + processExperimental(experimentalDir); + + // Then merge the experimental folder into the stable one. processExperimental + // will have already removed conflicting files. + // + // In CI, merging is handled automatically by CircleCI's workspace feature. + mergeDirsSync(experimentalDir + '/', stableDir + '/'); + + // Now restore the combined directory back to its original name + crossDeviceRenameSync(stableDir, './build'); } -} else { - // Running locally, no concurrency. Move each channel's build artifacts into - // a temporary directory so that they don't conflict. - buildForChannel('stable', '', ''); - const stableDir = tmp.dirSync().name; - crossDeviceRenameSync('./build', stableDir); - processStable(stableDir); - buildForChannel('experimental', '', ''); - const experimentalDir = tmp.dirSync().name; - crossDeviceRenameSync('./build', experimentalDir); - processExperimental(experimentalDir); - - // Then merge the experimental folder into the stable one. processExperimental - // will have already removed conflicting files. - // - // In CI, merging is handled automatically by CircleCI's workspace feature. - mergeDirsSync(experimentalDir + '/', stableDir + '/'); - - // Now restore the combined directory back to its original name - crossDeviceRenameSync(stableDir, './build'); } function buildForChannel(channel, nodeTotal, nodeIndex) { @@ -457,3 +503,5 @@ function mergeDirsSync(source, destination) { } } } + +main(); diff --git a/scripts/rollup/build-ghaction.js b/scripts/rollup/build-ghaction.js new file mode 100644 index 0000000000000..60871e3e9208c --- /dev/null +++ b/scripts/rollup/build-ghaction.js @@ -0,0 +1,871 @@ +'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 = argv.type + ? parseRequestedNames(argvType, 'uppercase') + : []; + +const requestedBundleNames = parseRequestedNames(argv._, '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, + // 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 isAskingForDifferentType = requestedBundleTypes.some( + requestedType => !bundleType.includes(requestedType) + ); + if (isAskingForDifferentType) { + 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(bundleTypeToBuild) { + 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); + }); + + // TODO: cleanup the rest of this function + bundles = bundles.filter( + ([, bundleType]) => bundleType === bundleTypeToBuild + ); + + await Promise.all( + bundles.map(([bundle, bundleType]) => { + return 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(); + } +} + +module.exports = { + buildEverything, +};