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