Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rush] Subspace Configuration Files #4442

Merged
merged 35 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0239c70
feat: add subspace modifications to RushConfiguration
william2958 Dec 1, 2023
b004a66
feat: add subspace configuration file
william2958 Dec 2, 2023
1bd9b71
feat: add subspace configuration loadFromDefaultLocation
william2958 Dec 4, 2023
d919129
chore: linting
william2958 Dec 4, 2023
3b755ee
chore: changefile
william2958 Dec 4, 2023
2fbdc7d
chore: fix documentation
william2958 Dec 4, 2023
2d53710
chore: documentation and PR comments
william2958 Dec 4, 2023
8f91e8a
feat: add subspace json template to rush init
william2958 Dec 4, 2023
d0370eb
chore: update description
william2958 Dec 4, 2023
c27c5ae
feat: clean up loading of subspace configuration
william2958 Dec 4, 2023
47e3e89
Clean up subspace configuration for PR
william2958 Dec 5, 2023
8c8b539
feat: update CommandLineHelp snapshot
william2958 Dec 5, 2023
b7e1afd
chore: update rush-sdk snapshot
william2958 Dec 5, 2023
8df0788
Update libraries/rush-lib/src/api/RushConfiguration.ts
william2958 Dec 5, 2023
306ee40
Update libraries/rush-lib/src/api/RushConfiguration.ts
william2958 Dec 5, 2023
d2ed1f2
chore: use official link for subspace schema
william2958 Dec 5, 2023
5b1b623
chore: update rush lib api signature
william2958 Dec 5, 2023
1705bc6
Update libraries/rush-lib/src/api/RushConfiguration.ts
william2958 Dec 5, 2023
bdf0c9e
chore: syntax
william2958 Dec 6, 2023
cf056e8
chore: refactor to export subspace configuration interface
william2958 Dec 7, 2023
43b274f
Update libraries/rush-lib/assets/rush-init/subspaces.json
william2958 Dec 7, 2023
b5d58c6
Update libraries/rush-lib/src/api/RushConfiguration.ts
william2958 Dec 7, 2023
8f65295
Update libraries/rush-lib/src/api/RushConfiguration.ts
william2958 Dec 7, 2023
8d42b59
Update libraries/rush-lib/src/api/SubspaceConfiguration.ts
william2958 Dec 7, 2023
d81881c
Update common/changes/@microsoft/rush/will-intro-subspace_2023-12-04-…
william2958 Dec 7, 2023
bf31ea6
Update libraries/rush-lib/src/schemas/rush.schema.json
william2958 Dec 7, 2023
0aa57a4
Update libraries/rush-lib/src/schemas/subspaces.schema.json
william2958 Dec 7, 2023
c8e85bf
PR comments
william2958 Dec 7, 2023
fa0c483
PR comments
william2958 Dec 7, 2023
64f1628
chore: add check to verify hydration of subspace names set
william2958 Dec 7, 2023
db32b88
rename _cachedRushProjectsBySubspaceName to _rushProjectsBySubspaceName
william2958 Dec 7, 2023
67c0c5f
Update libraries/rush-lib/src/api/RushConfiguration.ts
william2958 Dec 8, 2023
bf2cd20
Update libraries/rush-lib/src/api/RushConfiguration.ts
william2958 Dec 8, 2023
7184e21
PR comments
william2958 Dec 8, 2023
f7f4cfd
Update some config file docs; mark new API's as "@beta"
octogonz Dec 8, 2023
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 subspace configuration file structure",
william2958 marked this conversation as resolved.
Show resolved Hide resolved
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
9 changes: 9 additions & 0 deletions common/reviews/api/rush-lib.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,7 @@ export class RushConfiguration {
readonly commonRushConfigFolder: string;
readonly commonScriptsFolder: string;
readonly commonTempFolder: string;
readonly commonTempSubspaceFolderRoot: string;
// @deprecated
get commonVersions(): CommonVersionsConfiguration;
get currentInstalledVariant(): string | undefined;
Expand Down Expand Up @@ -1082,6 +1083,7 @@ export class RushConfiguration {
readonly gitSampleEmail: string;
readonly gitTagSeparator: string | undefined;
readonly gitVersionBumpCommitMessage: string | undefined;
get hasSubspaces(): boolean;
readonly hotfixChangeEnabled: boolean;
static loadFromConfigurationFile(rushJsonFilename: string): RushConfiguration;
// (undocumented)
Expand Down Expand Up @@ -1122,11 +1124,17 @@ export class RushConfiguration {
readonly _rushPluginsConfiguration: RushPluginsConfiguration;
readonly shrinkwrapFilename: string;
get shrinkwrapFilePhrase(): string;
// Warning: (ae-forgotten-export) The symbol "SubspaceConfiguration" needs to be exported by the entry point index.d.ts
william2958 marked this conversation as resolved.
Show resolved Hide resolved
readonly subspaceConfiguration?: SubspaceConfiguration;
// (undocumented)
get subspaceNames(): string[];
readonly subspaceShrinkwrapFilenames: (subspaceName: string) => string;
readonly suppressNodeLtsWarning: boolean;
// @beta
readonly telemetryEnabled: boolean;
readonly tempShrinkwrapFilename: string;
readonly tempShrinkwrapPreinstallFilename: string;
readonly tempSubspaceShrinkwrapFileName: (subspaceName: string) => string;
static tryFindRushJsonLocation(options?: ITryFindRushJsonLocationOptions): string | undefined;
tryGetProjectForPath(currentFolderPath: string): RushConfigurationProject | undefined;
// (undocumented)
Expand Down Expand Up @@ -1169,6 +1177,7 @@ export class RushConfigurationProject {
readonly rushConfiguration: RushConfiguration;
get shouldPublish(): boolean;
readonly skipRushCheck: boolean;
readonly subspace?: string;
// @beta
readonly tags: ReadonlySet<string>;
readonly tempProjectName: string;
Expand Down
109 changes: 108 additions & 1 deletion libraries/rush-lib/src/api/RushConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import schemaJson from '../schemas/rush.schema.json';
import type * as DependencyAnalyzerModuleType from '../logic/DependencyAnalyzer';
import type { PackageManagerOptionsConfigurationBase } from '../logic/base/BasePackageManagerOptionsConfiguration';
import { CustomTipsConfiguration } from './CustomTipsConfiguration';
import { SubspaceConfiguration } from './SubspaceConfiguration';

const MINIMUM_SUPPORTED_RUSH_JSON_VERSION: string = '0.0.0';
const DEFAULT_BRANCH: string = 'main';
Expand Down Expand Up @@ -186,6 +187,16 @@ export interface ICurrentVariantJson {
variant: string | JsonNull;
}

/**
* The filter parameters to search from all projects
*/
export interface IRushConfigurationProjectsFilter {
/**
* A string representation of the subspace to filter for
*/
subspace: string;
}

/**
* Options for `RushConfiguration.tryFindRushJsonLocation`.
* @public
Expand Down Expand Up @@ -222,6 +233,11 @@ export class RushConfiguration {
// Lazily loaded when the projectsByTag() getter is called.
private _projectsByTag: ReadonlyMap<string, ReadonlySet<RushConfigurationProject>> | undefined;

// Cache subspace projects
william2958 marked this conversation as resolved.
Show resolved Hide resolved
private _subspaceProjectsCache: Map<string, RushConfigurationProject[]>;
william2958 marked this conversation as resolved.
Show resolved Hide resolved

private _hasSubspaces: boolean | undefined;

// variant -> common-versions configuration
private _commonVersionsConfigurationsByVariant: Map<string, CommonVersionsConfiguration> | undefined;

Expand Down Expand Up @@ -287,6 +303,14 @@ export class RushConfiguration {
*/
public readonly commonTempFolder: string;

/**
* The folder where temporary files will be stored for subspaces. The specific folder will
* append the subspace name to this path.
*
* Example: `C:\MyRepo\common\temp\<subspace_name>`
william2958 marked this conversation as resolved.
Show resolved Hide resolved
*/
public readonly commonTempSubspaceFolderRoot: string;

/**
* The folder where automation scripts are stored. This is always a subfolder called "scripts"
* under the common folder.
Expand Down Expand Up @@ -348,6 +372,30 @@ export class RushConfiguration {
*/
public readonly tempShrinkwrapPreinstallFilename: string;

/**
* The object that specifies subspace configurations if they are provided in the rush workspace.
*/
public readonly subspaceConfiguration?: SubspaceConfiguration;

/**
* The filename (without any path) of the shrinkwrap file used for individual subspaces, used by the package manager.
* @remarks
* This property merely reports the filename; The file itself may not actually exist.
* Example: `pnpm-lock.yaml`
*/
public readonly subspaceShrinkwrapFilenames: (subspaceName: string) => string;

/**
* The full path of the temporary shrinkwrap file for a specific subspace.
* This function takes the subspace name, and returns the full path for the subspace's shrinkwrap file.
* This function also consults the depreciated option to allow for shrinkwraps to be stored under a package folder.
* This shrinkwrap file is used during "rush install", and may be rewritten by the package manager during installation
* @remarks
* This property merely reports the filename, the file itself may not actually exist.
* example: `C:\MyRepo\common\<subspace_name>\pnpm-lock.yaml`
*/
public readonly tempSubspaceShrinkwrapFileName: (subspaceName: string) => string;
william2958 marked this conversation as resolved.
Show resolved Hide resolved

/**
* The filename of the variant dependency data file. By default this is
* called 'current-variant.json' resides in the Rush common folder.
Expand Down Expand Up @@ -608,6 +656,8 @@ export class RushConfiguration {
EnvironmentConfiguration.rushTempFolderOverride ||
path.join(this.commonFolder, RushConstants.rushTempFolderName);

this.commonTempSubspaceFolderRoot = path.join(this.commonFolder, RushConstants.rushTempFolderName);

this.commonScriptsFolder = path.join(this.commonFolder, 'scripts');

this.npmCacheFolder = path.resolve(path.join(this.commonTempFolder, 'npm-cache'));
Expand All @@ -622,6 +672,15 @@ export class RushConfiguration {

this.ensureConsistentVersions = !!rushConfigurationJson.ensureConsistentVersions;

// Check if we have a subspace configuration file
const subspaceConfigLocation: string = path.join(this.rushJsonFolder, 'subspaces.json');
if (FileSystem.exists(subspaceConfigLocation)) {
// Try getting a subspace configuration
this.subspaceConfiguration = SubspaceConfiguration.loadFromConfigurationFile(subspaceConfigLocation);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rename loadFromConfigurationFile to tryLoadFromConfigurationFile and just return undefined if the file doesn't exist?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And ideally this load should be async.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to tryLoadFromConfigurationFile

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be made async?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(If the other similar Rush functions are not already async, then we shouldn't insist that Will fixes it in this PR.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused as to why this would be async - it only uses a call to FileSystem.exists, which as far as I can tell is a synchronous action, so if there are no asynchronous calls within the tryLoadFromConfigurationFile function, what purpose would labelling it async be?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a FileSystem.existsAsync, and there is a JsonFile.loadAndValidateAsync. Anything that touches the filesystem (including JSON file loading) should ideally be async to allow those calls to be parallelized.


this._subspaceProjectsCache = new Map<string, RushConfigurationProject[]>();

const experimentsConfigFile: string = path.join(
this.commonRushConfigFolder,
RushConstants.experimentsFilename
Expand Down Expand Up @@ -703,6 +762,15 @@ export class RushConfiguration {

this.shrinkwrapFilename = this.packageManagerWrapper.shrinkwrapFilename;

// From "pnpm-lock.yaml" --> "subspace-pnpm-lock.yaml"
this.subspaceShrinkwrapFilenames = (subspaceName: string): string => {
const shrinkwrapFilenameParsedPath: path.ParsedPath = path.parse(this.shrinkwrapFilename);
return path.join(
shrinkwrapFilenameParsedPath.dir,
`${subspaceName}-` + shrinkwrapFilenameParsedPath.name + shrinkwrapFilenameParsedPath.ext
);
william2958 marked this conversation as resolved.
Show resolved Hide resolved
};

this.tempShrinkwrapFilename = path.join(this.commonTempFolder, this.shrinkwrapFilename);
this.packageManagerToolFilename = path.resolve(
path.join(
Expand All @@ -714,6 +782,16 @@ export class RushConfiguration {
)
);

this.tempSubspaceShrinkwrapFileName = (subspaceName: string): string => {
william2958 marked this conversation as resolved.
Show resolved Hide resolved
// TODO: do subspace name validation here
const fullSubspacePath: string = path.join(
this.commonTempSubspaceFolderRoot,
subspaceName,
this.shrinkwrapFilename
);
william2958 marked this conversation as resolved.
Show resolved Hide resolved
return fullSubspacePath;
};

/// From "C:\repo\common\temp\pnpm-lock.yaml" --> "C:\repo\common\temp\pnpm-lock-preinstall.yaml"
const parsedPath: path.ParsedPath = path.parse(this.tempShrinkwrapFilename);
this.tempShrinkwrapPreinstallFilename = path.join(
Expand Down Expand Up @@ -880,6 +958,14 @@ export class RushConfiguration {
);
}
this._projectsByName.set(project.packageName, project);
if (projectJson.subspace) {
const subspaceName: string = projectJson.subspace;
if (this._subspaceProjectsCache.has(subspaceName)) {
(this._subspaceProjectsCache.get(subspaceName) as RushConfigurationProject[]).push(project);
} else {
this._subspaceProjectsCache.set(subspaceName, [project]);
}
william2958 marked this conversation as resolved.
Show resolved Hide resolved
}
}

for (const project of this._projects) {
Expand Down Expand Up @@ -1091,7 +1177,11 @@ export class RushConfiguration {

// If the package manager is pnpm, then also add the pnpm file to the known set.
if (packageManagerWrapper.packageManager === 'pnpm') {
knownSet.add((packageManagerWrapper as PnpmPackageManager).pnpmfileFilename.toUpperCase());
const pnpmPackageManager: PnpmPackageManager = packageManagerWrapper as PnpmPackageManager;
knownSet.add(pnpmPackageManager.pnpmfileFilename.toUpperCase());
// for (const subspaceName of this.subspaceNames()) {
// knownSet.add(pnpmPackageManager.subspacePnpmfileFilename(subspaceName).toUpperCase());
// }
}

// Is the filename something we know? If not, report an error.
Expand Down Expand Up @@ -1188,6 +1278,13 @@ export class RushConfiguration {
return this._projects!;
}

public get subspaceNames(): string[] {
if (!this._projects) {
this._initializeAndValidateLocalProjects();
}
return Array.from(this._subspaceProjectsCache.keys());
}
william2958 marked this conversation as resolved.
Show resolved Hide resolved

public get projectsByName(): Map<string, RushConfigurationProject> {
if (!this._projectsByName) {
this._initializeAndValidateLocalProjects();
Expand Down Expand Up @@ -1262,6 +1359,16 @@ export class RushConfiguration {
return commonVersionsFilename;
}

/**
* Does this project have subspaces
*/
public get hasSubspaces(): boolean {
if (undefined === this._hasSubspaces) {
this._hasSubspaces = this._subspaceProjectsCache.size > 0;
}
return this._hasSubspaces;
william2958 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Gets the settings from the common-versions.json config file for a specific variant.
* @param variant - The name of the current variant in use by the active command.
Expand Down
8 changes: 8 additions & 0 deletions libraries/rush-lib/src/api/RushConfigurationProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface IRushConfigurationProjectJson {
skipRushCheck?: boolean;
publishFolder?: string;
tags?: string[];
subspace?: string;
}

/**
Expand Down Expand Up @@ -184,6 +185,11 @@ export class RushConfigurationProject {
*/
public readonly tags: ReadonlySet<string>;

/**
* If this project is in a subspace, and which one
*/
public readonly subspace?: string;
william2958 marked this conversation as resolved.
Show resolved Hide resolved

/** @internal */
public constructor(options: IRushConfigurationProjectOptions) {
const { projectJson, rushConfiguration, tempProjectName, allowedProjectTags } = options;
Expand Down Expand Up @@ -324,6 +330,8 @@ export class RushConfigurationProject {
} else {
this.tags = new Set(projectJson.tags);
}

this.subspace = projectJson.subspace;
}

/**
Expand Down
73 changes: 73 additions & 0 deletions libraries/rush-lib/src/api/SubspaceConfiguration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { JsonFile } from '@rushstack/node-core-library';
import path from 'path';
import { trueCasePathSync } from 'true-case-path';
import { RushConfiguration } from './RushConfiguration';

export interface ISubspaceConfig {
subspaceName: string;
}

/**
* This represents the JSON data structure for the "subspace.json" configuration file.
* See subspace.schema.json for documentation.
*/
export interface ISubspaceConfigurationJson {
$schema: string;
enabled: boolean;
depreciatedTTSupport?: boolean;
william2958 marked this conversation as resolved.
Show resolved Hide resolved
availableSubspaces: ISubspaceConfig;
}

export class SubspaceConfiguration {
/**
* The absolute path to the "subspace.json" configuration file that was loaded to construct this object.
*/
public readonly subspaceJsonFile: string;
william2958 marked this conversation as resolved.
Show resolved Hide resolved

/**
* Gets the JSON data structure for the "subspace.json" configuration file.
*
* @internal
*/
public readonly subspaceConfigurationJson: ISubspaceConfigurationJson;

/**
* A set of the available subspaces
*/
public readonly availableSubspaceSet: Set<string>;
william2958 marked this conversation as resolved.
Show resolved Hide resolved

private constructor(subspaceConfigurationJson: ISubspaceConfigurationJson, subspaceJsonFilename: string) {
this.subspaceConfigurationJson = subspaceConfigurationJson;
this.subspaceJsonFile = subspaceJsonFilename;
this.availableSubspaceSet = new Set();

for (const { subspaceName } of Object.values(subspaceConfigurationJson.availableSubspaces)) {
this.availableSubspaceSet.add(subspaceName);
william2958 marked this conversation as resolved.
Show resolved Hide resolved
}
}

public static loadFromConfigurationFile(subspaceJsonFilename: string): SubspaceConfiguration {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally all of the things that touch the filesystem should be async.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function only calls FileSystem.exists, which is a synchronous function - does it still need to be labelled async?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iclanton RushConfiguration.tryLoadFromDefaultLocation() itself is synchronous. Synchronous functions cannot call async functions. Therefore I think we may be asking @william2958 to do nontrivial work that is out of scope for this PR.

let resolvedSubspaceJsonFilename: string = path.resolve(subspaceJsonFilename);
william2958 marked this conversation as resolved.
Show resolved Hide resolved

const subspaceConfigurationJson: ISubspaceConfigurationJson = JsonFile.load(resolvedSubspaceJsonFilename);

try {
resolvedSubspaceJsonFilename = trueCasePathSync(resolvedSubspaceJsonFilename);
william2958 marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
/* ignore errors from true-case-path */
Copy link
Collaborator

@octogonz octogonz Dec 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discarding every possible error is always a suspicious practice.

Looking up the implementation in true-case-path/index.js, there are actually 3 separate errors that it can throw (aside from indirect errors such as Cannot read properties of undefined):

  • [true-case-path]: Called with ${filePath}, but no matching file exists
  • [true-case-path]: basePath argument must be absolute. Received "${basePath}"
  • [true-case-path]: filePath must be relative when used with basePath

The first one is probably what we're hoping to catch, whereas inadvertently catching the others would probably hide a bug.

I found /* ignore errors from true-case-path */ in two other places in the Rush code base, so probably @william2958 is simply copying that practice. Thus addressing this is out of scope for this PR.

But in the future maybe someone should move true-case-path into a library function and invent a more correct solution for nonexistent files.

@iclanton @dmichon-msft

}

return new SubspaceConfiguration(subspaceConfigurationJson, resolvedSubspaceJsonFilename);
}

public static loadFromDefaultLocation(): SubspaceConfiguration | undefined {
const rushJsonLocation: string | undefined = RushConfiguration.tryFindRushJsonLocation();
william2958 marked this conversation as resolved.
Show resolved Hide resolved
if (rushJsonLocation) {
const subspaceJsonLocation: string = path.join(path.dirname(rushJsonLocation), 'subspace.json');
return SubspaceConfiguration.loadFromConfigurationFile(subspaceJsonLocation);
}
}
}
12 changes: 12 additions & 0 deletions libraries/rush-lib/src/api/packageManager/PnpmPackageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ export class PnpmPackageManager extends PackageManager {
*/
public readonly pnpmfileFilename: string;

/**
* The filename of the shrinkwrap file of a subspace that is used by the package manager
*
* @remarks
* Example: `.pnpmfile-<subspace_name>.cjs
*/
public subspacePnpmfileFilename: (subspaceName: string) => string;
william2958 marked this conversation as resolved.
Show resolved Hide resolved

/** @internal */
public constructor(version: string) {
super(version, 'pnpm', RushConstants.pnpmV3ShrinkwrapFilename);
Expand All @@ -35,6 +43,10 @@ export class PnpmPackageManager extends PackageManager {
this.pnpmfileFilename = RushConstants.pnpmfileV1Filename;
}

this.subspacePnpmfileFilename = (subspaceName: string) => {
return `.pnpmfile-${subspaceName}.cjs`;
william2958 marked this conversation as resolved.
Show resolved Hide resolved
};

// node_modules/.pnpm/lock.yaml
// See https://github.com/pnpm/pnpm/releases/tag/v4.0.0 for more details.
this.internalShrinkwrapRelativePath = path.join('node_modules', '.pnpm', 'lock.yaml');
Expand Down
4 changes: 4 additions & 0 deletions libraries/rush-lib/src/schemas/rush.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,10 @@
"type": "string",
"pattern": "^[a-z0-9.@]+([-/][a-z0-9.@]+)*$"
}
},
"subspace": {
"description": "An optional entry for specifying which subspace this project belongs to if the subspaces feature is enabled.",
william2958 marked this conversation as resolved.
Show resolved Hide resolved
"type": "string"
}
},
"additionalProperties": false,
Expand Down
Loading