Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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,11 @@
{
"changes": [
{
"comment": "Filter npm-incompatible properties from .npmrc when npm is used with a configuration intended for pnpm or yarn, to eliminate spurious warnings during package manager installation.",
"type": "none",
"packageName": "@microsoft/rush"
}
],
"packageName": "@microsoft/rush",
"email": "copilot@github.com"
}
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,10 @@ export class InstallHelpers {
// In particular, we'll assume that two different NPM registries cannot have two
// different implementations of the same version of the same package.
// This was needed for: https://github.com/microsoft/rushstack/issues/691
commonRushConfigFolder: rushConfiguration.commonRushConfigFolder
commonRushConfigFolder: rushConfiguration.commonRushConfigFolder,
// Only filter npm-incompatible properties when the repo uses pnpm or yarn.
// If the repo uses npm, the .npmrc is already configured for npm, so don't filter.
filterNpmIncompatibleProperties: rushConfiguration.packageManager !== 'npm'
});

logIfConsoleOutputIsNotRestricted(
Expand Down
10 changes: 8 additions & 2 deletions libraries/rush-lib/src/scripts/install-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,10 @@ function _resolvePackageVersion(
sourceNpmrcFolder,
targetNpmrcFolder: rushTempFolder,
logger,
supportEnvVarFallbackSyntax: false
supportEnvVarFallbackSyntax: false,
// Always filter npm-incompatible properties in install-run scripts.
// Any warnings will be shown when running Rush commands directly.
filterNpmIncompatibleProperties: true
});

// This returns something that looks like:
Expand Down Expand Up @@ -459,7 +462,10 @@ export function installAndRun(
sourceNpmrcFolder,
targetNpmrcFolder: packageInstallFolder,
logger,
supportEnvVarFallbackSyntax: false
supportEnvVarFallbackSyntax: false,
// Always filter npm-incompatible properties in install-run scripts.
// Any warnings will be shown when running Rush commands directly.
filterNpmIncompatibleProperties: true
});

_createPackageJson(packageInstallFolder, packageName, packageVersion);
Expand Down
14 changes: 12 additions & 2 deletions libraries/rush-lib/src/utilities/Utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ export interface IInstallPackageInDirectoryOptions {
maxInstallAttempts: number;
commonRushConfigFolder: string | undefined;
suppressOutput?: boolean;
/**
* Whether to filter npm-incompatible properties from .npmrc.
* This should be true when the .npmrc is configured for a different package manager (pnpm/yarn)
* but npm is being used to install packages.
*/
filterNpmIncompatibleProperties?: boolean;
}

export interface ILifecycleCommandOptions {
Expand Down Expand Up @@ -505,7 +511,8 @@ export class Utilities {
commonRushConfigFolder,
maxInstallAttempts,
suppressOutput,
directory
directory,
filterNpmIncompatibleProperties = false
}: IInstallPackageInDirectoryOptions): Promise<void> {
directory = path.resolve(directory);
const directoryExists: boolean = await FileSystem.existsAsync(directory);
Expand All @@ -531,7 +538,10 @@ export class Utilities {
Utilities.syncNpmrc({
sourceNpmrcFolder: commonRushConfigFolder,
targetNpmrcFolder: directory,
supportEnvVarFallbackSyntax: false
supportEnvVarFallbackSyntax: false,
// Filter out npm-incompatible properties only when the .npmrc is configured for
// a different package manager (pnpm/yarn) but npm is being used to install.
filterNpmIncompatibleProperties
});
}

Expand Down
198 changes: 156 additions & 42 deletions libraries/rush-lib/src/utilities/npmrcUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,22 @@ const _combinedNpmrcMap: Map<string, string> = new Map();
function _trimNpmrcFile(
options: Pick<
INpmrcTrimOptions,
'sourceNpmrcPath' | 'linesToAppend' | 'linesToPrepend' | 'supportEnvVarFallbackSyntax'
| 'sourceNpmrcPath'
| 'linesToAppend'
| 'linesToPrepend'
| 'supportEnvVarFallbackSyntax'
| 'filterNpmIncompatibleProperties'
| 'env'
>
): string {
const { sourceNpmrcPath, linesToPrepend, linesToAppend, supportEnvVarFallbackSyntax } = options;
const {
sourceNpmrcPath,
linesToPrepend,
linesToAppend,
supportEnvVarFallbackSyntax,
filterNpmIncompatibleProperties,
env = process.env
} = options;
const combinedNpmrcFromCache: string | undefined = _combinedNpmrcMap.get(sourceNpmrcPath);
if (combinedNpmrcFromCache !== undefined) {
return combinedNpmrcFromCache;
Expand All @@ -49,7 +61,12 @@ function _trimNpmrcFile(

npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim());

const resultLines: string[] = trimNpmrcFileLines(npmrcFileLines, process.env, supportEnvVarFallbackSyntax);
const resultLines: string[] = trimNpmrcFileLines(
npmrcFileLines,
env,
supportEnvVarFallbackSyntax,
filterNpmIncompatibleProperties
);

const combinedNpmrc: string = resultLines.join('\n');

Expand All @@ -59,17 +76,64 @@ function _trimNpmrcFile(
return combinedNpmrc;
}

/**
* List of npmrc properties that are not supported by npm but may be present in the config.
* These include pnpm-specific properties and deprecated npm properties.
*/
const NPM_INCOMPATIBLE_PROPERTIES: Set<string> = new Set([
// pnpm-specific hoisting configuration
'hoist',
'hoist-pattern',
'public-hoist-pattern',
'shamefully-hoist',
// Deprecated or unknown npm properties that cause warnings
'email',
'publish-branch'
]);

/**
* List of registry-scoped npmrc property suffixes that are pnpm-specific.
* These are properties like "//registry.example.com/:tokenHelper" where "tokenHelper"
* is the suffix after the last colon.
*/
const NPM_INCOMPATIBLE_REGISTRY_SCOPED_PROPERTIES: Set<string> = new Set([
// pnpm-specific token helper properties
'tokenHelper',
'urlTokenHelper'
]);

/**
* Regular expression to extract property names from .npmrc lines.
* Matches everything before '=', '[', or whitespace to capture the property name.
* Note: The 'g' flag is intentionally omitted since we only need the first match.
* Examples:
* "registry=https://..." -> matches "registry"
* "hoist-pattern[]=..." -> matches "hoist-pattern"
*/
const PROPERTY_NAME_REGEX: RegExp = /^([^=\[\s]+)/;

/**
* Regular expression to extract environment variable names and optional fallback values.
* Matches patterns like:
* nameString -> group 1: nameString, group 2: undefined
* nameString-fallbackString -> group 1: nameString, group 2: fallbackString
* nameString:-fallbackString -> group 1: nameString, group 2: fallbackString
*/
const ENV_VAR_WITH_FALLBACK_REGEX: RegExp = /^(?<name>[^:-]+)(?::?-(?<fallback>.+))?$/;

/**
*
* @param npmrcFileLines The npmrc file's lines
* @param env The environment variables object
* @param supportEnvVarFallbackSyntax Whether to support fallback values in the form of `${VAR_NAME:-fallback}`
* @returns
* @param filterNpmIncompatibleProperties Whether to filter out properties that npm doesn't understand
* @returns An array of processed npmrc file lines with undefined environment variables and npm-incompatible properties commented out
*/
export function trimNpmrcFileLines(
npmrcFileLines: string[],
env: NodeJS.ProcessEnv,
supportEnvVarFallbackSyntax: boolean
supportEnvVarFallbackSyntax: boolean,
filterNpmIncompatibleProperties: boolean = false
): string[] {
const resultLines: string[] = [];

Expand All @@ -82,6 +146,7 @@ export function trimNpmrcFileLines(
// Trim out lines that reference environment variables that aren't defined
for (let line of npmrcFileLines) {
let lineShouldBeTrimmed: boolean = false;
let trimReason: string = '';

//remove spaces before or after key and value
line = line
Expand All @@ -91,51 +156,92 @@ export function trimNpmrcFileLines(

// Ignore comment lines
if (!commentRegExp.test(line)) {
const environmentVariables: string[] | null = line.match(expansionRegExp);
if (environmentVariables) {
for (const token of environmentVariables) {
/**
* Remove the leading "${" and the trailing "}" from the token
*
* ${nameString} -> nameString
* ${nameString-fallbackString} -> name-fallbackString
* ${nameString:-fallbackString} -> name:-fallbackString
*/
const nameWithFallback: string = token.substring(2, token.length - 1);

let environmentVariableName: string;
let fallback: string | undefined;
if (supportEnvVarFallbackSyntax) {
/**
* Get the environment variable name and fallback value.
*
* name fallback
* nameString -> nameString undefined
* nameString-fallbackString -> nameString fallbackString
* nameString:-fallbackString -> nameString fallbackString
*/
const matched: string[] | null = nameWithFallback.match(/^([^:-]+)(?:\:?-(.+))?$/);
// matched: [originStr, variableName, fallback]
environmentVariableName = matched?.[1] ?? nameWithFallback;
fallback = matched?.[2];
// Check if this is a property that npm doesn't understand
if (filterNpmIncompatibleProperties) {
// Extract the property name (everything before the '=' or '[')
const match: RegExpMatchArray | null = line.match(PROPERTY_NAME_REGEX);
if (match) {
const propertyName: string = match[1];

// Check if this is a registry-scoped property (starts with "//" like "//registry.npmjs.org/:_authToken")
const isRegistryScoped: boolean = propertyName.startsWith('//');

if (isRegistryScoped) {
// For registry-scoped properties, check if the suffix (after the last colon) is npm-incompatible
// Example: "//registry.example.com/:tokenHelper" -> suffix is "tokenHelper"
const lastColonIndex: number = propertyName.lastIndexOf(':');
if (lastColonIndex !== -1) {
const registryPropertySuffix: string = propertyName.substring(lastColonIndex + 1);
if (NPM_INCOMPATIBLE_REGISTRY_SCOPED_PROPERTIES.has(registryPropertySuffix)) {
lineShouldBeTrimmed = true;
trimReason = 'NPM_INCOMPATIBLE_PROPERTY';
}
}
} else {
environmentVariableName = nameWithFallback;
// For non-registry-scoped properties, check the full property name
if (NPM_INCOMPATIBLE_PROPERTIES.has(propertyName)) {
lineShouldBeTrimmed = true;
trimReason = 'NPM_INCOMPATIBLE_PROPERTY';
}
}
}
}

// Is the environment variable and fallback value defined.
if (!env[environmentVariableName] && !fallback) {
// No, so trim this line
lineShouldBeTrimmed = true;
break;
// Check for undefined environment variables
if (!lineShouldBeTrimmed) {
const environmentVariables: string[] | null = line.match(expansionRegExp);
if (environmentVariables) {
for (const token of environmentVariables) {
/**
* Remove the leading "${" and the trailing "}" from the token
*
* ${nameString} -> nameString
* ${nameString-fallbackString} -> name-fallbackString
* ${nameString:-fallbackString} -> name:-fallbackString
*/
const nameWithFallback: string = token.slice(2, -1);

let environmentVariableName: string;
let fallback: string | undefined;
if (supportEnvVarFallbackSyntax) {
/**
* Get the environment variable name and fallback value.
*
* name fallback
* nameString -> nameString undefined
* nameString-fallbackString -> nameString fallbackString
* nameString:-fallbackString -> nameString fallbackString
*/
const matched: RegExpMatchArray | null = nameWithFallback.match(ENV_VAR_WITH_FALLBACK_REGEX);
environmentVariableName = matched?.groups?.name ?? nameWithFallback;
fallback = matched?.groups?.fallback;
} else {
environmentVariableName = nameWithFallback;
}

// Is the environment variable and fallback value defined.
if (!env[environmentVariableName] && !fallback) {
// No, so trim this line
lineShouldBeTrimmed = true;
trimReason = 'MISSING_ENVIRONMENT_VARIABLE';
break;
}
}
}
}
}

if (lineShouldBeTrimmed) {
// Example output:
// "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}"
resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line);
// Comment out the line with appropriate reason
if (trimReason === 'NPM_INCOMPATIBLE_PROPERTY') {
// Example output:
// "; UNSUPPORTED BY NPM: email=test@example.com"
resultLines.push('; UNSUPPORTED BY NPM: ' + line);
} else {
// Example output:
// "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}"
resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line);
}
} else {
resultLines.push(line);
}
Expand Down Expand Up @@ -165,6 +271,8 @@ interface INpmrcTrimOptions {
linesToPrepend?: string[];
linesToAppend?: string[];
supportEnvVarFallbackSyntax: boolean;
filterNpmIncompatibleProperties?: boolean;
env?: NodeJS.ProcessEnv;
}

function _copyAndTrimNpmrcFile(options: INpmrcTrimOptions): string {
Expand Down Expand Up @@ -197,6 +305,8 @@ export interface ISyncNpmrcOptions {
linesToPrepend?: string[];
linesToAppend?: string[];
createIfMissing?: boolean;
filterNpmIncompatibleProperties?: boolean;
env?: NodeJS.ProcessEnv;
}

export function syncNpmrc(options: ISyncNpmrcOptions): string | undefined {
Expand Down Expand Up @@ -252,7 +362,11 @@ export function isVariableSetInNpmrcFile(
return false;
}

const trimmedNpmrcFile: string = _trimNpmrcFile({ sourceNpmrcPath, supportEnvVarFallbackSyntax });
const trimmedNpmrcFile: string = _trimNpmrcFile({
sourceNpmrcPath,
supportEnvVarFallbackSyntax,
filterNpmIncompatibleProperties: false
});

const variableKeyRegExp: RegExp = new RegExp(`^${variableKey}=`, 'm');
return trimmedNpmrcFile.match(variableKeyRegExp) !== null;
Expand Down
Loading