@samitouri / QOS-React-2 / commits / 3bc79cd41c

[ci] Parallelize yarn build

ghstack-source-id: 8a13b456f1638a44c6f960c44f5752e8e4d32507 Pull Request resolved: https://github.com/facebook/react/pull/30071

Lauren Tan committed Jul 12, 2024 at 11:21 UTC 3bc79cd41c53d46776d17e11612f7ab56eda2c35
5 files changed +979 -44
.circleci/config.yml
+1 -1
@@ -97,7 +97,7 @@ jobs:
97 steps:
98 - checkout
99 - setup_node_modules
100 - - run: yarn build
100 + - run: yarn build --ci=circleci
101 - persist_to_workspace:
102 root: .
103 paths:
.github/workflows/runtime_build.yml
+19 -7
@@ -14,6 +14,11 @@ jobs:
14 build_and_lint:
15 name: yarn build and lint
16 runs-on: ubuntu-latest
17 + strategy:
18 + matrix:
19 + # yml is dumb. update the --total arg to yarn build if you change the number of workers
20 + worker_id: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]
21 + release_channel: [stable, experimental]
22 steps:
23 - uses: actions/checkout@v4
24 - uses: actions/setup-node@v4
@@ -32,11 +37,18 @@ jobs:
37 path: "**/node_modules"
38 key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
39 - run: yarn install --frozen-lockfile
35 - - run: yarn build
36 - - run: yarn lint-build
37 - - name: Cache build
38 - uses: actions/cache@v4
39 - id: build_cache
40 + - run: yarn build --index=${{ matrix.worker_id }} --total=20 --r=${{ matrix.release_channel }} --ci=github
41 + env:
42 + CI: github
43 + RELEASE_CHANNEL: ${{ matrix.release_channel }}
44 + NODE_INDEX: ${{ matrix.worker_id }}
45 + - name: Lint build
46 + run: yarn lint-build
47 + - name: Display structure of build
48 + run: ls -R build
49 + - name: Archive build
50 + uses: actions/upload-artifact@v4
51 with:
41 - path: build/**
42 - key: yarn-build-${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
52 + name: build_${{ matrix.worker_id }}_${{ matrix.release_channel }}
53 + path: |
54 + build
scripts/rollup/build-all-release-channels.js
+84 -36
@@ -15,6 +15,8 @@ const {
15 canaryChannelLabel,
16 rcNumber,
17 } = require('../../ReactVersions');
18 +const yargs = require('yargs');
19 +const {buildEverything} = require('./build-ghaction');
20
21 // Runs the build script for both stable and experimental release channels,
22 // by configuring an environment variable.
@@ -51,44 +53,88 @@ fs.writeFileSync(
53 `export default '${PLACEHOLDER_REACT_VERSION}';\n`
54 );
55
54 -if (process.env.CIRCLE_NODE_TOTAL) {
55 - // In CI, we use multiple concurrent processes. Allocate half the processes to
56 - // build the stable channel, and the other half for experimental. Override
57 - // the environment variables to "trick" the underlying build script.
58 - const total = parseInt(process.env.CIRCLE_NODE_TOTAL, 10);
59 - const halfTotal = Math.floor(total / 2);
60 - const index = parseInt(process.env.CIRCLE_NODE_INDEX, 10);
61 - if (index < halfTotal) {
62 - const nodeTotal = halfTotal;
63 - const nodeIndex = index;
64 - buildForChannel('stable', nodeTotal, nodeIndex);
65 - processStable('./build');
56 +const argv = yargs.wrap(yargs.terminalWidth()).options({
57 + releaseChannel: {
58 + alias: 'r',
59 + describe: 'Build the given release channel.',
60 + requiresArg: true,
61 + type: 'string',
62 + choices: ['experimental', 'stable'],
63 + },
64 + index: {
65 + alias: 'i',
66 + describe: 'Worker id.',
67 + requiresArg: true,
68 + type: 'number',
69 + },
70 + total: {
71 + alias: 't',
72 + describe: 'Total number of workers.',
73 + requiresArg: true,
74 + type: 'number',
75 + },
76 + ci: {
77 + describe: 'Run tests in CI',
78 + requiresArg: false,
79 + type: 'choices',
80 + choices: ['circleci', 'github'],
81 + },
82 +}).argv;
83 +
84 +async function main() {
85 + if (argv.ci === 'github') {
86 + await buildEverything(argv.index, argv.total);
87 + switch (argv.releaseChannel) {
88 + case 'stable': {
89 + processStable('./build');
90 + break;
91 + }
92 + case 'experimental': {
93 + processExperimental('./build');
94 + break;
95 + }
96 + default:
97 + throw new Error(`Unknown release channel ${argv.releaseChannel}`);
98 + }
99 + } else if (argv.ci === 'circleci') {
100 + // In CI, we use multiple concurrent processes. Allocate half the processes to
101 + // build the stable channel, and the other half for experimental. Override
102 + // the environment variables to "trick" the underlying build script.
103 + const total = parseInt(process.env.CIRCLE_NODE_TOTAL, 10);
104 + const halfTotal = Math.floor(total / 2);
105 + const index = parseInt(process.env.CIRCLE_NODE_INDEX, 10);
106 + if (index < halfTotal) {
107 + const nodeTotal = halfTotal;
108 + const nodeIndex = index;
109 + buildForChannel('stable', nodeTotal, nodeIndex);
110 + processStable('./build');
111 + } else {
112 + const nodeTotal = total - halfTotal;
113 + const nodeIndex = index - halfTotal;
114 + buildForChannel('experimental', nodeTotal, nodeIndex);
115 + processExperimental('./build');
116 + }
117 } else {
67 - const nodeTotal = total - halfTotal;
68 - const nodeIndex = index - halfTotal;
69 - buildForChannel('experimental', nodeTotal, nodeIndex);
70 - processExperimental('./build');
118 + // Running locally, no concurrency. Move each channel's build artifacts into
119 + // a temporary directory so that they don't conflict.
120 + buildForChannel('stable', '', '');
121 + const stableDir = tmp.dirSync().name;
122 + crossDeviceRenameSync('./build', stableDir);
123 + processStable(stableDir);
124 + buildForChannel('experimental', '', '');
125 + const experimentalDir = tmp.dirSync().name;
126 + crossDeviceRenameSync('./build', experimentalDir);
127 + processExperimental(experimentalDir);
128 +
129 + // Then merge the experimental folder into the stable one. processExperimental
130 + // will have already removed conflicting files.
131 + //
132 + // In CI, merging is handled automatically by CircleCI's workspace feature.
133 + mergeDirsSync(experimentalDir + '/', stableDir + '/');
134 +
135 + // Now restore the combined directory back to its original name
136 + crossDeviceRenameSync(stableDir, './build');
137 }
72 -} else {
73 - // Running locally, no concurrency. Move each channel's build artifacts into
74 - // a temporary directory so that they don't conflict.
75 - buildForChannel('stable', '', '');
76 - const stableDir = tmp.dirSync().name;
77 - crossDeviceRenameSync('./build', stableDir);
78 - processStable(stableDir);
79 - buildForChannel('experimental', '', '');
80 - const experimentalDir = tmp.dirSync().name;
81 - crossDeviceRenameSync('./build', experimentalDir);
82 - processExperimental(experimentalDir);
83 -
84 - // Then merge the experimental folder into the stable one. processExperimental
85 - // will have already removed conflicting files.
86 - //
87 - // In CI, merging is handled automatically by CircleCI's workspace feature.
88 - mergeDirsSync(experimentalDir + '/', stableDir + '/');
89 -
90 - // Now restore the combined directory back to its original name
91 - crossDeviceRenameSync(stableDir, './build');
138 }
139
140 function buildForChannel(channel, nodeTotal, nodeIndex) {
@@ -455,3 +501,5 @@ function mergeDirsSync(source, destination) {
501 }
502 }
503 }
504 +
505 +main();
scripts/rollup/build-ghaction.js new
+869
@@ -0,0 +1,869 @@
1 +'use strict';
2 +
3 +const rollup = require('rollup');
4 +const babel = require('@rollup/plugin-babel').babel;
5 +const closure = require('./plugins/closure-plugin');
6 +const flowRemoveTypes = require('flow-remove-types');
7 +const prettier = require('rollup-plugin-prettier');
8 +const replace = require('@rollup/plugin-replace');
9 +const stripBanner = require('rollup-plugin-strip-banner');
10 +const chalk = require('chalk');
11 +const resolve = require('@rollup/plugin-node-resolve').nodeResolve;
12 +const fs = require('fs');
13 +const argv = require('minimist')(process.argv.slice(2));
14 +const Modules = require('./modules');
15 +const Bundles = require('./bundles');
16 +const Stats = require('./stats');
17 +const Sync = require('./sync');
18 +const sizes = require('./plugins/sizes-plugin');
19 +const useForks = require('./plugins/use-forks-plugin');
20 +const dynamicImports = require('./plugins/dynamic-imports');
21 +const Packaging = require('./packaging');
22 +const {asyncRimRaf} = require('./utils');
23 +const codeFrame = require('@babel/code-frame');
24 +const Wrappers = require('./wrappers');
25 +
26 +const RELEASE_CHANNEL = process.env.RELEASE_CHANNEL;
27 +
28 +// Default to building in experimental mode. If the release channel is set via
29 +// an environment variable, then check if it's "experimental".
30 +const __EXPERIMENTAL__ =
31 + typeof RELEASE_CHANNEL === 'string'
32 + ? RELEASE_CHANNEL === 'experimental'
33 + : true;
34 +
35 +// Errors in promises should be fatal.
36 +let loggedErrors = new Set();
37 +process.on('unhandledRejection', err => {
38 + if (loggedErrors.has(err)) {
39 + // No need to print it twice.
40 + process.exit(1);
41 + }
42 + throw err;
43 +});
44 +
45 +const {
46 + NODE_ES2015,
47 + ESM_DEV,
48 + ESM_PROD,
49 + NODE_DEV,
50 + NODE_PROD,
51 + NODE_PROFILING,
52 + BUN_DEV,
53 + BUN_PROD,
54 + FB_WWW_DEV,
55 + FB_WWW_PROD,
56 + FB_WWW_PROFILING,
57 + RN_OSS_DEV,
58 + RN_OSS_PROD,
59 + RN_OSS_PROFILING,
60 + RN_FB_DEV,
61 + RN_FB_PROD,
62 + RN_FB_PROFILING,
63 + BROWSER_SCRIPT,
64 +} = Bundles.bundleTypes;
65 +
66 +const {getFilename} = Bundles;
67 +
68 +function parseRequestedNames(names, toCase) {
69 + let result = [];
70 + for (let i = 0; i < names.length; i++) {
71 + let splitNames = names[i].split(',');
72 + for (let j = 0; j < splitNames.length; j++) {
73 + let name = splitNames[j].trim();
74 + if (!name) {
75 + continue;
76 + }
77 + if (toCase === 'uppercase') {
78 + name = name.toUpperCase();
79 + } else if (toCase === 'lowercase') {
80 + name = name.toLowerCase();
81 + }
82 + result.push(name);
83 + }
84 + }
85 + return result;
86 +}
87 +
88 +const argvType = Array.isArray(argv.type) ? argv.type : [argv.type];
89 +const requestedBundleTypes = argv.type
90 + ? parseRequestedNames(argvType, 'uppercase')
91 + : [];
92 +
93 +const requestedBundleNames = parseRequestedNames(argv._, 'lowercase');
94 +const forcePrettyOutput = argv.pretty;
95 +const isWatchMode = argv.watch;
96 +const syncFBSourcePath = argv['sync-fbsource'];
97 +const syncWWWPath = argv['sync-www'];
98 +
99 +// Non-ES2015 stuff applied before closure compiler.
100 +const babelPlugins = [
101 + // These plugins filter out non-ES2015.
102 + ['@babel/plugin-proposal-class-properties', {loose: true}],
103 + 'syntax-trailing-function-commas',
104 + // These use loose mode which avoids embedding a runtime.
105 + // TODO: Remove object spread from the source. Prefer Object.assign instead.
106 + [
107 + '@babel/plugin-proposal-object-rest-spread',
108 + {loose: true, useBuiltIns: true},
109 + ],
110 + ['@babel/plugin-transform-template-literals', {loose: true}],
111 + // TODO: Remove for...of from the source. It requires a runtime to be embedded.
112 + '@babel/plugin-transform-for-of',
113 + // TODO: Remove array spread from the source. Prefer .apply instead.
114 + ['@babel/plugin-transform-spread', {loose: true, useBuiltIns: true}],
115 + '@babel/plugin-transform-parameters',
116 + // TODO: Remove array destructuring from the source. Requires runtime.
117 + ['@babel/plugin-transform-destructuring', {loose: true, useBuiltIns: true}],
118 + // Transform Object spread to shared/assign
119 + require('../babel/transform-object-assign'),
120 +];
121 +
122 +const babelToES5Plugins = [
123 + // These plugins transform DEV mode. Closure compiler deals with these in PROD.
124 + '@babel/plugin-transform-literals',
125 + '@babel/plugin-transform-arrow-functions',
126 + '@babel/plugin-transform-block-scoped-functions',
127 + '@babel/plugin-transform-shorthand-properties',
128 + '@babel/plugin-transform-computed-properties',
129 + ['@babel/plugin-transform-block-scoping', {throwIfClosureRequired: true}],
130 +];
131 +
132 +function getBabelConfig(
133 + updateBabelOptions,
134 + bundleType,
135 + packageName,
136 + externals,
137 + isDevelopment,
138 + bundle
139 +) {
140 + const canAccessReactObject =
141 + packageName === 'react' || externals.indexOf('react') !== -1;
142 + let options = {
143 + exclude: '/**/node_modules/**',
144 + babelrc: false,
145 + configFile: false,
146 + presets: [],
147 + plugins: [...babelPlugins],
148 + babelHelpers: 'bundled',
149 + sourcemap: false,
150 + };
151 + if (isDevelopment) {
152 + options.plugins.push(
153 + ...babelToES5Plugins,
154 + // Turn console.error/warn() into a custom wrapper
155 + [
156 + require('../babel/transform-replace-console-calls'),
157 + {
158 + shouldError: !canAccessReactObject,
159 + },
160 + ]
161 + );
162 + }
163 + if (updateBabelOptions) {
164 + options = updateBabelOptions(options);
165 + }
166 + // Controls whether to replace error messages with error codes in production.
167 + // By default, error messages are replaced in production.
168 + if (!isDevelopment && bundle.minifyWithProdErrorCodes !== false) {
169 + options.plugins.push(require('../error-codes/transform-error-messages'));
170 + }
171 +
172 + return options;
173 +}
174 +
175 +let getRollupInteropValue = id => {
176 + // We're setting Rollup to assume that imports are ES modules unless otherwise specified.
177 + // However, we also compile ES import syntax to `require()` using Babel.
178 + // This causes Rollup to turn uses of `import SomeDefaultImport from 'some-module' into
179 + // references to `SomeDefaultImport.default` due to CJS/ESM interop.
180 + // Some CJS modules don't have a `.default` export, and the rewritten import is incorrect.
181 + // Specifying `interop: 'default'` instead will have Rollup use the imported variable as-is,
182 + // without adding a `.default` to the reference.
183 + const modulesWithCommonJsExports = [
184 + 'art/core/transform',
185 + 'art/modes/current',
186 + 'art/modes/fast-noSideEffects',
187 + 'art/modes/svg',
188 + 'JSResourceReferenceImpl',
189 + 'error-stack-parser',
190 + 'neo-async',
191 + 'webpack/lib/dependencies/ModuleDependency',
192 + 'webpack/lib/dependencies/NullDependency',
193 + 'webpack/lib/Template',
194 + ];
195 +
196 + if (modulesWithCommonJsExports.includes(id)) {
197 + return 'default';
198 + }
199 +
200 + // For all other modules, handle imports without any import helper utils
201 + return 'esModule';
202 +};
203 +
204 +function getRollupOutputOptions(
205 + outputPath,
206 + format,
207 + globals,
208 + globalName,
209 + bundleType
210 +) {
211 + const isProduction = isProductionBundleType(bundleType);
212 +
213 + return {
214 + file: outputPath,
215 + format,
216 + globals,
217 + freeze: !isProduction,
218 + interop: getRollupInteropValue,
219 + name: globalName,
220 + sourcemap: false,
221 + esModule: false,
222 + exports: 'auto',
223 + };
224 +}
225 +
226 +function getFormat(bundleType) {
227 + switch (bundleType) {
228 + case NODE_ES2015:
229 + case NODE_DEV:
230 + case NODE_PROD:
231 + case NODE_PROFILING:
232 + case BUN_DEV:
233 + case BUN_PROD:
234 + case FB_WWW_DEV:
235 + case FB_WWW_PROD:
236 + case FB_WWW_PROFILING:
237 + case RN_OSS_DEV:
238 + case RN_OSS_PROD:
239 + case RN_OSS_PROFILING:
240 + case RN_FB_DEV:
241 + case RN_FB_PROD:
242 + case RN_FB_PROFILING:
243 + return `cjs`;
244 + case ESM_DEV:
245 + case ESM_PROD:
246 + return `es`;
247 + case BROWSER_SCRIPT:
248 + return `iife`;
249 + }
250 +}
251 +
252 +function isProductionBundleType(bundleType) {
253 + switch (bundleType) {
254 + case NODE_ES2015:
255 + return true;
256 + case ESM_DEV:
257 + case NODE_DEV:
258 + case BUN_DEV:
259 + case FB_WWW_DEV:
260 + case RN_OSS_DEV:
261 + case RN_FB_DEV:
262 + return false;
263 + case ESM_PROD:
264 + case NODE_PROD:
265 + case BUN_PROD:
266 + case NODE_PROFILING:
267 + case FB_WWW_PROD:
268 + case FB_WWW_PROFILING:
269 + case RN_OSS_PROD:
270 + case RN_OSS_PROFILING:
271 + case RN_FB_PROD:
272 + case RN_FB_PROFILING:
273 + case BROWSER_SCRIPT:
274 + return true;
275 + default:
276 + throw new Error(`Unknown type: ${bundleType}`);
277 + }
278 +}
279 +
280 +function isProfilingBundleType(bundleType) {
281 + switch (bundleType) {
282 + case NODE_ES2015:
283 + case FB_WWW_DEV:
284 + case FB_WWW_PROD:
285 + case NODE_DEV:
286 + case NODE_PROD:
287 + case BUN_DEV:
288 + case BUN_PROD:
289 + case RN_FB_DEV:
290 + case RN_FB_PROD:
291 + case RN_OSS_DEV:
292 + case RN_OSS_PROD:
293 + case ESM_DEV:
294 + case ESM_PROD:
295 + case BROWSER_SCRIPT:
296 + return false;
297 + case FB_WWW_PROFILING:
298 + case NODE_PROFILING:
299 + case RN_FB_PROFILING:
300 + case RN_OSS_PROFILING:
301 + return true;
302 + default:
303 + throw new Error(`Unknown type: ${bundleType}`);
304 + }
305 +}
306 +
307 +function getBundleTypeFlags(bundleType) {
308 + const isFBWWWBundle =
309 + bundleType === FB_WWW_DEV ||
310 + bundleType === FB_WWW_PROD ||
311 + bundleType === FB_WWW_PROFILING;
312 + const isRNBundle =
313 + bundleType === RN_OSS_DEV ||
314 + bundleType === RN_OSS_PROD ||
315 + bundleType === RN_OSS_PROFILING ||
316 + bundleType === RN_FB_DEV ||
317 + bundleType === RN_FB_PROD ||
318 + bundleType === RN_FB_PROFILING;
319 +
320 + const isFBRNBundle =
321 + bundleType === RN_FB_DEV ||
322 + bundleType === RN_FB_PROD ||
323 + bundleType === RN_FB_PROFILING;
324 +
325 + const shouldStayReadable = isFBWWWBundle || isRNBundle || forcePrettyOutput;
326 +
327 + return {
328 + isFBWWWBundle,
329 + isRNBundle,
330 + isFBRNBundle,
331 + shouldStayReadable,
332 + };
333 +}
334 +
335 +function forbidFBJSImports() {
336 + return {
337 + name: 'forbidFBJSImports',
338 + resolveId(importee, importer) {
339 + if (/^fbjs\//.test(importee)) {
340 + throw new Error(
341 + `Don't import ${importee} (found in ${importer}). ` +
342 + `Use the utilities in packages/shared/ instead.`
343 + );
344 + }
345 + },
346 + };
347 +}
348 +
349 +function getPlugins(
350 + entry,
351 + externals,
352 + updateBabelOptions,
353 + filename,
354 + packageName,
355 + bundleType,
356 + globalName,
357 + moduleType,
358 + pureExternalModules,
359 + bundle
360 +) {
361 + try {
362 + const forks = Modules.getForks(bundleType, entry, moduleType, bundle);
363 + const isProduction = isProductionBundleType(bundleType);
364 + const isProfiling = isProfilingBundleType(bundleType);
365 +
366 + const needsMinifiedByClosure =
367 + bundleType !== ESM_PROD && bundleType !== ESM_DEV;
368 +
369 + return [
370 + // Keep dynamic imports as externals
371 + dynamicImports(),
372 + {
373 + name: 'rollup-plugin-flow-remove-types',
374 + transform(code) {
375 + const transformed = flowRemoveTypes(code);
376 + return {
377 + code: transformed.toString(),
378 + map: null,
379 + };
380 + },
381 + },
382 + // Shim any modules that need forking in this environment.
383 + useForks(forks),
384 + // Ensure we don't try to bundle any fbjs modules.
385 + forbidFBJSImports(),
386 + // Use Node resolution mechanism.
387 + resolve({
388 + // skip: externals, // TODO: options.skip was removed in @rollup/plugin-node-resolve 3.0.0
389 + }),
390 + // Remove license headers from individual modules
391 + stripBanner({
392 + exclude: 'node_modules/**/*',
393 + }),
394 + // Compile to ES2015.
395 + babel(
396 + getBabelConfig(
397 + updateBabelOptions,
398 + bundleType,
399 + packageName,
400 + externals,
401 + !isProduction,
402 + bundle
403 + )
404 + ),
405 + // Remove 'use strict' from individual source files.
406 + {
407 + name: "remove 'use strict'",
408 + transform(source) {
409 + return source.replace(/['"]use strict["']/g, '');
410 + },
411 + },
412 + // Turn __DEV__ and process.env checks into constants.
413 + replace({
414 + preventAssignment: true,
415 + values: {
416 + __DEV__: isProduction ? 'false' : 'true',
417 + __PROFILE__: isProfiling || !isProduction ? 'true' : 'false',
418 + 'process.env.NODE_ENV': isProduction
419 + ? "'production'"
420 + : "'development'",
421 + __EXPERIMENTAL__,
422 + },
423 + }),
424 + {
425 + name: 'top-level-definitions',
426 + renderChunk(source) {
427 + return Wrappers.wrapWithTopLevelDefinitions(
428 + source,
429 + bundleType,
430 + globalName,
431 + filename,
432 + moduleType,
433 + bundle.wrapWithModuleBoundaries
434 + );
435 + },
436 + },
437 + // For production builds, compile with Closure. We do this even for the
438 + // "non-minified" production builds because Closure is much better at
439 + // minification than what most applications use. During this step, we do
440 + // preserve the original symbol names, though, so the resulting code is
441 + // relatively readable.
442 + //
443 + // For the minified builds, the names will be mangled later.
444 + //
445 + // We don't bother with sourcemaps at this step. The sourcemaps we publish
446 + // are only for whitespace and symbol renaming; they don't map back to
447 + // before Closure was applied.
448 + needsMinifiedByClosure &&
449 + closure({
450 + compilation_level: 'SIMPLE',
451 + language_in: 'ECMASCRIPT_2020',
452 + language_out:
453 + bundleType === NODE_ES2015
454 + ? 'ECMASCRIPT_2020'
455 + : bundleType === BROWSER_SCRIPT
456 + ? 'ECMASCRIPT5'
457 + : 'ECMASCRIPT5_STRICT',
458 + emit_use_strict:
459 + bundleType !== BROWSER_SCRIPT &&
460 + bundleType !== ESM_PROD &&
461 + bundleType !== ESM_DEV,
462 + env: 'CUSTOM',
463 + warning_level: 'QUIET',
464 + source_map_include_content: true,
465 + use_types_for_optimization: false,
466 + process_common_js_modules: false,
467 + rewrite_polyfills: false,
468 + inject_libraries: false,
469 + allow_dynamic_import: true,
470 +
471 + // Don't let it create global variables in the browser.
472 + // https://github.com/facebook/react/issues/10909
473 + assume_function_wrapper: true,
474 +
475 + // Don't rename symbols (variable names, functions, etc). We leave
476 + // this up to the application to handle, if they want. Otherwise gzip
477 + // takes care of it.
478 + renaming: false,
479 + }),
480 + needsMinifiedByClosure &&
481 + // Add the whitespace back
482 + prettier({
483 + parser: 'flow',
484 + singleQuote: false,
485 + trailingComma: 'none',
486 + bracketSpacing: true,
487 + }),
488 + {
489 + name: 'license-and-signature-header',
490 + renderChunk(source) {
491 + return Wrappers.wrapWithLicenseHeader(
492 + source,
493 + bundleType,
494 + globalName,
495 + filename,
496 + moduleType
497 + );
498 + },
499 + },
500 + // Record bundle size.
501 + sizes({
502 + getSize: (size, gzip) => {
503 + const currentSizes = Stats.currentBuildResults.bundleSizes;
504 + const recordIndex = currentSizes.findIndex(
505 + record =>
506 + record.filename === filename && record.bundleType === bundleType
507 + );
508 + const index = recordIndex !== -1 ? recordIndex : currentSizes.length;
509 + currentSizes[index] = {
510 + filename,
511 + bundleType,
512 + packageName,
513 + size,
514 + gzip,
515 + };
516 + },
517 + }),
518 + ].filter(Boolean);
519 + } catch (error) {
520 + console.error(
521 + chalk.red(`There was an error preparing plugins for entry "${entry}"`)
522 + );
523 + throw error;
524 + }
525 +}
526 +
527 +function shouldSkipBundle(bundle, bundleType) {
528 + const shouldSkipBundleType = bundle.bundleTypes.indexOf(bundleType) === -1;
529 + if (shouldSkipBundleType) {
530 + return true;
531 + }
532 + if (requestedBundleTypes.length > 0) {
533 + const isAskingForDifferentType = requestedBundleTypes.some(
534 + requestedType => !bundleType.includes(requestedType)
535 + );
536 + if (isAskingForDifferentType) {
537 + return true;
538 + }
539 + }
540 + if (requestedBundleNames.length > 0) {
541 + // If the name ends with `something/index` we only match if the
542 + // entry ends in something. Such as `react-dom/index` only matches
543 + // `react-dom` but not `react-dom/server`. Everything else is fuzzy
544 + // search.
545 + const entryLowerCase = bundle.entry.toLowerCase() + '/index.js';
546 + const isAskingForDifferentNames = requestedBundleNames.every(
547 + requestedName => {
548 + const matchEntry = entryLowerCase.indexOf(requestedName) !== -1;
549 + if (!bundle.name) {
550 + return !matchEntry;
551 + }
552 + const matchName =
553 + bundle.name.toLowerCase().indexOf(requestedName) !== -1;
554 + return !matchEntry && !matchName;
555 + }
556 + );
557 + if (isAskingForDifferentNames) {
558 + return true;
559 + }
560 + }
561 + return false;
562 +}
563 +
564 +function resolveEntryFork(resolvedEntry, isFBBundle) {
565 + // Pick which entry point fork to use:
566 + // .modern.fb.js
567 + // .classic.fb.js
568 + // .fb.js
569 + // .stable.js
570 + // .experimental.js
571 + // .js
572 + // or any of those plus .development.js
573 +
574 + if (isFBBundle) {
575 + const resolvedFBEntry = resolvedEntry.replace(
576 + '.js',
577 + __EXPERIMENTAL__ ? '.modern.fb.js' : '.classic.fb.js'
578 + );
579 + const developmentFBEntry = resolvedFBEntry.replace(
580 + '.js',
581 + '.development.js'
582 + );
583 + if (fs.existsSync(developmentFBEntry)) {
584 + return developmentFBEntry;
585 + }
586 + if (fs.existsSync(resolvedFBEntry)) {
587 + return resolvedFBEntry;
588 + }
589 + const resolvedGenericFBEntry = resolvedEntry.replace('.js', '.fb.js');
590 + const developmentGenericFBEntry = resolvedGenericFBEntry.replace(
591 + '.js',
592 + '.development.js'
593 + );
594 + if (fs.existsSync(developmentGenericFBEntry)) {
595 + return developmentGenericFBEntry;
596 + }
597 + if (fs.existsSync(resolvedGenericFBEntry)) {
598 + return resolvedGenericFBEntry;
599 + }
600 + // Even if it's a FB bundle we fallthrough to pick stable or experimental if we don't have an FB fork.
601 + }
602 + const resolvedForkedEntry = resolvedEntry.replace(
603 + '.js',
604 + __EXPERIMENTAL__ ? '.experimental.js' : '.stable.js'
605 + );
606 + const devForkedEntry = resolvedForkedEntry.replace('.js', '.development.js');
607 + if (fs.existsSync(devForkedEntry)) {
608 + return devForkedEntry;
609 + }
610 + if (fs.existsSync(resolvedForkedEntry)) {
611 + return resolvedForkedEntry;
612 + }
613 + // Just use the plain .js one.
614 + return resolvedEntry;
615 +}
616 +
617 +async function createBundle(bundle, bundleType) {
618 + const filename = getFilename(bundle, bundleType);
619 + const logKey =
620 + chalk.white.bold(filename) + chalk.dim(` (${bundleType.toLowerCase()})`);
621 + const format = getFormat(bundleType);
622 + const packageName = Packaging.getPackageName(bundle.entry);
623 +
624 + const {isFBWWWBundle, isFBRNBundle} = getBundleTypeFlags(bundleType);
625 +
626 + let resolvedEntry = resolveEntryFork(
627 + require.resolve(bundle.entry),
628 + isFBWWWBundle || isFBRNBundle,
629 + !isProductionBundleType(bundleType)
630 + );
631 +
632 + const peerGlobals = Modules.getPeerGlobals(bundle.externals, bundleType);
633 + let externals = Object.keys(peerGlobals);
634 +
635 + const deps = Modules.getDependencies(bundleType, bundle.entry);
636 + externals = externals.concat(deps);
637 +
638 + const importSideEffects = Modules.getImportSideEffects();
639 + const pureExternalModules = Object.keys(importSideEffects).filter(
640 + module => !importSideEffects[module]
641 + );
642 +
643 + const rollupConfig = {
644 + input: resolvedEntry,
645 + treeshake: {
646 + moduleSideEffects: (id, external) =>
647 + !(external && pureExternalModules.includes(id)),
648 + propertyReadSideEffects: false,
649 + },
650 + external(id) {
651 + const containsThisModule = pkg => id === pkg || id.startsWith(pkg + '/');
652 + const isProvidedByDependency = externals.some(containsThisModule);
653 + if (isProvidedByDependency) {
654 + if (id.indexOf('/src/') !== -1) {
655 + throw Error(
656 + 'You are trying to import ' +
657 + id +
658 + ' but ' +
659 + externals.find(containsThisModule) +
660 + ' is one of npm dependencies, ' +
661 + 'so it will not contain that source file. You probably want ' +
662 + 'to create a new bundle entry point for it instead.'
663 + );
664 + }
665 + return true;
666 + }
667 + return !!peerGlobals[id];
668 + },
669 + onwarn: handleRollupWarning,
670 + plugins: getPlugins(
671 + bundle.entry,
672 + externals,
673 + bundle.babel,
674 + filename,
675 + packageName,
676 + bundleType,
677 + bundle.global,
678 + bundle.moduleType,
679 + pureExternalModules,
680 + bundle
681 + ),
682 + output: {
683 + externalLiveBindings: false,
684 + freeze: false,
685 + interop: getRollupInteropValue,
686 + esModule: false,
687 + },
688 + };
689 + const mainOutputPath = Packaging.getBundleOutputPath(
690 + bundle,
691 + bundleType,
692 + filename,
693 + packageName
694 + );
695 +
696 + const rollupOutputOptions = getRollupOutputOptions(
697 + mainOutputPath,
698 + format,
699 + peerGlobals,
700 + bundle.global,
701 + bundleType
702 + );
703 +
704 + if (isWatchMode) {
705 + rollupConfig.output = [rollupOutputOptions];
706 + const watcher = rollup.watch(rollupConfig);
707 + watcher.on('event', async event => {
708 + switch (event.code) {
709 + case 'BUNDLE_START':
710 + console.log(`${chalk.bgYellow.black(' BUILDING ')} ${logKey}`);
711 + break;
712 + case 'BUNDLE_END':
713 + console.log(`${chalk.bgGreen.black(' COMPLETE ')} ${logKey}\n`);
714 + break;
715 + case 'ERROR':
716 + case 'FATAL':
717 + console.log(`${chalk.bgRed.black(' OH NOES! ')} ${logKey}\n`);
718 + handleRollupError(event.error);
719 + break;
720 + }
721 + });
722 + } else {
723 + console.log(`${chalk.bgYellow.black(' BUILDING ')} ${logKey}`);
724 + try {
725 + const result = await rollup.rollup(rollupConfig);
726 + await result.write(rollupOutputOptions);
727 + } catch (error) {
728 + console.log(`${chalk.bgRed.black(' OH NOES! ')} ${logKey}\n`);
729 + handleRollupError(error);
730 + throw error;
731 + }
732 + console.log(`${chalk.bgGreen.black(' COMPLETE ')} ${logKey}\n`);
733 + }
734 +}
735 +
736 +function handleRollupWarning(warning) {
737 + if (warning.code === 'UNUSED_EXTERNAL_IMPORT') {
738 + const match = warning.message.match(/external module "([^"]+)"/);
739 + if (!match || typeof match[1] !== 'string') {
740 + throw new Error(
741 + 'Could not parse a Rollup warning. ' + 'Fix this method.'
742 + );
743 + }
744 + const importSideEffects = Modules.getImportSideEffects();
745 + const externalModule = match[1];
746 + if (typeof importSideEffects[externalModule] !== 'boolean') {
747 + throw new Error(
748 + 'An external module "' +
749 + externalModule +
750 + '" is used in a DEV-only code path ' +
751 + 'but we do not know if it is safe to omit an unused require() to it in production. ' +
752 + 'Please add it to the `importSideEffects` list in `scripts/rollup/modules.js`.'
753 + );
754 + }
755 + // Don't warn. We will remove side effectless require() in a later pass.
756 + return;
757 + }
758 +
759 + if (warning.code === 'CIRCULAR_DEPENDENCY') {
760 + // Ignored
761 + } else if (typeof warning.code === 'string') {
762 + // This is a warning coming from Rollup itself.
763 + // These tend to be important (e.g. clashes in namespaced exports)
764 + // so we'll fail the build on any of them.
765 + console.error();
766 + console.error(warning.message || warning);
767 + console.error();
768 + process.exit(1);
769 + } else {
770 + // The warning is from one of the plugins.
771 + // Maybe it's not important, so just print it.
772 + console.warn(warning.message || warning);
773 + }
774 +}
775 +
776 +function handleRollupError(error) {
777 + loggedErrors.add(error);
778 + if (!error.code) {
779 + console.error(error);
780 + return;
781 + }
782 + console.error(
783 + `\x1b[31m-- ${error.code}${error.plugin ? ` (${error.plugin})` : ''} --`
784 + );
785 + console.error(error.stack);
786 + if (error.loc && error.loc.file) {
787 + const {file, line, column} = error.loc;
788 + // This looks like an error from Rollup, e.g. missing export.
789 + // We'll use the accurate line numbers provided by Rollup but
790 + // use Babel code frame because it looks nicer.
791 + const rawLines = fs.readFileSync(file, 'utf-8');
792 + // column + 1 is required due to rollup counting column start position from 0
793 + // whereas babel-code-frame counts from 1
794 + const frame = codeFrame(rawLines, line, column + 1, {
795 + highlightCode: true,
796 + });
797 + console.error(frame);
798 + } else if (error.codeFrame) {
799 + // This looks like an error from a plugin (e.g. Babel).
800 + // In this case we'll resort to displaying the provided code frame
801 + // because we can't be sure the reported location is accurate.
802 + console.error(error.codeFrame);
803 + }
804 +}
805 +
806 +async function buildEverything(index, total) {
807 + if (!argv['unsafe-partial']) {
808 + await asyncRimRaf('build');
809 + }
810 +
811 + // Run them serially for better console output
812 + // and to avoid any potential race conditions.
813 +
814 + let bundles = [];
815 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
816 + for (const bundle of Bundles.bundles) {
817 + bundles.push(
818 + [bundle, NODE_ES2015],
819 + [bundle, ESM_DEV],
820 + [bundle, ESM_PROD],
821 + [bundle, NODE_DEV],
822 + [bundle, NODE_PROD],
823 + [bundle, NODE_PROFILING],
824 + [bundle, BUN_DEV],
825 + [bundle, BUN_PROD],
826 + [bundle, FB_WWW_DEV],
827 + [bundle, FB_WWW_PROD],
828 + [bundle, FB_WWW_PROFILING],
829 + [bundle, RN_OSS_DEV],
830 + [bundle, RN_OSS_PROD],
831 + [bundle, RN_OSS_PROFILING],
832 + [bundle, RN_FB_DEV],
833 + [bundle, RN_FB_PROD],
834 + [bundle, RN_FB_PROFILING],
835 + [bundle, BROWSER_SCRIPT]
836 + );
837 + }
838 +
839 + bundles = bundles.filter(([bundle, bundleType]) => {
840 + return !shouldSkipBundle(bundle, bundleType);
841 + });
842 +
843 + const nodeTotal = parseInt(total, 10);
844 + const nodeIndex = parseInt(index, 10);
845 + bundles = bundles.filter((_, i) => i % nodeTotal === nodeIndex);
846 +
847 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
848 + for (const [bundle, bundleType] of bundles) {
849 + await createBundle(bundle, bundleType);
850 + }
851 +
852 + await Packaging.copyAllShims();
853 + await Packaging.prepareNpmPackages();
854 +
855 + if (syncFBSourcePath) {
856 + await Sync.syncReactNative(syncFBSourcePath);
857 + } else if (syncWWWPath) {
858 + await Sync.syncReactDom('build/facebook-www', syncWWWPath);
859 + }
860 +
861 + console.log(Stats.printResults());
862 + if (!forcePrettyOutput) {
863 + Stats.saveResults();
864 + }
865 +}
866 +
867 +module.exports = {
868 + buildEverything,
869 +};
scripts/rollup/stats.js
+6
@@ -28,6 +28,12 @@ function saveResults() {
28 join('build', 'sizes', `bundle-sizes-${nodeIndex}.json`),
29 JSON.stringify(currentBuildResults, null, 2)
30 );
31 + } else if (process.env.CI === 'github') {
32 + mkdirp.sync('build/sizes');
33 + fs.writeFileSync(
34 + join('build', 'sizes', `bundle-sizes-${process.env.NODE_INDEX}.json`),
35 + JSON.stringify(currentBuildResults, null, 2)
36 + );
37 } else {
38 // Write all the bundle sizes to a single JSON file.
39 fs.writeFileSync(