1. 'use strict';
    
  2. 
    
  3. const {exec} = require('child-process-promise');
    
  4. const {createPatch} = require('diff');
    
  5. const {hashElement} = require('folder-hash');
    
  6. const {existsSync, readFileSync, writeFileSync} = require('fs');
    
  7. const {readJson, writeJson} = require('fs-extra');
    
  8. const fetch = require('node-fetch');
    
  9. const logUpdate = require('log-update');
    
  10. const {join} = require('path');
    
  11. const createLogger = require('progress-estimator');
    
  12. const prompt = require('prompt-promise');
    
  13. const theme = require('./theme');
    
  14. const {stablePackages, experimentalPackages} = require('../../ReactVersions');
    
  15. 
    
  16. // https://www.npmjs.com/package/progress-estimator#configuration
    
  17. const logger = createLogger({
    
  18.   storagePath: join(__dirname, '.progress-estimator'),
    
  19. });
    
  20. 
    
  21. const addDefaultParamValue = (optionalShortName, longName, defaultValue) => {
    
  22.   let found = false;
    
  23.   for (let i = 0; i < process.argv.length; i++) {
    
  24.     const current = process.argv[i];
    
  25.     if (current === optionalShortName || current.startsWith(`${longName}=`)) {
    
  26.       found = true;
    
  27.       break;
    
  28.     }
    
  29.   }
    
  30. 
    
  31.   if (!found) {
    
  32.     process.argv.push(`${longName}=${defaultValue}`);
    
  33.   }
    
  34. };
    
  35. 
    
  36. const confirm = async message => {
    
  37.   const confirmation = await prompt(theme`\n{caution ${message}} (y/N) `);
    
  38.   prompt.done();
    
  39.   if (confirmation !== 'y' && confirmation !== 'Y') {
    
  40.     console.log(theme`\n{caution Release cancelled.}`);
    
  41.     process.exit(0);
    
  42.   }
    
  43. };
    
  44. 
    
  45. const execRead = async (command, options) => {
    
  46.   const {stdout} = await exec(command, options);
    
  47. 
    
  48.   return stdout.trim();
    
  49. };
    
  50. 
    
  51. const extractCommitFromVersionNumber = version => {
    
  52.   // Support stable version format e.g. "0.0.0-0e526bcec-20210202"
    
  53.   // and experimental version format e.g. "0.0.0-experimental-0e526bcec-20210202"
    
  54.   const match = version.match(/0\.0\.0\-([a-z]+\-){0,1}([^-]+).+/);
    
  55.   if (match === null) {
    
  56.     throw Error(`Could not extra commit from version "${version}"`);
    
  57.   }
    
  58.   return match[2];
    
  59. };
    
  60. 
    
  61. const getArtifactsList = async buildID => {
    
  62.   const headers = {};
    
  63.   const {CIRCLE_CI_API_TOKEN} = process.env;
    
  64.   if (CIRCLE_CI_API_TOKEN != null) {
    
  65.     headers['Circle-Token'] = CIRCLE_CI_API_TOKEN;
    
  66.   }
    
  67.   const jobArtifactsURL = `https://circleci.com/api/v1.1/project/github/facebook/react/${buildID}/artifacts`;
    
  68.   const jobArtifacts = await fetch(jobArtifactsURL, {headers});
    
  69.   return jobArtifacts.json();
    
  70. };
    
  71. 
    
  72. const getBuildInfo = async () => {
    
  73.   const cwd = join(__dirname, '..', '..');
    
  74. 
    
  75.   const isExperimental = process.env.RELEASE_CHANNEL === 'experimental';
    
  76. 
    
  77.   const branch = await execRead('git branch | grep \\* | cut -d " " -f2', {
    
  78.     cwd,
    
  79.   });
    
  80.   const commit = await execRead('git show -s --no-show-signature --format=%h', {
    
  81.     cwd,
    
  82.   });
    
  83.   const checksum = await getChecksumForCurrentRevision(cwd);
    
  84.   const dateString = await getDateStringForCommit(commit);
    
  85.   const version = isExperimental
    
  86.     ? `0.0.0-experimental-${commit}-${dateString}`
    
  87.     : `0.0.0-${commit}-${dateString}`;
    
  88. 
    
  89.   // Only available for Circle CI builds.
    
  90.   // https://circleci.com/docs/2.0/env-vars/
    
  91.   const buildNumber = process.env.CIRCLE_BUILD_NUM;
    
  92. 
    
  93.   // React version is stored explicitly, separately for DevTools support.
    
  94.   // See updateVersionsForNext() below for more info.
    
  95.   const packageJSON = await readJson(
    
  96.     join(cwd, 'packages', 'react', 'package.json')
    
  97.   );
    
  98.   const reactVersion = isExperimental
    
  99.     ? `${packageJSON.version}-experimental-${commit}-${dateString}`
    
  100.     : `${packageJSON.version}-${commit}-${dateString}`;
    
  101. 
    
  102.   return {branch, buildNumber, checksum, commit, reactVersion, version};
    
  103. };
    
  104. 
    
  105. const getChecksumForCurrentRevision = async cwd => {
    
  106.   const packagesDir = join(cwd, 'packages');
    
  107.   const hashedPackages = await hashElement(packagesDir, {
    
  108.     encoding: 'hex',
    
  109.     files: {exclude: ['.DS_Store']},
    
  110.   });
    
  111.   return hashedPackages.hash.slice(0, 7);
    
  112. };
    
  113. 
    
  114. const getDateStringForCommit = async commit => {
    
  115.   let dateString = await execRead(
    
  116.     `git show -s --no-show-signature --format=%cd --date=format:%Y%m%d ${commit}`
    
  117.   );
    
  118. 
    
  119.   // On CI environment, this string is wrapped with quotes '...'s
    
  120.   if (dateString.startsWith("'")) {
    
  121.     dateString = dateString.slice(1, 9);
    
  122.   }
    
  123. 
    
  124.   return dateString;
    
  125. };
    
  126. 
    
  127. const getCommitFromCurrentBuild = async () => {
    
  128.   const cwd = join(__dirname, '..', '..');
    
  129. 
    
  130.   // If this build includes a build-info.json file, extract the commit from it.
    
  131.   // Otherwise fall back to parsing from the package version number.
    
  132.   // This is important to make the build reproducible (e.g. by Mozilla reviewers).
    
  133.   const buildInfoJSON = join(
    
  134.     cwd,
    
  135.     'build',
    
  136.     'oss-experimental',
    
  137.     'react',
    
  138.     'build-info.json'
    
  139.   );
    
  140.   if (existsSync(buildInfoJSON)) {
    
  141.     const buildInfo = await readJson(buildInfoJSON);
    
  142.     return buildInfo.commit;
    
  143.   } else {
    
  144.     const packageJSON = join(
    
  145.       cwd,
    
  146.       'build',
    
  147.       'oss-experimental',
    
  148.       'react',
    
  149.       'package.json'
    
  150.     );
    
  151.     const {version} = await readJson(packageJSON);
    
  152.     return extractCommitFromVersionNumber(version);
    
  153.   }
    
  154. };
    
  155. 
    
  156. const getPublicPackages = isExperimental => {
    
  157.   const packageNames = Object.keys(stablePackages);
    
  158.   if (isExperimental) {
    
  159.     packageNames.push(...experimentalPackages);
    
  160.   }
    
  161.   return packageNames;
    
  162. };
    
  163. 
    
  164. const handleError = error => {
    
  165.   logUpdate.clear();
    
  166. 
    
  167.   const message = error.message.trim().replace(/\n +/g, '\n');
    
  168.   const stack = error.stack.replace(error.message, '');
    
  169. 
    
  170.   console.log(theme`{error ${message}}\n\n{path ${stack}}`);
    
  171.   process.exit(1);
    
  172. };
    
  173. 
    
  174. const logPromise = async (promise, text, estimate) =>
    
  175.   logger(promise, text, {estimate});
    
  176. 
    
  177. const printDiff = (path, beforeContents, afterContents) => {
    
  178.   const patch = createPatch(path, beforeContents, afterContents);
    
  179.   const coloredLines = patch
    
  180.     .split('\n')
    
  181.     .slice(2) // Trim index file
    
  182.     .map((line, index) => {
    
  183.       if (index <= 1) {
    
  184.         return theme.diffHeader(line);
    
  185.       }
    
  186.       switch (line[0]) {
    
  187.         case '+':
    
  188.           return theme.diffAdded(line);
    
  189.         case '-':
    
  190.           return theme.diffRemoved(line);
    
  191.         case ' ':
    
  192.           return line;
    
  193.         case '@':
    
  194.           return null;
    
  195.         case '\\':
    
  196.           return null;
    
  197.       }
    
  198.     })
    
  199.     .filter(line => line);
    
  200.   console.log(coloredLines.join('\n'));
    
  201.   return patch;
    
  202. };
    
  203. 
    
  204. // Convert an array param (expected format "--foo bar baz")
    
  205. // to also accept comma input (e.g. "--foo bar,baz")
    
  206. const splitCommaParams = array => {
    
  207.   for (let i = array.length - 1; i >= 0; i--) {
    
  208.     const param = array[i];
    
  209.     if (param.includes(',')) {
    
  210.       array.splice(i, 1, ...param.split(','));
    
  211.     }
    
  212.   }
    
  213. };
    
  214. 
    
  215. // This method is used by both local Node release scripts and Circle CI bash scripts.
    
  216. // It updates version numbers in package JSONs (both the version field and dependencies),
    
  217. // As well as the embedded renderer version in "packages/shared/ReactVersion".
    
  218. // Canaries version numbers use the format of 0.0.0-<sha>-<date> to be easily recognized (e.g. 0.0.0-01974a867-20200129).
    
  219. // A separate "React version" is used for the embedded renderer version to support DevTools,
    
  220. // since it needs to distinguish between different version ranges of React.
    
  221. // It is based on the version of React in the local package.json (e.g. 16.12.0-01974a867-20200129).
    
  222. // Both numbers will be replaced if the "next" release is promoted to a stable release.
    
  223. const updateVersionsForNext = async (cwd, reactVersion, version) => {
    
  224.   const isExperimental = reactVersion.includes('experimental');
    
  225.   const packages = getPublicPackages(isExperimental);
    
  226.   const packagesDir = join(cwd, 'packages');
    
  227. 
    
  228.   // Update the shared React version source file.
    
  229.   // This is bundled into built renderers.
    
  230.   // The promote script will replace this with a final version later.
    
  231.   const sourceReactVersionPath = join(cwd, 'packages/shared/ReactVersion.js');
    
  232.   const sourceReactVersion = readFileSync(
    
  233.     sourceReactVersionPath,
    
  234.     'utf8'
    
  235.   ).replace(/export default '[^']+';/, `export default '${reactVersion}';`);
    
  236.   writeFileSync(sourceReactVersionPath, sourceReactVersion);
    
  237. 
    
  238.   // Update the root package.json.
    
  239.   // This is required to pass a later version check script.
    
  240.   {
    
  241.     const packageJSONPath = join(cwd, 'package.json');
    
  242.     const packageJSON = await readJson(packageJSONPath);
    
  243.     packageJSON.version = version;
    
  244.     await writeJson(packageJSONPath, packageJSON, {spaces: 2});
    
  245.   }
    
  246. 
    
  247.   for (let i = 0; i < packages.length; i++) {
    
  248.     const packageName = packages[i];
    
  249.     const packagePath = join(packagesDir, packageName);
    
  250. 
    
  251.     // Update version numbers in package JSONs
    
  252.     const packageJSONPath = join(packagePath, 'package.json');
    
  253.     const packageJSON = await readJson(packageJSONPath);
    
  254.     packageJSON.version = version;
    
  255. 
    
  256.     // Also update inter-package dependencies.
    
  257.     // Next releases always have exact version matches.
    
  258.     // The promote script may later relax these (e.g. "^x.x.x") based on source package JSONs.
    
  259.     const {dependencies, peerDependencies} = packageJSON;
    
  260.     for (let j = 0; j < packages.length; j++) {
    
  261.       const dependencyName = packages[j];
    
  262.       if (dependencies && dependencies[dependencyName]) {
    
  263.         dependencies[dependencyName] = version;
    
  264.       }
    
  265.       if (peerDependencies && peerDependencies[dependencyName]) {
    
  266.         peerDependencies[dependencyName] = version;
    
  267.       }
    
  268.     }
    
  269. 
    
  270.     await writeJson(packageJSONPath, packageJSON, {spaces: 2});
    
  271.   }
    
  272. };
    
  273. 
    
  274. module.exports = {
    
  275.   addDefaultParamValue,
    
  276.   confirm,
    
  277.   execRead,
    
  278.   getArtifactsList,
    
  279.   getBuildInfo,
    
  280.   getChecksumForCurrentRevision,
    
  281.   getCommitFromCurrentBuild,
    
  282.   getDateStringForCommit,
    
  283.   getPublicPackages,
    
  284.   handleError,
    
  285.   logPromise,
    
  286.   printDiff,
    
  287.   splitCommaParams,
    
  288.   theme,
    
  289.   updateVersionsForNext,
    
  290. };