1. 'use strict';
    
  2. 
    
  3. /* eslint-disable no-for-of-loops/no-for-of-loops */
    
  4. 
    
  5. const crypto = require('node:crypto');
    
  6. const fs = require('fs');
    
  7. const fse = require('fs-extra');
    
  8. const {spawnSync} = require('child_process');
    
  9. const path = require('path');
    
  10. const tmp = require('tmp');
    
  11. 
    
  12. const {
    
  13.   ReactVersion,
    
  14.   stablePackages,
    
  15.   experimentalPackages,
    
  16.   canaryChannelLabel,
    
  17. } = require('../../ReactVersions');
    
  18. 
    
  19. // Runs the build script for both stable and experimental release channels,
    
  20. // by configuring an environment variable.
    
  21. 
    
  22. const sha = String(
    
  23.   spawnSync('git', ['show', '-s', '--no-show-signature', '--format=%h']).stdout
    
  24. ).trim();
    
  25. 
    
  26. let dateString = String(
    
  27.   spawnSync('git', [
    
  28.     'show',
    
  29.     '-s',
    
  30.     '--no-show-signature',
    
  31.     '--format=%cd',
    
  32.     '--date=format:%Y%m%d',
    
  33.     sha,
    
  34.   ]).stdout
    
  35. ).trim();
    
  36. 
    
  37. // On CI environment, this string is wrapped with quotes '...'s
    
  38. if (dateString.startsWith("'")) {
    
  39.   dateString = dateString.slice(1, 9);
    
  40. }
    
  41. 
    
  42. // Build the artifacts using a placeholder React version. We'll then do a string
    
  43. // replace to swap it with the correct version per release channel.
    
  44. const PLACEHOLDER_REACT_VERSION = ReactVersion + '-PLACEHOLDER';
    
  45. 
    
  46. // TODO: We should inject the React version using a build-time parameter
    
  47. // instead of overwriting the source files.
    
  48. fs.writeFileSync(
    
  49.   './packages/shared/ReactVersion.js',
    
  50.   `export default '${PLACEHOLDER_REACT_VERSION}';\n`
    
  51. );
    
  52. 
    
  53. if (process.env.CIRCLE_NODE_TOTAL) {
    
  54.   // In CI, we use multiple concurrent processes. Allocate half the processes to
    
  55.   // build the stable channel, and the other half for experimental. Override
    
  56.   // the environment variables to "trick" the underlying build script.
    
  57.   const total = parseInt(process.env.CIRCLE_NODE_TOTAL, 10);
    
  58.   const halfTotal = Math.floor(total / 2);
    
  59.   const index = parseInt(process.env.CIRCLE_NODE_INDEX, 10);
    
  60.   if (index < halfTotal) {
    
  61.     const nodeTotal = halfTotal;
    
  62.     const nodeIndex = index;
    
  63.     buildForChannel('stable', nodeTotal, nodeIndex);
    
  64.     processStable('./build');
    
  65.   } else {
    
  66.     const nodeTotal = total - halfTotal;
    
  67.     const nodeIndex = index - halfTotal;
    
  68.     buildForChannel('experimental', nodeTotal, nodeIndex);
    
  69.     processExperimental('./build');
    
  70.   }
    
  71. } else {
    
  72.   // Running locally, no concurrency. Move each channel's build artifacts into
    
  73.   // a temporary directory so that they don't conflict.
    
  74.   buildForChannel('stable', '', '');
    
  75.   const stableDir = tmp.dirSync().name;
    
  76.   crossDeviceRenameSync('./build', stableDir);
    
  77.   processStable(stableDir);
    
  78.   buildForChannel('experimental', '', '');
    
  79.   const experimentalDir = tmp.dirSync().name;
    
  80.   crossDeviceRenameSync('./build', experimentalDir);
    
  81.   processExperimental(experimentalDir);
    
  82. 
    
  83.   // Then merge the experimental folder into the stable one. processExperimental
    
  84.   // will have already removed conflicting files.
    
  85.   //
    
  86.   // In CI, merging is handled automatically by CircleCI's workspace feature.
    
  87.   mergeDirsSync(experimentalDir + '/', stableDir + '/');
    
  88. 
    
  89.   // Now restore the combined directory back to its original name
    
  90.   crossDeviceRenameSync(stableDir, './build');
    
  91. }
    
  92. 
    
  93. function buildForChannel(channel, nodeTotal, nodeIndex) {
    
  94.   const {status} = spawnSync(
    
  95.     'node',
    
  96.     ['./scripts/rollup/build.js', ...process.argv.slice(2)],
    
  97.     {
    
  98.       stdio: ['pipe', process.stdout, process.stderr],
    
  99.       env: {
    
  100.         ...process.env,
    
  101.         RELEASE_CHANNEL: channel,
    
  102.         CIRCLE_NODE_TOTAL: nodeTotal,
    
  103.         CIRCLE_NODE_INDEX: nodeIndex,
    
  104.       },
    
  105.     }
    
  106.   );
    
  107. 
    
  108.   if (status !== 0) {
    
  109.     // Error of spawned process is already piped to this stderr
    
  110.     process.exit(status);
    
  111.   }
    
  112. }
    
  113. 
    
  114. function processStable(buildDir) {
    
  115.   if (fs.existsSync(buildDir + '/node_modules')) {
    
  116.     // Identical to `oss-stable` but with real, semver versions. This is what
    
  117.     // will get published to @latest.
    
  118.     spawnSync('cp', [
    
  119.       '-r',
    
  120.       buildDir + '/node_modules',
    
  121.       buildDir + '/oss-stable-semver',
    
  122.     ]);
    
  123. 
    
  124.     const defaultVersionIfNotFound = '0.0.0' + '-' + sha + '-' + dateString;
    
  125.     const versionsMap = new Map();
    
  126.     for (const moduleName in stablePackages) {
    
  127.       const version = stablePackages[moduleName];
    
  128.       versionsMap.set(
    
  129.         moduleName,
    
  130.         version + '-' + canaryChannelLabel + '-' + sha + '-' + dateString,
    
  131.         defaultVersionIfNotFound
    
  132.       );
    
  133.     }
    
  134.     updatePackageVersions(
    
  135.       buildDir + '/node_modules',
    
  136.       versionsMap,
    
  137.       defaultVersionIfNotFound,
    
  138.       true
    
  139.     );
    
  140.     fs.renameSync(buildDir + '/node_modules', buildDir + '/oss-stable');
    
  141.     updatePlaceholderReactVersionInCompiledArtifacts(
    
  142.       buildDir + '/oss-stable',
    
  143.       ReactVersion + '-' + canaryChannelLabel + '-' + sha + '-' + dateString
    
  144.     );
    
  145. 
    
  146.     // Now do the semver ones
    
  147.     const semverVersionsMap = new Map();
    
  148.     for (const moduleName in stablePackages) {
    
  149.       const version = stablePackages[moduleName];
    
  150.       semverVersionsMap.set(moduleName, version);
    
  151.     }
    
  152.     updatePackageVersions(
    
  153.       buildDir + '/oss-stable-semver',
    
  154.       semverVersionsMap,
    
  155.       defaultVersionIfNotFound,
    
  156.       false
    
  157.     );
    
  158.     updatePlaceholderReactVersionInCompiledArtifacts(
    
  159.       buildDir + '/oss-stable-semver',
    
  160.       ReactVersion
    
  161.     );
    
  162.   }
    
  163. 
    
  164.   if (fs.existsSync(buildDir + '/facebook-www')) {
    
  165.     const hash = crypto.createHash('sha1');
    
  166.     for (const fileName of fs.readdirSync(buildDir + '/facebook-www').sort()) {
    
  167.       const filePath = buildDir + '/facebook-www/' + fileName;
    
  168.       const stats = fs.statSync(filePath);
    
  169.       if (!stats.isDirectory()) {
    
  170.         hash.update(fs.readFileSync(filePath));
    
  171.         fs.renameSync(filePath, filePath.replace('.js', '.classic.js'));
    
  172.       }
    
  173.     }
    
  174.     updatePlaceholderReactVersionInCompiledArtifacts(
    
  175.       buildDir + '/facebook-www',
    
  176.       ReactVersion + '-www-classic-' + hash.digest('hex').slice(0, 8)
    
  177.     );
    
  178.   }
    
  179. 
    
  180.   const reactNativeBuildDir = buildDir + '/react-native/implementations/';
    
  181.   if (fs.existsSync(reactNativeBuildDir)) {
    
  182.     const hash = crypto.createHash('sha1');
    
  183.     for (const fileName of fs.readdirSync(reactNativeBuildDir).sort()) {
    
  184.       const filePath = reactNativeBuildDir + fileName;
    
  185.       const stats = fs.statSync(filePath);
    
  186.       if (!stats.isDirectory()) {
    
  187.         hash.update(fs.readFileSync(filePath));
    
  188.       }
    
  189.     }
    
  190.     updatePlaceholderReactVersionInCompiledArtifacts(
    
  191.       reactNativeBuildDir,
    
  192.       ReactVersion +
    
  193.         '-' +
    
  194.         canaryChannelLabel +
    
  195.         '-' +
    
  196.         hash.digest('hex').slice(0, 8)
    
  197.     );
    
  198.   }
    
  199. 
    
  200.   // Update remaining placeholders with canary channel version
    
  201.   updatePlaceholderReactVersionInCompiledArtifacts(
    
  202.     buildDir,
    
  203.     ReactVersion + '-' + canaryChannelLabel + '-' + sha + '-' + dateString
    
  204.   );
    
  205. 
    
  206.   if (fs.existsSync(buildDir + '/sizes')) {
    
  207.     fs.renameSync(buildDir + '/sizes', buildDir + '/sizes-stable');
    
  208.   }
    
  209. }
    
  210. 
    
  211. function processExperimental(buildDir, version) {
    
  212.   if (fs.existsSync(buildDir + '/node_modules')) {
    
  213.     const defaultVersionIfNotFound =
    
  214.       '0.0.0' + '-experimental-' + sha + '-' + dateString;
    
  215.     const versionsMap = new Map();
    
  216.     for (const moduleName in stablePackages) {
    
  217.       versionsMap.set(moduleName, defaultVersionIfNotFound);
    
  218.     }
    
  219.     for (const moduleName of experimentalPackages) {
    
  220.       versionsMap.set(moduleName, defaultVersionIfNotFound);
    
  221.     }
    
  222.     updatePackageVersions(
    
  223.       buildDir + '/node_modules',
    
  224.       versionsMap,
    
  225.       defaultVersionIfNotFound,
    
  226.       true
    
  227.     );
    
  228.     fs.renameSync(buildDir + '/node_modules', buildDir + '/oss-experimental');
    
  229.     updatePlaceholderReactVersionInCompiledArtifacts(
    
  230.       buildDir + '/oss-experimental',
    
  231.       // TODO: The npm version for experimental releases does not include the
    
  232.       // React version, but the runtime version does so that DevTools can do
    
  233.       // feature detection. Decide what to do about this later.
    
  234.       ReactVersion + '-experimental-' + sha + '-' + dateString
    
  235.     );
    
  236.   }
    
  237. 
    
  238.   if (fs.existsSync(buildDir + '/facebook-www')) {
    
  239.     const hash = crypto.createHash('sha1');
    
  240.     for (const fileName of fs.readdirSync(buildDir + '/facebook-www').sort()) {
    
  241.       const filePath = buildDir + '/facebook-www/' + fileName;
    
  242.       const stats = fs.statSync(filePath);
    
  243.       if (!stats.isDirectory()) {
    
  244.         hash.update(fs.readFileSync(filePath));
    
  245.         fs.renameSync(filePath, filePath.replace('.js', '.modern.js'));
    
  246.       }
    
  247.     }
    
  248.     updatePlaceholderReactVersionInCompiledArtifacts(
    
  249.       buildDir + '/facebook-www',
    
  250.       ReactVersion + '-www-modern-' + hash.digest('hex').slice(0, 8)
    
  251.     );
    
  252.   }
    
  253. 
    
  254.   // Update remaining placeholders with canary channel version
    
  255.   updatePlaceholderReactVersionInCompiledArtifacts(
    
  256.     buildDir,
    
  257.     ReactVersion + '-' + canaryChannelLabel + '-' + sha + '-' + dateString
    
  258.   );
    
  259. 
    
  260.   if (fs.existsSync(buildDir + '/sizes')) {
    
  261.     fs.renameSync(buildDir + '/sizes', buildDir + '/sizes-experimental');
    
  262.   }
    
  263. 
    
  264.   // Delete all other artifacts that weren't handled above. We assume they are
    
  265.   // duplicates of the corresponding artifacts in the stable channel. Ideally,
    
  266.   // the underlying build script should not have produced these files in the
    
  267.   // first place.
    
  268.   for (const pathName of fs.readdirSync(buildDir)) {
    
  269.     if (
    
  270.       pathName !== 'oss-experimental' &&
    
  271.       pathName !== 'facebook-www' &&
    
  272.       pathName !== 'sizes-experimental'
    
  273.     ) {
    
  274.       spawnSync('rm', ['-rm', buildDir + '/' + pathName]);
    
  275.     }
    
  276.   }
    
  277. }
    
  278. 
    
  279. function crossDeviceRenameSync(source, destination) {
    
  280.   return fse.moveSync(source, destination, {overwrite: true});
    
  281. }
    
  282. 
    
  283. /*
    
  284.  * Grabs the built packages in ${tmp_build_dir}/node_modules and updates the
    
  285.  * `version` key in their package.json to 0.0.0-${date}-${commitHash} for the commit
    
  286.  * you're building. Also updates the dependencies and peerDependencies
    
  287.  * to match this version for all of the 'React' packages
    
  288.  * (packages available in this repo).
    
  289.  */
    
  290. function updatePackageVersions(
    
  291.   modulesDir,
    
  292.   versionsMap,
    
  293.   defaultVersionIfNotFound,
    
  294.   pinToExactVersion
    
  295. ) {
    
  296.   for (const moduleName of fs.readdirSync(modulesDir)) {
    
  297.     let version = versionsMap.get(moduleName);
    
  298.     if (version === undefined) {
    
  299.       // TODO: If the module is not in the version map, we should exclude it
    
  300.       // from the build artifacts.
    
  301.       version = defaultVersionIfNotFound;
    
  302.     }
    
  303.     const packageJSONPath = path.join(modulesDir, moduleName, 'package.json');
    
  304.     const stats = fs.statSync(packageJSONPath);
    
  305.     if (stats.isFile()) {
    
  306.       const packageInfo = JSON.parse(fs.readFileSync(packageJSONPath));
    
  307. 
    
  308.       // Update version
    
  309.       packageInfo.version = version;
    
  310. 
    
  311.       if (packageInfo.dependencies) {
    
  312.         for (const dep of Object.keys(packageInfo.dependencies)) {
    
  313.           const depVersion = versionsMap.get(dep);
    
  314.           if (depVersion !== undefined) {
    
  315.             packageInfo.dependencies[dep] = pinToExactVersion
    
  316.               ? depVersion
    
  317.               : '^' + depVersion;
    
  318.           }
    
  319.         }
    
  320.       }
    
  321.       if (packageInfo.peerDependencies) {
    
  322.         if (
    
  323.           !pinToExactVersion &&
    
  324.           (moduleName === 'use-sync-external-store' ||
    
  325.             moduleName === 'use-subscription')
    
  326.         ) {
    
  327.           // use-sync-external-store supports older versions of React, too, so
    
  328.           // we don't override to the latest version. We should figure out some
    
  329.           // better way to handle this.
    
  330.           // TODO: Remove this special case.
    
  331.         } else {
    
  332.           for (const dep of Object.keys(packageInfo.peerDependencies)) {
    
  333.             const depVersion = versionsMap.get(dep);
    
  334.             if (depVersion !== undefined) {
    
  335.               packageInfo.peerDependencies[dep] = pinToExactVersion
    
  336.                 ? depVersion
    
  337.                 : '^' + depVersion;
    
  338.             }
    
  339.           }
    
  340.         }
    
  341.       }
    
  342. 
    
  343.       // Write out updated package.json
    
  344.       fs.writeFileSync(packageJSONPath, JSON.stringify(packageInfo, null, 2));
    
  345.     }
    
  346.   }
    
  347. }
    
  348. 
    
  349. function updatePlaceholderReactVersionInCompiledArtifacts(
    
  350.   artifactsDirectory,
    
  351.   newVersion
    
  352. ) {
    
  353.   // Update the version of React in the compiled artifacts by searching for
    
  354.   // the placeholder string and replacing it with a new one.
    
  355.   const artifactFilenames = String(
    
  356.     spawnSync('grep', [
    
  357.       '-lr',
    
  358.       PLACEHOLDER_REACT_VERSION,
    
  359.       '--',
    
  360.       artifactsDirectory,
    
  361.     ]).stdout
    
  362.   )
    
  363.     .trim()
    
  364.     .split('\n')
    
  365.     .filter(filename => filename.endsWith('.js'));
    
  366. 
    
  367.   for (const artifactFilename of artifactFilenames) {
    
  368.     const originalText = fs.readFileSync(artifactFilename, 'utf8');
    
  369.     const replacedText = originalText.replaceAll(
    
  370.       PLACEHOLDER_REACT_VERSION,
    
  371.       newVersion
    
  372.     );
    
  373.     fs.writeFileSync(artifactFilename, replacedText);
    
  374.   }
    
  375. }
    
  376. 
    
  377. /**
    
  378.  * cross-platform alternative to `rsync -ar`
    
  379.  * @param {string} source
    
  380.  * @param {string} destination
    
  381.  */
    
  382. function mergeDirsSync(source, destination) {
    
  383.   for (const sourceFileBaseName of fs.readdirSync(source)) {
    
  384.     const sourceFileName = path.join(source, sourceFileBaseName);
    
  385.     const targetFileName = path.join(destination, sourceFileBaseName);
    
  386. 
    
  387.     const sourceFile = fs.statSync(sourceFileName);
    
  388.     if (sourceFile.isDirectory()) {
    
  389.       fse.ensureDirSync(targetFileName);
    
  390.       mergeDirsSync(sourceFileName, targetFileName);
    
  391.     } else {
    
  392.       fs.copyFileSync(sourceFileName, targetFileName);
    
  393.     }
    
  394.   }
    
  395. }