Skip to content

Commit

Permalink
Merge pull request microsoft#3972 from dmichon-msft/rush-sdk-env
Browse files Browse the repository at this point in the history
[rush-sdk] Use rush-lib from parent process
  • Loading branch information
dmichon-msft authored Feb 17, 2023
2 parents ac4e41e + 1625aa3 commit 838c4ae
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 18 deletions.
10 changes: 10 additions & 0 deletions common/changes/@microsoft/rush/rush-sdk-env_2023-02-10-23-15.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Add code path to @rushstack/rush-sdk for inheriting @microsoft/rush-lib location from a parent process via the RUSH_LIB_PATH environment variable.",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
1 change: 1 addition & 0 deletions common/reviews/api/rush-lib.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export enum EnvironmentVariableNames {
RUSH_GIT_BINARY_PATH = "RUSH_GIT_BINARY_PATH",
RUSH_GLOBAL_FOLDER = "RUSH_GLOBAL_FOLDER",
RUSH_INVOKED_FOLDER = "RUSH_INVOKED_FOLDER",
RUSH_LIB_PATH = "RUSH_LIB_PATH",
RUSH_PARALLELISM = "RUSH_PARALLELISM",
RUSH_PNPM_STORE_PATH = "RUSH_PNPM_STORE_PATH",
RUSH_PNPM_VERIFY_STORE_INTEGRITY = "RUSH_PNPM_VERIFY_STORE_INTEGRITY",
Expand Down
8 changes: 7 additions & 1 deletion libraries/rush-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,11 @@
"publishOnlyDependencies": {
"@rushstack/rush-amazon-s3-build-cache-plugin": "workspace:*",
"@rushstack/rush-azure-storage-build-cache-plugin": "workspace:*"
}
},
"sideEffects": [
"lib-esnext/start-pnpm.js",
"lib-esnext/start.js",
"lib-esnext/startx.js",
"lib-esnext/utilities/SetRushLibPath.js"
]
}
7 changes: 7 additions & 0 deletions libraries/rush-lib/src/api/EnvironmentConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ export enum EnvironmentVariableNames {
*/
RUSH_TAR_BINARY_PATH = 'RUSH_TAR_BINARY_PATH',

/**
* Explicitly specifies the path for the version of `@microsoft/rush-lib` being executed.
* Will be set upon loading Rush.
*/
RUSH_LIB_PATH = 'RUSH_LIB_PATH',

/**
* When Rush executes shell scripts, it sometimes changes the working directory to be a project folder or
* the repository root folder. The original working directory (where the Rush command was invoked) is assigned
Expand Down Expand Up @@ -441,6 +447,7 @@ export class EnvironmentConfiguration {
break;

case EnvironmentVariableNames.RUSH_INVOKED_FOLDER:
case EnvironmentVariableNames.RUSH_LIB_PATH:
// Assigned by Rush itself
break;

Expand Down
2 changes: 2 additions & 0 deletions libraries/rush-lib/src/api/Rush.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
PackageJsonLookup
} from '@rushstack/node-core-library';

import '../utilities/SetRushLibPath';

import { RushCommandLineParser } from '../cli/RushCommandLineParser';
import { RushStartupBanner } from '../cli/RushStartupBanner';
import { RushXCommandLine } from '../cli/RushXCommandLine';
Expand Down
12 changes: 12 additions & 0 deletions libraries/rush-lib/src/utilities/SetRushLibPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PackageJsonLookup } from '@rushstack/node-core-library';

import { EnvironmentVariableNames } from '../api/EnvironmentConfiguration';

if (!process.env[EnvironmentVariableNames.RUSH_LIB_PATH]) {
const rootDir: string | undefined = PackageJsonLookup.instance.tryGetPackageFolderFor(__dirname);
if (rootDir) {
// Route to the 'main' field of package.json
const rushLibIndex: string = require.resolve(rootDir, { paths: [] });
process.env[EnvironmentVariableNames.RUSH_LIB_PATH] = rushLibIndex;
}
}
8 changes: 5 additions & 3 deletions libraries/rush-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ The **@rushstack/rush-sdk** package acts as a lightweight proxy for accessing th

2. When authoring unit tests for a Rush plugin, developers should add **@microsoft/rush-lib** to their **package.json** `devDependencies`. In this context, **@rushstack/rush-sdk** will resolve to that instance for testing purposes.

3. For scripts and tools that are designed to be used in a Rush monorepo, in the future **@rushstack/rush-sdk** will automatically invoke **install-run-rush.js** and load the local installation. This ensures that tools load a compatible version of the Rush engine for the given branch. Once this is implemented, **@rushstack/rush-sdk** can replace **@microsoft/rush-lib** entirely as the official API interface, with the latter serving as the underlying implementation.
3. For projects within a monorepo that use **@rushstack/rush-sdk** during their build process, child processes will inherit the installation of Rush that invoked them. This is communicated using the `RUSH_LIB_PATH` environment variable.

4. For scripts and tools that are designed to be used in a Rush monorepo, in the future **@rushstack/rush-sdk** will automatically invoke **install-run-rush.js** and load the local installation. This ensures that tools load a compatible version of the Rush engine for the given branch. Once this is implemented, **@rushstack/rush-sdk** can replace **@microsoft/rush-lib** entirely as the official API interface, with the latter serving as the underlying implementation.

The **@rushstack/rush-sdk** API declarations are identical to the corresponding version of **@microsoft/rush-lib**.

Expand Down Expand Up @@ -42,8 +44,8 @@ Example 2: How to import an internal API:
// WARNING: INTERNAL APIS MAY CHANGE AT ANY TIME -- USE THIS AT YOUR OWN RISK:

// Important: Since we're calling an internal API, we need to use the unbundled .d.ts files
// instead of the normal .d.ts rollup
import { RushConfiguration } from '@rushstack/rush-sdk/lib/';
// instead of the normal .d.ts rollup, otherwise TypeScript will complain about a type mismatch.
import { RushConfiguration } from '@rushstack/rush-sdk/lib/index';
const config = RushConfiguration.loadFromDefaultLocation();
console.log(config.commonFolder);

Expand Down
33 changes: 30 additions & 3 deletions libraries/rush-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from '@rushstack/node-core-library';
import type { SpawnSyncReturns } from 'child_process';

const RUSH_LIB_NAME: string = '@microsoft/rush-lib';
const RUSH_LIB_NAME: '@microsoft/rush-lib' = '@microsoft/rush-lib';

const verboseEnabled: boolean = typeof process !== 'undefined' && process.env.RUSH_SDK_DEBUG === '1';
const terminal: Terminal = new Terminal(
Expand All @@ -28,6 +28,7 @@ type RushLibModuleType = Record<string, unknown>;
declare const global: NodeJS.Global &
typeof globalThis & {
___rush___rushLibModule?: RushLibModuleType;
___rush___rushLibModuleFromEnvironment?: RushLibModuleType;
___rush___rushLibModuleFromInstallAndRunRush?: RushLibModuleType;
};

Expand All @@ -46,7 +47,9 @@ function _require<TResult>(moduleName: string): TResult {
// SCENARIO 1: Rush's PluginManager has initialized "rush-sdk" with Rush's own instance of rush-lib.
// The Rush host process will assign "global.___rush___rushLibModule" before loading the plugin.
let rushLibModule: RushLibModuleType | undefined =
global.___rush___rushLibModule || global.___rush___rushLibModuleFromInstallAndRunRush;
global.___rush___rushLibModule ||
global.___rush___rushLibModuleFromEnvironment ||
global.___rush___rushLibModuleFromInstallAndRunRush;
let errorMessage: string = '';

// SCENARIO 2: The project importing "rush-sdk" has installed its own instance of "rush-lib"
Expand Down Expand Up @@ -89,7 +92,31 @@ if (rushLibModule === undefined) {
}
}

// SCENARIO 3: A tool or script depends on "rush-sdk", and is meant to be used inside a monorepo folder.
// SCENARIO 3: A tool or script has been invoked as a child process by an instance of "rush-lib" and can use the
// version that invoked it. In this case, use process.env.RUSH_LIB_PATH to find "rush-lib".
if (rushLibModule === undefined) {
const rushLibVariable: 'RUSH_LIB_PATH' = 'RUSH_LIB_PATH';
const rushLibPath: string | undefined = process.env[rushLibVariable];
if (rushLibPath) {
terminal.writeVerboseLine(
`Try to load ${RUSH_LIB_NAME} from process.env.${rushLibVariable} from caller package`
);
try {
rushLibModule = _require(rushLibPath);
} catch (error) {
// Log this as a warning, since it is unexpected to define an incorrect value of the variable.
terminal.writeWarningLine(`Failed to load ${RUSH_LIB_NAME} via process.env.${rushLibVariable}`);
}

if (rushLibModule !== undefined) {
// to track which scenario is active and how it got initialized.
global.___rush___rushLibModuleFromEnvironment = rushLibModule;
terminal.writeVerboseLine(`Loaded ${RUSH_LIB_NAME} from process.env.${rushLibVariable}`);
}
}
}

// SCENARIO 4: A standalone tool or script depends on "rush-sdk", and is meant to be used inside a monorepo folder.
// In this case, we can use install-run-rush.js to obtain the appropriate rush-lib version for the monorepo.
if (rushLibModule === undefined) {
try {
Expand Down
76 changes: 76 additions & 0 deletions libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`@rushstack/rush-sdk Should load via env when Rush has loaded (for child processes): stderr 1`] = `""`;

exports[`@rushstack/rush-sdk Should load via env when Rush has loaded (for child processes): stdout 1`] = `
"Try to load @microsoft/rush-lib from process.env.RUSH_LIB_PATH from caller package
Loaded @microsoft/rush-lib from process.env.RUSH_LIB_PATH
[
'_rushSdk_loadInternalModule',
'ApprovedPackagesConfiguration',
'ApprovedPackagesItem',
'ApprovedPackagesPolicy',
'BuildCacheConfiguration',
'BumpType',
'ChangeManager',
'CommonVersionsConfiguration',
'CredentialCache',
'DependencyType',
'EnvironmentConfiguration',
'EnvironmentVariableNames',
'Event',
'EventHooks',
'ExperimentsConfiguration',
'FileSystemBuildCacheProvider',
'IndividualVersionPolicy',
'LockStepVersionPolicy',
'LookupByPath',
'NpmOptionsConfiguration',
'Operation',
'OperationStatus',
'PackageJsonDependency',
'PackageJsonEditor',
'PackageManager',
'PackageManagerOptionsConfigurationBase',
'PhasedCommandHooks',
'PnpmOptionsConfiguration',
'ProjectChangeAnalyzer',
'RepoStateFile',
'Rush',
'RushConfiguration',
'RushConfigurationProject',
'RushConstants',
'RushLifecycleHooks',
'RushSession',
'RushUserConfiguration',
'VersionPolicy',
'VersionPolicyConfiguration',
'VersionPolicyDefinitionName',
'YarnOptionsConfiguration',
'_LastInstallFlag',
'_OperationMetadataManager',
'_OperationStateFile',
'_RushGlobalFolder',
'_RushInternals'
]"
`;

exports[`@rushstack/rush-sdk Should load via global (for plugins): stderr 1`] = `""`;

exports[`@rushstack/rush-sdk Should load via global (for plugins): stdout 1`] = `"[ '_rushSdk_loadInternalModule', 'foo' ]"`;

exports[`@rushstack/rush-sdk Should load via install-run (for standalone tools): stderr 1`] = `""`;

exports[`@rushstack/rush-sdk Should load via install-run (for standalone tools): stdout 1`] = `
"Trying to load @microsoft/rush-lib installed by install-run-rush
Loaded @microsoft/rush-lib installed by install-run-rush
[ '_rushSdk_loadInternalModule', 'foo' ]"
`;

exports[`@rushstack/rush-sdk Should load via process.env.RUSH_LIB_PATH (for child processes): stderr 1`] = `""`;

exports[`@rushstack/rush-sdk Should load via process.env.RUSH_LIB_PATH (for child processes): stdout 1`] = `
"Try to load @microsoft/rush-lib from process.env.RUSH_LIB_PATH from caller package
Loaded @microsoft/rush-lib from process.env.RUSH_LIB_PATH
[ '_rushSdk_loadInternalModule', 'foo' ]"
`;
86 changes: 75 additions & 11 deletions libraries/rush-sdk/src/test/script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,72 @@ const mockRushLibPath: string = `${__dirname}/fixture/mock-rush-lib.js`;

const coreLibPath: string = require.resolve('@rushstack/node-core-library');

describe('used in script', () => {
it('should work when used in script', () => {
describe('@rushstack/rush-sdk', () => {
it('Should load via global (for plugins)', () => {
const result = Executable.spawnSync(
'node',
[
'-e',
`
global.___rush___rushLibModule = { foo: 1 };
console.log(Object.keys(require(${JSON.stringify(rushSdkPath)})));`
],
{
currentWorkingDirectory: mockPackageFolder,
environment: {
...process.env,
RUSH_SDK_DEBUG: '1',
RUSH_LIB_PATH: '' // Need to clear if invoked via Rush
}
}
);
expect(result.stderr.trim()).toMatchSnapshot('stderr');
expect(result.stdout.trim()).toMatchSnapshot('stdout');
expect(result.status).toBe(0);
});

it('Should load via env when Rush has loaded (for child processes)', () => {
const result = Executable.spawnSync(
'node',
[
'-e',
`
require('@microsoft/rush-lib');
console.log(Object.keys(require(${JSON.stringify(rushSdkPath)})));`
],
{
currentWorkingDirectory: mockPackageFolder,
environment: {
...process.env,
RUSH_SDK_DEBUG: '1',
RUSH_LIB_PATH: '' // Need to clear if invoked via Rush
}
}
);
expect(result.stderr.trim()).toMatchSnapshot('stderr');
expect(result.stdout.trim()).toMatchSnapshot('stdout');
expect(result.status).toBe(0);
});

it('Should load via process.env.RUSH_LIB_PATH (for child processes)', () => {
const result = Executable.spawnSync(
'node',
['-e', `console.log(Object.keys(require(${JSON.stringify(rushSdkPath)})));`],
{
currentWorkingDirectory: mockPackageFolder,
environment: {
...process.env,
RUSH_SDK_DEBUG: '1',
RUSH_LIB_PATH: mockRushLibPath
}
}
);
expect(result.stderr.trim()).toMatchSnapshot('stderr');
expect(result.stdout.trim()).toMatchSnapshot('stdout');
expect(result.status).toBe(0);
});

it('Should load via install-run (for standalone tools)', () => {
const result = Executable.spawnSync(
'node',
[
Expand All @@ -24,20 +88,20 @@ const mockResolveModule = (options) => {
return originalResolveModule(options);
}
Import.resolveModule = mockResolveModule;
console.log(require(${JSON.stringify(rushSdkPath)}));
console.log(Object.keys(require(${JSON.stringify(rushSdkPath)})));
`
],
{
currentWorkingDirectory: mockPackageFolder
currentWorkingDirectory: mockPackageFolder,
environment: {
...process.env,
RUSH_SDK_DEBUG: '1',
RUSH_LIB_PATH: '' // Need to clear if invoked via Rush
}
}
);
expect(result.stderr.trim()).toMatchInlineSnapshot(`""`);
expect(result.stdout.trim()).toMatchInlineSnapshot(`
"{
_rushSdk_loadInternalModule: [Function: _rushSdk_loadInternalModule],
foo: [Getter]
}"
`);
expect(result.stderr.trim()).toMatchSnapshot('stderr');
expect(result.stdout.trim()).toMatchSnapshot('stdout');
expect(result.status).toBe(0);
});
});

0 comments on commit 838c4ae

Please sign in to comment.