Skip to content

Commit

Permalink
Add single source of truth for package versions (#21608)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite authored Jun 3, 2021
1 parent 86715ef commit 6736a38
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 20 deletions.
62 changes: 62 additions & 0 deletions ReactVersions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict';

// This module is the single source of truth for versioning packages that we
// publish to npm.
//
// Packages will not be published unless they are added here.
//
// The @latest channel uses the version as-is, e.g.:
//
// 18.0.0
//
// The @next channel appends additional information, with the scheme
// <version>-<label>-<commit_sha>, e.g.:
//
// 18.0.0-next-a1c2d3e4
//
// (TODO: ^ this isn't enabled quite yet. We still use <version>-<commit_sha>.)
//
// The @experimental channel doesn't include a version, only a sha, e.g.:
//
// 0.0.0-experimental-a1c2d3e4

// TODO: Main includes breaking changes. Bump this to 18.0.0.
const ReactVersion = '17.0.3';

// The label used by the @next channel. Represents the upcoming release's
// stability. Could be "alpha", "beta", "rc", etc.
const nextChannelLabel = 'next';

const stablePackages = {
'create-subscription': ReactVersion,
'eslint-plugin-react-hooks': '4.2.1',
'jest-react': '0.12.1',
react: ReactVersion,
'react-art': ReactVersion,
'react-dom': ReactVersion,
'react-is': ReactVersion,
'react-reconciler': '0.27.0',
'react-refresh': '0.11.0',
'react-test-renderer': ReactVersion,
'use-subscription': '1.6.0',
scheduler: '0.21.0',
};

// These packages do not exist in the @next or @latest channel, only
// @experimental. We don't use semver, just the commit sha, so this is just a
// list of package names instead of a map.
const experimentalPackages = [
'react-fetch',
'react-fs',
'react-pg',
'react-server-dom-webpack',
'react-server',
];

// TODO: Export a map of every package and its version.
module.exports = {
ReactVersion,
nextChannelLabel,
stablePackages,
experimentalPackages,
};
3 changes: 3 additions & 0 deletions packages/shared/ReactVersion.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@
// TODO: 17.0.3 has not been released to NPM;
// It exists as a placeholder so that DevTools can support work tag changes between releases.
// When we next publish a release (either 17.0.3 or 17.1.0), update the matching TODO in backend/renderer.js
// TODO: This module is used both by the release scripts and to expose a version
// at runtime. We should instead inject the version number as part of the build
// process, and use the ReactVersions.js module as the single source of truth.
export default '17.0.3';
1 change: 1 addition & 0 deletions scripts/release/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const getCommitFromCurrentBuild = async () => {
};

const getPublicPackages = isExperimental => {
// TODO: Use ReactVersions.js as source of truth.
if (isExperimental) {
return [
'create-subscription',
Expand Down
72 changes: 52 additions & 20 deletions scripts/rollup/build-all-release-channels.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ const {spawnSync} = require('child_process');
const path = require('path');
const tmp = require('tmp');

const {
ReactVersion,
stablePackages,
experimentalPackages,
} = require('../../ReactVersions');

// Runs the build script for both stable and experimental release channels,
// by configuring an environment variable.

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

if (process.env.CIRCLE_NODE_TOTAL) {
// In CI, we use multiple concurrent processes. Allocate half the processes to
Expand All @@ -27,19 +31,17 @@ if (process.env.CIRCLE_NODE_TOTAL) {
if (index < halfTotal) {
const nodeTotal = halfTotal;
const nodeIndex = index;
const version = '0.0.0-' + sha;
updateTheReactVersionThatDevToolsReads(ReactVersion + '-' + sha);
buildForChannel('stable', nodeTotal, nodeIndex);
processStable('./build', version);
processStable('./build');
} else {
const nodeTotal = total - halfTotal;
const nodeIndex = index - halfTotal;
const version = '0.0.0-experimental-' + sha;
updateTheReactVersionThatDevToolsReads(
ReactVersion + '-experimental-' + sha
);
buildForChannel('experimental', nodeTotal, nodeIndex);
processExperimental('./build', version);
processExperimental('./build');
}

// TODO: Currently storing artifacts as `./build2` so that it doesn't conflict
Expand All @@ -48,17 +50,16 @@ if (process.env.CIRCLE_NODE_TOTAL) {
} else {
// Running locally, no concurrency. Move each channel's build artifacts into
// a temporary directory so that they don't conflict.
const stableVersion = '0.0.0-' + sha;
updateTheReactVersionThatDevToolsReads(ReactVersion + '-' + sha);
buildForChannel('stable', '', '');
const stableDir = tmp.dirSync().name;
crossDeviceRenameSync('./build', stableDir);
processStable(stableDir, stableVersion);

const experimentalVersion = '0.0.0-experimental-' + sha;
processStable(stableDir);
updateTheReactVersionThatDevToolsReads(ReactVersion + '-experimental-' + sha);
buildForChannel('experimental', '', '');
const experimentalDir = tmp.dirSync().name;
crossDeviceRenameSync('./build', experimentalDir);
processExperimental(experimentalDir, experimentalVersion);
processExperimental(experimentalDir);

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

function processStable(buildDir, version) {
function processStable(buildDir) {
if (fs.existsSync(buildDir + '/node_modules')) {
updatePackageVersions(buildDir + '/node_modules', version);
const defaultVersionIfNotFound = '0.0.0' + '-' + sha;
const versionsMap = new Map();
for (const moduleName in stablePackages) {
// TODO: Use version declared in ReactVersions module instead of 0.0.0.
// const version = stablePackages[moduleName];
// versionsMap.set(moduleName, version + '-' + nextChannelLabel + '-' + sha);
versionsMap.set(moduleName, defaultVersionIfNotFound);
}
updatePackageVersions(
buildDir + '/node_modules',
versionsMap,
defaultVersionIfNotFound
);
fs.renameSync(buildDir + '/node_modules', buildDir + '/oss-stable');
}

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

function processExperimental(buildDir, version) {
if (fs.existsSync(buildDir + '/node_modules')) {
updatePackageVersions(buildDir + '/node_modules', version);
const defaultVersionIfNotFound = '0.0.0' + '-' + 'experimental' + '-' + sha;
const versionsMap = new Map();
for (const moduleName in stablePackages) {
versionsMap.set(moduleName, defaultVersionIfNotFound);
}
for (const moduleName of experimentalPackages) {
versionsMap.set(moduleName, defaultVersionIfNotFound);
}
updatePackageVersions(
buildDir + '/node_modules',
versionsMap,
defaultVersionIfNotFound
);
fs.renameSync(buildDir + '/node_modules', buildDir + '/oss-experimental');
}

Expand Down Expand Up @@ -151,9 +176,18 @@ function crossDeviceRenameSync(source, destination) {
* to match this version for all of the 'React' packages
* (packages available in this repo).
*/
function updatePackageVersions(modulesDir, version) {
const allReactModuleNames = fs.readdirSync('packages');
function updatePackageVersions(
modulesDir,
versionsMap,
defaultVersionIfNotFound
) {
for (const moduleName of fs.readdirSync(modulesDir)) {
let version = versionsMap.get(moduleName);
if (version === undefined) {
// TODO: If the module is not in the version map, we should exclude it
// from the build artifacts.
version = defaultVersionIfNotFound;
}
const packageJSONPath = path.join(modulesDir, moduleName, 'package.json');
const stats = fs.statSync(packageJSONPath);
if (stats.isFile()) {
Expand All @@ -164,16 +198,14 @@ function updatePackageVersions(modulesDir, version) {

if (packageInfo.dependencies) {
for (const dep of Object.keys(packageInfo.dependencies)) {
// if it's a react package (available in the current repo), update the version
// TODO: is this too broad? Assumes all of the packages were built.
if (allReactModuleNames.includes(dep)) {
if (modulesDir.includes(dep)) {
packageInfo.dependencies[dep] = version;
}
}
}
if (packageInfo.peerDependencies) {
for (const dep of Object.keys(packageInfo.peerDependencies)) {
if (allReactModuleNames.includes(dep)) {
if (modulesDir.includes(dep)) {
packageInfo.peerDependencies[dep] = version;
}
}
Expand Down

0 comments on commit 6736a38

Please sign in to comment.