Skip to content

Commit 6736a38

Browse files
authored
Add single source of truth for package versions (#21608)
The versioning scheme for `@next` releases does not include semver information. Like `@experimental`, the versions are based only on the hash, i.e. `0.0.0-<commit_sha>`. The reason we do this is to prevent the use of a tilde (~) or caret (^) to match a range of prerelease versions. For `@experimental`, I think this rationale still makes sense — those releases are very unstable, with frequent breaking changes. But `@next` is not as volatile. It represents the next stable release. So, I think we can afford to include an actual verison number at the beginning of the string instead of `0.0.0`. We can also add a label that indicates readiness of the upcoming release, like "alpha", "beta", "rc", etc. To prepare for this the new versioning scheme, I updated the build script. However, **this PR does not enable the new versioning scheme yet**. I left a TODO above the line that we'll change once we're ready. We need to specify the expected next version numbers for each package, somewhere. These aren't encoded anywhere today — we don't specify version numbers until right before publishing to `@latest`, using an interactive script: `prepare-release-from-npm`. Instead, what we can do is track these version numbers in a module. I added `ReactVersions.js` that acts as the single source of truth for every package's version. The build script uses this module to build the `@next` packages. In the future, I want to start building the `@latest` packages the same way we do `@next` and `@experimental`. (What we do now is download a `@next` release from npm and swap out its version numbers.) Then we could run automated tests in CI to confirm the packages are releasable, instead of waiting to verify that right before publish.
1 parent 86715ef commit 6736a38

File tree

4 files changed

+118
-20
lines changed

4 files changed

+118
-20
lines changed

ReactVersions.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict';
2+
3+
// This module is the single source of truth for versioning packages that we
4+
// publish to npm.
5+
//
6+
// Packages will not be published unless they are added here.
7+
//
8+
// The @latest channel uses the version as-is, e.g.:
9+
//
10+
// 18.0.0
11+
//
12+
// The @next channel appends additional information, with the scheme
13+
// <version>-<label>-<commit_sha>, e.g.:
14+
//
15+
// 18.0.0-next-a1c2d3e4
16+
//
17+
// (TODO: ^ this isn't enabled quite yet. We still use <version>-<commit_sha>.)
18+
//
19+
// The @experimental channel doesn't include a version, only a sha, e.g.:
20+
//
21+
// 0.0.0-experimental-a1c2d3e4
22+
23+
// TODO: Main includes breaking changes. Bump this to 18.0.0.
24+
const ReactVersion = '17.0.3';
25+
26+
// The label used by the @next channel. Represents the upcoming release's
27+
// stability. Could be "alpha", "beta", "rc", etc.
28+
const nextChannelLabel = 'next';
29+
30+
const stablePackages = {
31+
'create-subscription': ReactVersion,
32+
'eslint-plugin-react-hooks': '4.2.1',
33+
'jest-react': '0.12.1',
34+
react: ReactVersion,
35+
'react-art': ReactVersion,
36+
'react-dom': ReactVersion,
37+
'react-is': ReactVersion,
38+
'react-reconciler': '0.27.0',
39+
'react-refresh': '0.11.0',
40+
'react-test-renderer': ReactVersion,
41+
'use-subscription': '1.6.0',
42+
scheduler: '0.21.0',
43+
};
44+
45+
// These packages do not exist in the @next or @latest channel, only
46+
// @experimental. We don't use semver, just the commit sha, so this is just a
47+
// list of package names instead of a map.
48+
const experimentalPackages = [
49+
'react-fetch',
50+
'react-fs',
51+
'react-pg',
52+
'react-server-dom-webpack',
53+
'react-server',
54+
];
55+
56+
// TODO: Export a map of every package and its version.
57+
module.exports = {
58+
ReactVersion,
59+
nextChannelLabel,
60+
stablePackages,
61+
experimentalPackages,
62+
};

packages/shared/ReactVersion.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@
1010
// TODO: 17.0.3 has not been released to NPM;
1111
// It exists as a placeholder so that DevTools can support work tag changes between releases.
1212
// When we next publish a release (either 17.0.3 or 17.1.0), update the matching TODO in backend/renderer.js
13+
// TODO: This module is used both by the release scripts and to expose a version
14+
// at runtime. We should instead inject the version number as part of the build
15+
// process, and use the ReactVersions.js module as the single source of truth.
1316
export default '17.0.3';

scripts/release/utils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ const getCommitFromCurrentBuild = async () => {
132132
};
133133

134134
const getPublicPackages = isExperimental => {
135+
// TODO: Use ReactVersions.js as source of truth.
135136
if (isExperimental) {
136137
return [
137138
'create-subscription',

scripts/rollup/build-all-release-channels.js

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@ const {spawnSync} = require('child_process');
88
const path = require('path');
99
const tmp = require('tmp');
1010

11+
const {
12+
ReactVersion,
13+
stablePackages,
14+
experimentalPackages,
15+
} = require('../../ReactVersions');
16+
1117
// Runs the build script for both stable and experimental release channels,
1218
// by configuring an environment variable.
1319

1420
const sha = (
1521
spawnSync('git', ['show', '-s', '--format=%h']).stdout + ''
1622
).trim();
17-
const ReactVersion = JSON.parse(fs.readFileSync('packages/react/package.json'))
18-
.version;
1923

2024
if (process.env.CIRCLE_NODE_TOTAL) {
2125
// In CI, we use multiple concurrent processes. Allocate half the processes to
@@ -27,19 +31,17 @@ if (process.env.CIRCLE_NODE_TOTAL) {
2731
if (index < halfTotal) {
2832
const nodeTotal = halfTotal;
2933
const nodeIndex = index;
30-
const version = '0.0.0-' + sha;
3134
updateTheReactVersionThatDevToolsReads(ReactVersion + '-' + sha);
3235
buildForChannel('stable', nodeTotal, nodeIndex);
33-
processStable('./build', version);
36+
processStable('./build');
3437
} else {
3538
const nodeTotal = total - halfTotal;
3639
const nodeIndex = index - halfTotal;
37-
const version = '0.0.0-experimental-' + sha;
3840
updateTheReactVersionThatDevToolsReads(
3941
ReactVersion + '-experimental-' + sha
4042
);
4143
buildForChannel('experimental', nodeTotal, nodeIndex);
42-
processExperimental('./build', version);
44+
processExperimental('./build');
4345
}
4446

4547
// TODO: Currently storing artifacts as `./build2` so that it doesn't conflict
@@ -48,17 +50,16 @@ if (process.env.CIRCLE_NODE_TOTAL) {
4850
} else {
4951
// Running locally, no concurrency. Move each channel's build artifacts into
5052
// a temporary directory so that they don't conflict.
51-
const stableVersion = '0.0.0-' + sha;
53+
updateTheReactVersionThatDevToolsReads(ReactVersion + '-' + sha);
5254
buildForChannel('stable', '', '');
5355
const stableDir = tmp.dirSync().name;
5456
crossDeviceRenameSync('./build', stableDir);
55-
processStable(stableDir, stableVersion);
56-
57-
const experimentalVersion = '0.0.0-experimental-' + sha;
57+
processStable(stableDir);
58+
updateTheReactVersionThatDevToolsReads(ReactVersion + '-experimental-' + sha);
5859
buildForChannel('experimental', '', '');
5960
const experimentalDir = tmp.dirSync().name;
6061
crossDeviceRenameSync('./build', experimentalDir);
61-
processExperimental(experimentalDir, experimentalVersion);
62+
processExperimental(experimentalDir);
6263

6364
// Then merge the experimental folder into the stable one. processExperimental
6465
// will have already removed conflicting files.
@@ -84,9 +85,21 @@ function buildForChannel(channel, nodeTotal, nodeIndex) {
8485
});
8586
}
8687

87-
function processStable(buildDir, version) {
88+
function processStable(buildDir) {
8889
if (fs.existsSync(buildDir + '/node_modules')) {
89-
updatePackageVersions(buildDir + '/node_modules', version);
90+
const defaultVersionIfNotFound = '0.0.0' + '-' + sha;
91+
const versionsMap = new Map();
92+
for (const moduleName in stablePackages) {
93+
// TODO: Use version declared in ReactVersions module instead of 0.0.0.
94+
// const version = stablePackages[moduleName];
95+
// versionsMap.set(moduleName, version + '-' + nextChannelLabel + '-' + sha);
96+
versionsMap.set(moduleName, defaultVersionIfNotFound);
97+
}
98+
updatePackageVersions(
99+
buildDir + '/node_modules',
100+
versionsMap,
101+
defaultVersionIfNotFound
102+
);
90103
fs.renameSync(buildDir + '/node_modules', buildDir + '/oss-stable');
91104
}
92105

@@ -107,7 +120,19 @@ function processStable(buildDir, version) {
107120

108121
function processExperimental(buildDir, version) {
109122
if (fs.existsSync(buildDir + '/node_modules')) {
110-
updatePackageVersions(buildDir + '/node_modules', version);
123+
const defaultVersionIfNotFound = '0.0.0' + '-' + 'experimental' + '-' + sha;
124+
const versionsMap = new Map();
125+
for (const moduleName in stablePackages) {
126+
versionsMap.set(moduleName, defaultVersionIfNotFound);
127+
}
128+
for (const moduleName of experimentalPackages) {
129+
versionsMap.set(moduleName, defaultVersionIfNotFound);
130+
}
131+
updatePackageVersions(
132+
buildDir + '/node_modules',
133+
versionsMap,
134+
defaultVersionIfNotFound
135+
);
111136
fs.renameSync(buildDir + '/node_modules', buildDir + '/oss-experimental');
112137
}
113138

@@ -151,9 +176,18 @@ function crossDeviceRenameSync(source, destination) {
151176
* to match this version for all of the 'React' packages
152177
* (packages available in this repo).
153178
*/
154-
function updatePackageVersions(modulesDir, version) {
155-
const allReactModuleNames = fs.readdirSync('packages');
179+
function updatePackageVersions(
180+
modulesDir,
181+
versionsMap,
182+
defaultVersionIfNotFound
183+
) {
156184
for (const moduleName of fs.readdirSync(modulesDir)) {
185+
let version = versionsMap.get(moduleName);
186+
if (version === undefined) {
187+
// TODO: If the module is not in the version map, we should exclude it
188+
// from the build artifacts.
189+
version = defaultVersionIfNotFound;
190+
}
157191
const packageJSONPath = path.join(modulesDir, moduleName, 'package.json');
158192
const stats = fs.statSync(packageJSONPath);
159193
if (stats.isFile()) {
@@ -164,16 +198,14 @@ function updatePackageVersions(modulesDir, version) {
164198

165199
if (packageInfo.dependencies) {
166200
for (const dep of Object.keys(packageInfo.dependencies)) {
167-
// if it's a react package (available in the current repo), update the version
168-
// TODO: is this too broad? Assumes all of the packages were built.
169-
if (allReactModuleNames.includes(dep)) {
201+
if (modulesDir.includes(dep)) {
170202
packageInfo.dependencies[dep] = version;
171203
}
172204
}
173205
}
174206
if (packageInfo.peerDependencies) {
175207
for (const dep of Object.keys(packageInfo.peerDependencies)) {
176-
if (allReactModuleNames.includes(dep)) {
208+
if (modulesDir.includes(dep)) {
177209
packageInfo.peerDependencies[dep] = version;
178210
}
179211
}

0 commit comments

Comments
 (0)