Skip to content

Commit fc5daa5

Browse files
authored
Merge pull request #4270 from microsoft/octogonz/rush-sdk-proxy
[rush-sdk] Introduce a proxy API for customizing how rush-lib is loaded
2 parents afe6e69 + ab411e3 commit fc5daa5

File tree

14 files changed

+593
-100
lines changed

14 files changed

+593
-100
lines changed

build-tests/install-test-workspace/workspace/common/pnpm-lock.yaml

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@microsoft/rush",
5+
"comment": "@rushstack/rush-sdk now exposes a secondary API for manually loading the Rush engine and monitoring installation progress",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush"
10+
}

common/config/rush/pnpm-lock.yaml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/reviews/api/rush-sdk.api.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
## API Report File for "@rushstack/rush-sdk"
2+
3+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4+
5+
```ts
6+
7+
/// <reference types="node" />
8+
9+
// @public
10+
export interface ILoadSdkAsyncOptions {
11+
abortSignal?: AbortSignal;
12+
onNotifyEvent?: SdkNotifyEventCallback;
13+
rushJsonSearchFolder?: string;
14+
}
15+
16+
// @public
17+
export interface IProgressBarCallbackLogMessage {
18+
kind: 'info' | 'debug';
19+
text: string;
20+
}
21+
22+
// @public
23+
export interface ISdkCallbackEvent {
24+
logMessage: IProgressBarCallbackLogMessage | undefined;
25+
progressPercent: number | undefined;
26+
}
27+
28+
// @public
29+
export class RushSdkLoader {
30+
static get isLoaded(): boolean;
31+
static loadAsync(options?: ILoadSdkAsyncOptions): Promise<void>;
32+
}
33+
34+
// @public
35+
export type SdkNotifyEventCallback = (sdkEvent: ISdkCallbackEvent) => void;
36+
37+
// (No @packageDocumentation comment for this package)
38+
39+
```

libraries/rush-sdk/README.md

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,110 @@ This is a companion package for the Rush tool. See the [@microsoft/rush](https:/
44

55
**_THIS PACKAGE IS EXPERIMENTAL_**
66

7-
The **@rushstack/rush-sdk** package acts as a lightweight proxy for accessing the APIs of the **@microsoft/rush-lib** engine. It is intended to support three different use cases:
7+
The **@rushstack/rush-sdk** package acts as a lightweight proxy for accessing the APIs of the **@microsoft/rush-lib** engine. It is intended to support five different use cases:
88

9-
1. Rush plugins should import from **@rushstack/rush-sdk** instead of **@microsoft/rush-lib**. This gives plugins full access to Rush APIs while avoiding a redundant installation of those packages. At runtime, the APIs will be bound to the correct `rushVersion` from **rush.json**, and guaranteed to be the same **@microsoft/rush-lib** module instance as the plugin host.
9+
1. **Rush plugins:** Rush plugins should import from **@rushstack/rush-sdk** instead of **@microsoft/rush-lib**. This gives plugins full access to Rush APIs while avoiding a redundant installation of those packages. At runtime, the APIs will be bound to the correct `rushVersion` from **rush.json**, and guaranteed to be the same **@microsoft/rush-lib** module instance as the plugin host.
1010

11-
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.
11+
2. **Unit tests:** When authoring unit tests (for a Rush plugin, for example), developers should add **@microsoft/rush-lib** to their **package.json** `devDependencies` and add **@rushstack/rush-sdk** to the regular `dependencies`. In this context, **@rushstack/rush-sdk** will resolve to the locally installed instance for testing purposes.
1212

13-
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.
13+
3. **Rush subprocesses:** For tools within a monorepo that import **@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.
1414

15-
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.
15+
4. **Monorepo tools:** For scripts and tools that are designed to be used in a Rush monorepo, **@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.
16+
17+
5. **Advanced scenarios:** The secondary `@rushstack/rush-sdk/loader` entry point can be imported by tools that need to explicitly control where **@microsoft/rush-lib** gets loaded from. This API also allows monitoring installation and canceling the operation. This API is used by the Rush Stack VS Code extension, for example.
1618

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

21+
## Basic usage
22+
23+
Here's an example of basic usage that works with cases 1-4 above:
24+
25+
```ts
26+
// CommonJS notation:
27+
const { RushConfiguration } = require('@rushstack/rush-sdk');
28+
29+
const config = RushConfiguration.loadFromDefaultLocation();
30+
console.log(config.commonFolder);
31+
```
32+
33+
```ts
34+
// TypeScript notation:
35+
import { RushConfiguration } from '@rushstack/rush-sdk';
36+
37+
const config = RushConfiguration.loadFromDefaultLocation();
38+
console.log(config.commonFolder);
39+
```
40+
41+
## Loader API
42+
43+
Here's a basic example of how to manually load **@rushstack/rush-sdk** and monitor installation progress:
44+
45+
```ts
46+
import { RushSdkLoader, ISdkCallbackEvent } from '@rushstack/rush-sdk/loader';
47+
48+
if (!RushSdkLoader.isLoaded) {
49+
await RushSdkLoader.loadAsync({
50+
// the search for rush.json starts here:
51+
rushJsonSearchFolder: "path/to/my-repo/apps/my-app",
52+
53+
onNotifyEvent: (event: ISdkCallbackEvent) => {
54+
if (event.logMessage) {
55+
// Your tool can show progress about the loading:
56+
if (event.logMessage.kind === 'info') {
57+
console.log(event.logMessage.text);
58+
}
59+
}
60+
}
61+
});
62+
}
63+
64+
// Any subsequent attempts to call require() will return the same instance
65+
// that was loaded above.
66+
const rushSdk = require('@rushstack/rush-sdk');
67+
const config = rushSdk.RushConfiguration.loadFromDefaultLocation();
68+
```
69+
70+
Here's a more elaborate example illustrating other API features:
71+
72+
```ts
73+
import { RushSdkLoader, ISdkCallbackEvent } from '@rushstack/rush-sdk/loader';
74+
75+
// Use an AbortController to cancel the operation after a certain time period
76+
const abortController = new AbortController();
77+
setTimeout(() => {
78+
abortController.abort();
79+
}, 1000);
80+
81+
if (!RushSdkLoader.isLoaded) {
82+
await RushSdkLoader.loadAsync({
83+
// the search for rush.json starts here:
84+
rushJsonSearchFolder: "path/to/my-repo/apps/my-app",
85+
86+
abortSignal: abortController.signal,
87+
88+
onNotifyEvent: (event: ISdkCallbackEvent) => {
89+
if (event.logMessage) {
90+
// Your tool can show progress about the loading:
91+
if (event.logMessage.kind === 'info') {
92+
console.log(event.logMessage.text);
93+
}
94+
}
95+
96+
if (event.progressPercent !== undefined) {
97+
// If installation takes a long time, your tool can display a progress bar
98+
displayYourProgressBar(event.progressPercent);
99+
}
100+
}
101+
});
102+
}
103+
104+
// Any subsequent attempts to call require() will return the same instance
105+
// that was loaded above.
106+
const rushSdk = require('@rushstack/rush-sdk');
107+
const config = rushSdk.RushConfiguration.loadFromDefaultLocation();
108+
```
109+
110+
19111
## Importing internal APIs
20112

21113
Backwards compatibility is only guaranteed for the APIs marked as `@public` in the official `rush-lib.d.ts` entry point.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
3+
4+
"mainEntryPointFilePath": "<projectFolder>/lib-shim/loader.d.ts",
5+
6+
"apiReport": {
7+
"enabled": true,
8+
"reportFolder": "../../../common/reviews/api"
9+
},
10+
11+
"docModel": {
12+
"enabled": false,
13+
"apiJsonFilePath": "../../../common/temp/api/<unscopedPackageName>.api.json"
14+
},
15+
16+
"dtsRollup": {
17+
"enabled": true,
18+
"publicTrimmedFilePath": "<projectFolder>/dist/loader.d.ts"
19+
}
20+
}

libraries/rush-sdk/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@
1010
"homepage": "https://rushjs.io",
1111
"main": "lib-shim/index.js",
1212
"typings": "dist/rush-lib.d.ts",
13+
"exports": {
14+
".": "./lib-shim/index.js",
15+
"./loader": "./lib-shim/loader.js"
16+
},
17+
"typesVersions": {
18+
"*": {
19+
"loader": [
20+
"./dist/loader.d.ts"
21+
]
22+
}
23+
},
1324
"scripts": {
1425
"build": "heft build --clean",
1526
"_phase:build": "heft run --only build -- --clean",
@@ -29,6 +40,7 @@
2940
"@rushstack/stream-collator": "workspace:*",
3041
"@rushstack/ts-command-line": "workspace:*",
3142
"@rushstack/terminal": "workspace:*",
43+
"@types/node": "14.18.36",
3244
"@types/semver": "7.5.0",
3345
"@types/webpack-env": "1.18.0"
3446
}

libraries/rush-sdk/src/helpers.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import * as path from 'path';
5+
import { Import, FileSystem } from '@rushstack/node-core-library';
6+
import type { EnvironmentVariableNames } from '@microsoft/rush-lib';
7+
8+
export const RUSH_LIB_NAME: '@microsoft/rush-lib' = '@microsoft/rush-lib';
9+
export const RUSH_LIB_PATH_ENV_VAR_NAME: typeof EnvironmentVariableNames.RUSH_LIB_PATH = '_RUSH_LIB_PATH';
10+
11+
export type RushLibModuleType = Record<string, unknown>;
12+
13+
export interface ISdkContext {
14+
rushLibModule: RushLibModuleType | undefined;
15+
}
16+
17+
export const sdkContext: ISdkContext = {
18+
rushLibModule: undefined
19+
};
20+
21+
/**
22+
* Find the rush.json location and return the path, or undefined if a rush.json can't be found.
23+
*
24+
* @privateRemarks
25+
* Keep this in sync with `RushConfiguration.tryFindRushJsonLocation`.
26+
*/
27+
export function tryFindRushJsonLocation(startingFolder: string): string | undefined {
28+
let currentFolder: string = startingFolder;
29+
30+
// Look upwards at parent folders until we find a folder containing rush.json
31+
for (let i: number = 0; i < 10; ++i) {
32+
const rushJsonFilename: string = path.join(currentFolder, 'rush.json');
33+
34+
if (FileSystem.exists(rushJsonFilename)) {
35+
return rushJsonFilename;
36+
}
37+
38+
const parentFolder: string = path.dirname(currentFolder);
39+
if (parentFolder === currentFolder) {
40+
break;
41+
}
42+
43+
currentFolder = parentFolder;
44+
}
45+
46+
return undefined;
47+
}
48+
49+
export function _require<TResult>(moduleName: string): TResult {
50+
if (typeof __non_webpack_require__ === 'function') {
51+
// If this library has been bundled with Webpack, we need to call the real `require` function
52+
// that doesn't get turned into a `__webpack_require__` statement.
53+
// `__non_webpack_require__` is a Webpack macro that gets turned into a `require` statement
54+
// during bundling.
55+
return __non_webpack_require__(moduleName);
56+
} else {
57+
return require(moduleName);
58+
}
59+
}
60+
61+
/**
62+
* Require `@microsoft/rush-lib` under the specified folder path.
63+
*/
64+
export function requireRushLibUnderFolderPath(folderPath: string): RushLibModuleType {
65+
const rushLibModulePath: string = Import.resolveModule({
66+
modulePath: RUSH_LIB_NAME,
67+
baseFolderPath: folderPath
68+
});
69+
70+
return _require(rushLibModulePath);
71+
}

0 commit comments

Comments
 (0)