Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Add a new `dependsOnNodeVersion` setting for operation entries in rush-project.json. When enabled, the Node.js version is included in the build cache hash, ensuring that cached outputs are invalidated when the Node.js version changes. Accepts `true` (alias for `\"patch\"`), `\"major\"`, `\"minor\"`, or `\"patch\"` to control the granularity of version matching.",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
4 changes: 4 additions & 0 deletions common/reviews/api/rush-lib.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,7 @@ export interface IOperationSettings {
allowCobuildWithoutCache?: boolean;
dependsOnAdditionalFiles?: string[];
dependsOnEnvVars?: string[];
dependsOnNodeVersion?: boolean | NodeVersionGranularity;
disableBuildCacheForOperation?: boolean;
ignoreChangedProjectsOnlyFlag?: boolean;
operationName: string;
Expand Down Expand Up @@ -961,6 +962,9 @@ export class LockStepVersionPolicy extends VersionPolicy {

export { LookupByPath }

// @alpha
export type NodeVersionGranularity = 'major' | 'minor' | 'patch';

// @public
export class NpmOptionsConfiguration extends PackageManagerOptionsConfigurationBase {
// @internal
Expand Down
25 changes: 25 additions & 0 deletions libraries/rush-lib/src/api/RushProjectConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ export interface IRushPhaseSharding {
shardOperationSettings?: unknown;
}

/**
* The granularity at which the Node.js version is included in the build cache hash.
*
* - `"major"` — includes only the major version (e.g. `18`)
* - `"minor"` — includes the major and minor version (e.g. `18.17`)
* - `"patch"` — includes the full version (e.g. `18.17.1`)
*
* @alpha
*/
export type NodeVersionGranularity = 'major' | 'minor' | 'patch';

/**
* @alpha
*/
Expand Down Expand Up @@ -112,6 +123,20 @@ export interface IOperationSettings {
*/
dependsOnEnvVars?: string[];

/**
* Specifies whether and at what granularity the Node.js version should be included in the hash
* used for the build cache. When enabled, changing the Node.js version at the specified granularity
* will invalidate cached outputs and cause the operation to be re-executed. This is useful for
* projects that produce Node.js-version-specific outputs, such as native module builds.
*
* Allowed values:
* - `true` — alias for `"patch"`, includes the full version (e.g. `18.17.1`)
* - `"major"` — includes only the major version (e.g. `18`)
* - `"minor"` — includes the major and minor version (e.g. `18.17`)
* - `"patch"` — includes the full version (e.g. `18.17.1`)
*/
dependsOnNodeVersion?: boolean | NodeVersionGranularity;

/**
* An optional list of glob (minimatch) patterns pointing to files that can affect this operation.
* The hash values of the contents of these files will become part of the final hash when reading
Expand Down
1 change: 1 addition & 0 deletions libraries/rush-lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export { RushConfigurationProject } from './api/RushConfigurationProject';
export {
type IRushProjectJson as _IRushProjectJson,
type IOperationSettings,
type NodeVersionGranularity,
RushProjectConfiguration,
type IRushPhaseSharding
} from './api/RushProjectConfiguration';
Expand Down
44 changes: 42 additions & 2 deletions libraries/rush-lib/src/logic/incremental/InputsSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import { type IReadonlyLookupByPath, LookupByPath } from '@rushstack/lookup-by-p
import { InternalError, Path, Sort } from '@rushstack/node-core-library';

import type { RushConfigurationProject } from '../../api/RushConfigurationProject';
import type { IOperationSettings, RushProjectConfiguration } from '../../api/RushProjectConfiguration';
import type {
IOperationSettings,
NodeVersionGranularity,
RushProjectConfiguration
} from '../../api/RushProjectConfiguration';
import { RushConstants } from '../RushConstants';

/**
Expand Down Expand Up @@ -90,6 +94,11 @@ export interface IInputsSnapshotParameters {
* @defaultValue \{ ...process.env \}
*/
environment?: Record<string, string | undefined>;
/**
* The Node.js version string to use for `dependsOnNodeVersion`. Defaults to `process.version`.
* @defaultValue process.version
*/
nodeVersion?: string;
/**
* File paths (keys into additionalHashes or hashes) to be included as part of every operation's dependencies.
*/
Expand Down Expand Up @@ -205,6 +214,10 @@ export class InputsSnapshot implements IInputsSnapshot {
* The environment to use for `dependsOnEnvVars`.
*/
private readonly _environment: Record<string, string | undefined>;
/**
* Pre-computed Node.js version strings at each granularity level for `dependsOnNodeVersion`.
*/
private readonly _nodeVersionByGranularity: Readonly<Record<NodeVersionGranularity, string>>;

/**
*
Expand All @@ -219,6 +232,7 @@ export class InputsSnapshot implements IInputsSnapshot {
hashes,
hasUncommittedChanges,
lookupByPath,
nodeVersion = process.version,
rootDir
} = params;
const projectMetadataMap: Map<
Expand Down Expand Up @@ -274,6 +288,8 @@ export class InputsSnapshot implements IInputsSnapshot {
this._globalAdditionalHashes = globalAdditionalHashes;
// Snapshot the environment so that queries are not impacted by when they happen
this._environment = environment;
// Parse Node.js version once so it doesn't need to be re-parsed per operation
this._nodeVersionByGranularity = _parseNodeVersion(nodeVersion);
this.hashes = hashes;
this.hasUncommittedChanges = hasUncommittedChanges;
this.rootDirectory = rootDir;
Expand Down Expand Up @@ -380,7 +396,7 @@ export class InputsSnapshot implements IInputsSnapshot {
const operationSettings: Readonly<IOperationSettings> | undefined =
record.projectConfig?.operationSettingsByOperationName.get(operationName);
if (operationSettings) {
const { dependsOnEnvVars, outputFolderNames } = operationSettings;
const { dependsOnEnvVars, dependsOnNodeVersion, outputFolderNames } = operationSettings;
if (dependsOnEnvVars) {
// As long as we enumerate environment variables in a consistent order, we will get a stable hash.
// Changing the order in rush-project.json will change the hash anyway since the file contents are part of the hash.
Expand All @@ -389,6 +405,12 @@ export class InputsSnapshot implements IInputsSnapshot {
}
}

if (dependsOnNodeVersion) {
const granularity: NodeVersionGranularity =
dependsOnNodeVersion === true ? 'patch' : dependsOnNodeVersion;
hasher.update(`${hashDelimiter}nodeVersion=${this._nodeVersionByGranularity[granularity]}`);
}

if (outputFolderNames) {
hasher.update(`${hashDelimiter}${JSON.stringify(outputFolderNames)}`);
}
Expand Down Expand Up @@ -421,6 +443,24 @@ export class InputsSnapshot implements IInputsSnapshot {
}
}

/**
* Parses a Node.js version string once and returns pre-computed strings for each granularity level.
*
* @param rawVersion - The full Node.js version string (e.g. `v18.17.1`)
* @returns An object with pre-computed version strings for `major`, `minor`, and `patch` granularities
*/
function _parseNodeVersion(rawVersion: string): Record<NodeVersionGranularity, string> {
// Strip leading 'v' if present
const version: string = rawVersion.startsWith('v') ? rawVersion.slice(1) : rawVersion;
const [major, minor]: string[] = version.split('.');

return {
major,
minor: `${major}.${minor}`,
patch: version
};
}

function getOrCreateProjectFilter(
record: IInternalInputsSnapshotProjectMetadata
): (filePath: string) => boolean {
Expand Down
Loading