[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(