diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7cfeeb2c71..216794a5366 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,10 +18,10 @@ jobs: - NodeVersion: 20.18.x NodeVersionDisplayName: 20 OS: ubuntu-latest - - NodeVersion: 22.12.x + - NodeVersion: 22.19.x NodeVersionDisplayName: 22 OS: ubuntu-latest - - NodeVersion: 22.12.x + - NodeVersion: 22.19.x NodeVersionDisplayName: 22 OS: windows-latest name: Node.js v${{ matrix.NodeVersionDisplayName }} (${{ matrix.OS }}) @@ -95,3 +95,7 @@ jobs: - name: Rush test (rush-lib) run: node ${{ github.workspace }}/repo-a/apps/rush/lib/start-dev.js test --verbose --production --timeline working-directory: repo-b + + - name: Rush test (rush-lib) again to verify build cache hits + run: node ${{ github.workspace }}/repo-a/apps/rush/lib/start-dev.js test --verbose --production --timeline + working-directory: repo-b diff --git a/apps/lockfile-explorer-web/package.json b/apps/lockfile-explorer-web/package.json index 59923434733..bc10ae7500c 100644 --- a/apps/lockfile-explorer-web/package.json +++ b/apps/lockfile-explorer-web/package.json @@ -14,6 +14,7 @@ "dependencies": { "@reduxjs/toolkit": "~1.8.6", "@rushstack/rush-themed-ui": "workspace:*", + "prism-react-renderer": "~2.4.1", "react-dom": "~17.0.2", "react-redux": "~8.0.4", "react": "~17.0.2", diff --git a/apps/lockfile-explorer-web/src/containers/PackageJsonViewer/CodeBox.tsx b/apps/lockfile-explorer-web/src/containers/PackageJsonViewer/CodeBox.tsx new file mode 100644 index 00000000000..6294d35a121 --- /dev/null +++ b/apps/lockfile-explorer-web/src/containers/PackageJsonViewer/CodeBox.tsx @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import React from 'react'; + +import { Highlight, themes } from 'prism-react-renderer'; + +// Generate this list by doing console.log(Object.keys(Prism.languages)) +// BUT THEN DELETE the APIs that are bizarrely mixed into this namespace: +// "extend", "insertBefore", "DFS" +export type PrismLanguage = + | 'plain' + | 'plaintext' + | 'text' + | 'txt' + | 'markup' + | 'html' + | 'mathml' + | 'svg' + | 'xml' + | 'ssml' + | 'atom' + | 'rss' + | 'regex' + | 'clike' + | 'javascript' + | 'js' + | 'actionscript' + | 'coffeescript' + | 'coffee' + | 'javadoclike' + | 'css' + | 'yaml' + | 'yml' + | 'markdown' + | 'md' + | 'graphql' + | 'sql' + | 'typescript' + | 'ts' + | 'jsdoc' + | 'flow' + | 'n4js' + | 'n4jsd' + | 'jsx' + | 'tsx' + | 'swift' + | 'kotlin' + | 'kt' + | 'kts' + | 'c' + | 'objectivec' + | 'objc' + | 'reason' + | 'rust' + | 'go' + | 'cpp' + | 'python' + | 'py' + | 'json' + | 'webmanifest'; + +export const CodeBox = (props: { code: string; language: PrismLanguage }): JSX.Element => { + return ( + + {({ className, style, tokens, getLineProps, getTokenProps }) => ( +
+          {tokens.map((line, i) => (
+            
+ {line.map((token, key) => ( + + ))} +
+ ))} +
+ )} +
+ ); +}; diff --git a/apps/lockfile-explorer-web/src/containers/PackageJsonViewer/index.tsx b/apps/lockfile-explorer-web/src/containers/PackageJsonViewer/index.tsx index b7588f360a7..a0be0ebd7c3 100644 --- a/apps/lockfile-explorer-web/src/containers/PackageJsonViewer/index.tsx +++ b/apps/lockfile-explorer-web/src/containers/PackageJsonViewer/index.tsx @@ -2,8 +2,8 @@ // See LICENSE in the project root for license information. import React, { useCallback, useEffect, useState } from 'react'; + import { readPnpmfileAsync, readPackageSpecAsync, readPackageJsonAsync } from '../../helpers/lfxApiClient'; -import styles from './styles.scss'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import { selectCurrentEntry } from '../../store/slices/entrySlice'; import type { IPackageJson } from '../../types/IPackageJson'; @@ -13,6 +13,9 @@ import { displaySpecChanges } from '../../helpers/displaySpecChanges'; import { isEntryModified } from '../../helpers/isEntryModified'; import { ScrollArea, Tabs, Text } from '@rushstack/rush-themed-ui'; import { LfxGraphEntryKind } from '../../packlets/lfx-shared'; +import { CodeBox } from './CodeBox'; + +import styles from './styles.scss'; const PackageView: { [key: string]: string } = { PACKAGE_JSON: 'PACKAGE_JSON', @@ -48,9 +51,9 @@ export const PackageJsonViewer = (): JSX.Element => { useEffect(() => { async function loadPackageDetailsAsync(packageName: string): Promise { - const packageJSONFile = await readPackageJsonAsync(packageName); + const packageJSONFile: IPackageJson | undefined = await readPackageJsonAsync(packageName); setPackageJSON(packageJSONFile); - const parsedJSON = await readPackageSpecAsync(packageName); + const parsedJSON: IPackageJson | undefined = await readPackageSpecAsync(packageName); setParsedPackageJSON(parsedJSON); if (packageJSONFile && parsedJSON) { @@ -161,7 +164,7 @@ export const PackageJsonViewer = (): JSX.Element => { Please select a Project or Package to view it's package.json ); - return
{JSON.stringify(packageJSON, null, 2)}
; + return ; case PackageView.PACKAGE_SPEC: if (!pnpmfile) { return ( @@ -171,7 +174,7 @@ export const PackageJsonViewer = (): JSX.Element => { ); } - return
{pnpmfile}
; + return ; case PackageView.PARSED_PACKAGE_JSON: if (!parsedPackageJSON) return ( diff --git a/apps/lockfile-explorer-web/src/packlets/lfx-shared/IJsonLfxWorkspace.ts b/apps/lockfile-explorer-web/src/packlets/lfx-shared/IJsonLfxWorkspace.ts index 31c081d1c91..d5115c1b33b 100644 --- a/apps/lockfile-explorer-web/src/packlets/lfx-shared/IJsonLfxWorkspace.ts +++ b/apps/lockfile-explorer-web/src/packlets/lfx-shared/IJsonLfxWorkspace.ts @@ -3,7 +3,7 @@ export interface IJsonLfxWorkspaceRushConfig { /** - * The rushVersion from rush.json. + * The `rushVersion` field from rush.json. */ readonly rushVersion: string; @@ -12,23 +12,57 @@ export interface IJsonLfxWorkspaceRushConfig { * Otherwise this will be an empty string. */ readonly subspaceName: string; + + /** + * The path to Rush's input file `.pnpmfile.cjs`, relative to `workspaceRootFullPath` + * and normalized to use forward slashes without a leading slash. In a Rush workspace, + * {@link IJsonLfxWorkspace.pnpmfilePath} is a temporary file that is generated from `rushPnpmfilePath`. + * + * @example `"common/config/my-subspace/pnpm-lock.yaml"` + */ + readonly rushPnpmfilePath: string; } export interface IJsonLfxWorkspace { /** - * Absolute path to the workspace folder that is opened by the app. - * Relative paths are generally relative to this path. + * Absolute path to the workspace folder that is opened by the app, normalized to use forward slashes + * without a trailing slash. + * + * @example `"C:/path/to/MyRepo"` */ - readonly workspaceRootFolder: string; + readonly workspaceRootFullPath: string; /** - * The path to the pnpm-lock.yaml file. + * The path to the "pnpm-lock.yaml" file, relative to `workspaceRootFullPath` + * and normalized to use forward slashes without a leading slash. + * + * @example `"common/temp/my-subspace/pnpm-lock.yaml"` + * @example `"pnpm-lock.yaml"` */ readonly pnpmLockfilePath: string; /** - * If this is a Rush workspace (versus a plain PNPM workspace), then - * this section will be defined. + * The path to the folder of "pnpm-lock.yaml" file, relative to `workspaceRootFullPath` + * and normalized to use forward slashes without a leading slash. + * + * If `pnpm-lack.yaml` is in the `workspaceRootFullPath` folder, then pnpmLockfileFolder + * is the empty string. + * + * @example `"common/temp/my-subspace"` + * @example `""` + */ + readonly pnpmLockfileFolder: string; + + /** + * The path to the `.pnpmfile.cjs` file that is loaded by PNPM. In a Rush workspace, + * this is a temporary file that is generated from `rushPnpmfilePath`. + * + * @example `"common/temp/my-subspace/.pnpmfile.cjs"` + */ + readonly pnpmfilePath: string; + + /** + * This section will be defined only if this is a Rush workspace (versus a plain PNPM workspace). */ readonly rushConfig: IJsonLfxWorkspaceRushConfig | undefined; } diff --git a/apps/lockfile-explorer-web/webpack.config.js b/apps/lockfile-explorer-web/webpack.config.js index 10ce2633322..7b09bf573cd 100644 --- a/apps/lockfile-explorer-web/webpack.config.js +++ b/apps/lockfile-explorer-web/webpack.config.js @@ -17,11 +17,11 @@ module.exports = function createConfig(env, argv) { } }, performance: { - hints: env.production ? 'error' : false + hints: env.production ? 'error' : false, // This specifies the bundle size limit that will trigger Webpack's warning saying: // "The following entrypoint(s) combined asset size exceeds the recommended limit." - // maxEntrypointSize: 500000, - // maxAssetSize: 500000 + maxEntrypointSize: Infinity, + maxAssetSize: Infinity }, devServer: { port: 8096, diff --git a/apps/lockfile-explorer/.vscode/launch.json b/apps/lockfile-explorer/.vscode/launch.json index 9cc2fd9f58d..2ee133e0e98 100644 --- a/apps/lockfile-explorer/.vscode/launch.json +++ b/apps/lockfile-explorer/.vscode/launch.json @@ -20,7 +20,7 @@ "name": "Single Jest test", "program": "${workspaceFolder}/node_modules/@rushstack/heft/lib/start.js", "cwd": "${workspaceFolder}", - "args": ["--debug", "test", "--clean", "-u", "--test-path-pattern", "lfxGraphLoader60"], + "args": ["--debug", "test", "--clean", "-u", "--test-path-pattern", "lfxGraph-website-sample-1-v6.0.test"], "console": "integratedTerminal", "sourceMaps": true }, diff --git a/apps/lockfile-explorer/package.json b/apps/lockfile-explorer/package.json index 4e175cb7851..62e03b0be7f 100644 --- a/apps/lockfile-explorer/package.json +++ b/apps/lockfile-explorer/package.json @@ -39,7 +39,7 @@ "_phase:test": "heft run --only test -- --clean" }, "peerDependencies": { - "@types/express": "^4.17.21" + "@types/express": "^5.0.3" }, "peerDependenciesMeta": { "@types/express": { @@ -55,12 +55,12 @@ "@types/update-notifier": "~6.0.1", "eslint": "~9.25.1", "local-node-rig": "workspace:*", - "@pnpm/lockfile-types": "^5.1.5", + "@pnpm/lockfile.types": "1002.0.1", + "@pnpm/types": "1000.8.0", "@types/semver": "7.5.0" }, "dependencies": { "tslib": "~2.8.1", - "@lifaon/path": "~2.1.0", "@microsoft/rush-lib": "workspace:*", "@pnpm/dependency-path-lockfile-pre-v9": "npm:@pnpm/dependency-path@~2.1.2", "@rushstack/node-core-library": "workspace:*", diff --git a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts index cc40653e354..aa351cd1e81 100644 --- a/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts +++ b/apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts @@ -7,7 +7,7 @@ import cors from 'cors'; import process from 'process'; import open from 'open'; import updateNotifier from 'update-notifier'; - +import * as path from 'node:path'; import { FileSystem, type IPackageJson, JsonFile, PackageJsonLookup } from '@rushstack/node-core-library'; import { ConsoleTerminalProvider, type ITerminal, Terminal, Colorize } from '@rushstack/terminal'; import { @@ -15,16 +15,19 @@ import { CommandLineParser, type IRequiredCommandLineStringParameter } from '@rushstack/ts-command-line'; -import type { Lockfile } from '@pnpm/lockfile-types'; + import { type LfxGraph, lfxGraphSerializer, type IAppContext, - type IJsonLfxGraph + type IJsonLfxGraph, + type IJsonLfxWorkspace } from '../../../build/lfx-shared'; +import * as lockfilePath from '../../graph/lockfilePath'; import type { IAppState } from '../../state'; import { init } from '../../utils/init'; +import { PnpmfileRunner } from '../../graph/PnpmfileRunner'; import * as lfxGraphLoader from '../../graph/lfxGraphLoader'; const EXPLORER_TOOL_FILENAME: 'lockfile-explorer' = 'lockfile-explorer'; @@ -99,6 +102,8 @@ export class ExplorerCommandLineParser extends CommandLineParser { subspaceName: this._subspaceParameter.value }); + const lfxWorkspace: IJsonLfxWorkspace = appState.lfxWorkspace; + // Important: This must happen after init() reads the current working directory process.chdir(appState.lockfileExplorerProjectRoot); @@ -152,13 +157,9 @@ export class ExplorerCommandLineParser extends CommandLineParser { app.get('/api/graph', async (req: express.Request, res: express.Response) => { const pnpmLockfileText: string = await FileSystem.readFileAsync(appState.pnpmLockfileLocation); - const lockfile: Lockfile = yaml.load(pnpmLockfileText) as Lockfile; + const lockfile: unknown = yaml.load(pnpmLockfileText) as unknown; - const graph: LfxGraph = lfxGraphLoader.generateLockfileGraph( - appState.lfxWorkspace, - lockfile as lfxGraphLoader.ILockfilePackageType, - appState.lfxWorkspace.rushConfig?.subspaceName ?? '' - ); + const graph: LfxGraph = lfxGraphLoader.generateLockfileGraph(lockfile, lfxWorkspace); const jsonGraph: IJsonLfxGraph = lfxGraphSerializer.serializeToJson(graph); res.type('application/json').send(jsonGraph); @@ -188,13 +189,18 @@ export class ExplorerCommandLineParser extends CommandLineParser { ); app.get('/api/pnpmfile', async (req: express.Request, res: express.Response) => { + const pnpmfilePath: string = lockfilePath.join( + lfxWorkspace.workspaceRootFullPath, + lfxWorkspace.rushConfig?.rushPnpmfilePath ?? lfxWorkspace.pnpmfilePath + ); + let pnpmfile: string; try { - pnpmfile = await FileSystem.readFileAsync(appState.pnpmfileLocation); + pnpmfile = await FileSystem.readFileAsync(pnpmfilePath); } catch (e) { if (FileSystem.isNotExistError(e)) { return res.status(404).send({ - message: `Could not load pnpmfile file in this repo.`, + message: `Could not load .pnpmfile.cjs file in this repo: "${pnpmfilePath}"`, error: `No .pnpmifile.cjs found.` }); } else { @@ -223,10 +229,18 @@ export class ExplorerCommandLineParser extends CommandLineParser { } } - const { - hooks: { readPackage } - } = require(appState.pnpmfileLocation); - const parsedPackage: {} = readPackage(packageJson, {}); + let parsedPackage: IPackageJson = packageJson; + + const pnpmfilePath: string = path.join(lfxWorkspace.workspaceRootFullPath, lfxWorkspace.pnpmfilePath); + if (await FileSystem.existsAsync(pnpmfilePath)) { + const pnpmFileRunner: PnpmfileRunner = new PnpmfileRunner(pnpmfilePath); + try { + parsedPackage = await pnpmFileRunner.transformPackageAsync(packageJson, fileLocation); + } finally { + await pnpmFileRunner.disposeAsync(); + } + } + res.send(parsedPackage); } ); diff --git a/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts index 389bb27001b..12390a1b274 100644 --- a/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts +++ b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts @@ -7,6 +7,8 @@ import { RushConfiguration, type RushConfigurationProject, type Subspace } from import path from 'path'; import yaml from 'js-yaml'; import semver from 'semver'; +import type * as lockfileTypes from '@pnpm/lockfile.types'; +import type * as pnpmTypes from '@pnpm/types'; import { AlreadyReportedError, Async, FileSystem, JsonFile, JsonSchema } from '@rushstack/node-core-library'; import lockfileLintSchema from '../../../schemas/lockfile-lint.schema.json'; @@ -17,7 +19,6 @@ import { parseDependencyPath, splicePackageWithVersion } from '../../../utils/shrinkwrap'; -import type { Lockfile, LockfileV6 } from '@pnpm/lockfile-types'; export interface ILintRule { rule: 'restrict-versions'; @@ -40,7 +41,7 @@ export class CheckAction extends CommandLineAction { private _rushConfiguration!: RushConfiguration; private _checkedProjects: Set; - private _docMap: Map; + private _docMap: Map; public constructor(parser: LintCommandLineParser) { super({ @@ -59,8 +60,8 @@ export class CheckAction extends CommandLineAction { private async _checkVersionCompatibilityAsync( shrinkwrapFileMajorVersion: number, - packages: Lockfile['packages'], - dependencyPath: string, + packages: lockfileTypes.PackageSnapshots | undefined, + dependencyPath: pnpmTypes.DepPath, requiredVersions: Record, checkedDependencyPaths: Set ): Promise { @@ -84,7 +85,7 @@ export class CheckAction extends CommandLineAction { shrinkwrapFileMajorVersion, dependencyPackageName, dependencyPackageVersion - ), + ) as pnpmTypes.DepPath, requiredVersions, checkedDependencyPaths ); @@ -103,12 +104,12 @@ export class CheckAction extends CommandLineAction { const projectFolder: string = project.projectFolder; const subspace: Subspace = project.subspace; const shrinkwrapFilename: string = subspace.getCommittedShrinkwrapFilePath(); - let doc: Lockfile | LockfileV6; + let doc: lockfileTypes.LockfileObject; if (this._docMap.has(shrinkwrapFilename)) { doc = this._docMap.get(shrinkwrapFilename)!; } else { const pnpmLockfileText: string = await FileSystem.readFileAsync(shrinkwrapFilename); - doc = yaml.load(pnpmLockfileText) as Lockfile | LockfileV6; + doc = yaml.load(pnpmLockfileText) as lockfileTypes.LockfileObject; this._docMap.set(shrinkwrapFilename, doc); } const { importers, lockfileVersion, packages } = doc; @@ -120,7 +121,7 @@ export class CheckAction extends CommandLineAction { if (path.resolve(projectFolder, relativePath) === projectFolder) { const dependenciesEntries: [string, unknown][] = Object.entries(dependencies ?? {}); for (const [dependencyName, dependencyValue] of dependenciesEntries) { - const fullDependencyPath: string = splicePackageWithVersion( + const fullDependencyPath: pnpmTypes.DepPath = splicePackageWithVersion( shrinkwrapFileMajorVersion, dependencyName, typeof dependencyValue === 'string' @@ -131,7 +132,7 @@ export class CheckAction extends CommandLineAction { specifier: string; } ).version - ); + ) as pnpmTypes.DepPath; if (fullDependencyPath.includes('link:')) { const dependencyProject: RushConfigurationProject | undefined = this._rushConfiguration.getProjectByName(dependencyName); diff --git a/apps/lockfile-explorer/src/graph/IPnpmfileModule.ts b/apps/lockfile-explorer/src/graph/IPnpmfileModule.ts new file mode 100644 index 00000000000..05ee1eae2e7 --- /dev/null +++ b/apps/lockfile-explorer/src/graph/IPnpmfileModule.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { IPackageJson } from '@rushstack/node-core-library'; + +export interface IReadPackageContext { + log: (message: string) => void; +} + +export type IReadPackageHook = ( + packageJson: IPackageJson, + context: IReadPackageContext +) => IPackageJson | Promise; + +export interface IPnpmHooks { + readPackage?: IReadPackageHook; +} + +/** + * Type of the `.pnpmfile.cjs` module. + */ +export interface IPnpmfileModule { + hooks?: IPnpmHooks; +} diff --git a/apps/lockfile-explorer/src/graph/PnpmfileRunner.ts b/apps/lockfile-explorer/src/graph/PnpmfileRunner.ts new file mode 100644 index 00000000000..6049f215fcb --- /dev/null +++ b/apps/lockfile-explorer/src/graph/PnpmfileRunner.ts @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { Worker } from 'node:worker_threads'; +import * as path from 'node:path'; +import type { IPackageJson } from '@rushstack/node-core-library'; + +import type { IRequestMessage, ResponseMessage } from './pnpmfileRunnerWorkerThread'; + +interface IPromise { + resolve: (r: IPackageJson) => void; + reject: (e: Error) => void; +} + +/** + * Evals `.pnpmfile.cjs` in an isolated thread, so `transformPackageAsync()` can be used to rewrite + * package.json files. Calling `disposeAsync()` will free the loaded modules. + */ +export class PnpmfileRunner { + private _worker: Worker; + private _nextId: number = 1000; + private _promisesById: Map = new Map(); + private _disposed: boolean = false; + + public logger: ((message: string) => void) | undefined = undefined; + + public constructor(pnpmfilePath: string) { + this._worker = new Worker(path.join(`${__dirname}/pnpmfileRunnerWorkerThread.js`), { + workerData: { pnpmfilePath } + }); + + this._worker.on('message', (message: ResponseMessage) => { + const id: number = message.id; + const promise: IPromise | undefined = this._promisesById.get(id); + if (!promise) { + return; + } + + if (message.kind === 'return') { + this._promisesById.delete(id); + // TODO: Validate the user's readPackage() return value + const result: IPackageJson = message.result as IPackageJson; + promise.resolve(result); + } else if (message.kind === 'log') { + // No this._promisesById.delete(id) for this case + if (this.logger) { + this.logger(message.log); + } else { + console.log('.pnpmfile.cjs: ' + message.log); + } + } else { + this._promisesById.delete(id); + promise.reject(new Error(message.error || 'An unknown error occurred')); + } + }); + + this._worker.on('error', (err) => { + for (const promise of this._promisesById.values()) { + promise.reject(err); + } + this._promisesById.clear(); + }); + + this._worker.on('exit', (code) => { + if (!this._disposed) { + const error: Error = new Error( + `PnpmfileRunner worker thread terminated unexpectedly with exit code ${code}` + ); + console.error(error); + for (const promise of this._promisesById.values()) { + promise.reject(error); + } + this._promisesById.clear(); + } + }); + } + + /** + * Invokes the readPackage() hook from .pnpmfile.cjs + */ + public transformPackageAsync( + packageJson: IPackageJson, + packageJsonFullPath: string + ): Promise { + if (this._disposed) { + return Promise.reject(new Error('The operation failed because PnpmfileRunner has been disposed')); + } + + const id: number = this._nextId++; + return new Promise((resolve, reject) => { + this._promisesById.set(id, { resolve, reject }); + this._worker.postMessage({ id, packageJson, packageJsonFullPath } satisfies IRequestMessage); + }); + } + + public async disposeAsync(): Promise { + if (this._disposed) { + return; + } + for (const pending of this._promisesById.values()) { + pending.reject(new Error('Aborted because PnpmfileRunner was disposed')); + } + this._promisesById.clear(); + this._disposed = true; + await this._worker.terminate(); + } +} diff --git a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts index 64ed34f997a..ba421861c21 100644 --- a/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts +++ b/apps/lockfile-explorer/src/graph/lfxGraphLoader.ts @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { Path } from '@lifaon/path'; +import type * as lockfileTypes from '@pnpm/lockfile.types'; +import type * as pnpmTypes from '@pnpm/types'; +import { Text } from '@rushstack/node-core-library'; import { type ILfxGraphDependencyOptions, @@ -13,77 +15,29 @@ import { LfxGraphDependency, type IJsonLfxWorkspace } from '../../build/lfx-shared'; +import * as lockfilePath from './lockfilePath'; + +type PnpmLockfileVersion = 54 | 60 | 90; +type PeerDependenciesMeta = lockfileTypes.LockfilePackageInfo['peerDependenciesMeta']; + +function createPackageLockfileDependency(options: { + name: string; + version: string; + kind: LfxDependencyKind; + containingEntry: LfxGraphEntry; + peerDependenciesMeta?: PeerDependenciesMeta; + pnpmLockfileVersion: PnpmLockfileVersion; + workspace: IJsonLfxWorkspace; +}): LfxGraphDependency { + const { + name, + version, + kind: dependencyType, + containingEntry, + peerDependenciesMeta, + pnpmLockfileVersion + } = options; -import { convertLockfileV6DepPathToV5DepPath } from '../utils/shrinkwrap'; - -enum PnpmLockfileVersion { - V6, - V5 -} - -export interface ILockfileImporterV6 { - dependencies?: { - [key: string]: { - specifier: string; - version: string; - }; - }; - devDependencies?: { - [key: string]: { - specifier: string; - version: string; - }; - }; -} -export interface ILockfileImporterV5 { - specifiers?: Record; - dependencies?: Record; - devDependencies?: Record; -} -export interface ILockfilePackageType { - lockfileVersion: number | string; - importers?: { - [key: string]: ILockfileImporterV5 | ILockfileImporterV6; - }; - packages?: { - [key: string]: { - resolution: { - integrity: string; - }; - dependencies?: Record; - peerDependencies?: Record; - dev: boolean; - }; - }; -} - -export interface ILockfileNode { - dependencies?: { - [key: string]: string; - }; - devDependencies?: { - [key: string]: string; - }; - peerDependencies?: { - [key: string]: string; - }; - peerDependenciesMeta?: { - [key: string]: { - optional: boolean; - }; - }; - transitivePeerDependencies?: string[]; -} - -const packageEntryIdRegex: RegExp = new RegExp('/(.*)/([^/]+)$'); - -function createLockfileDependency( - name: string, - version: string, - dependencyType: LfxDependencyKind, - containingEntry: LfxGraphEntry, - node?: ILockfileNode -): LfxGraphDependency { const result: ILfxGraphDependencyOptions = { name, version, @@ -95,58 +49,91 @@ function createLockfileDependency( if (version.startsWith('link:')) { const relativePath: string = version.substring('link:'.length); - const rootRelativePath: Path | null = new Path('.').relative( - new Path(containingEntry.packageJsonFolderPath).concat(relativePath) - ); - if (!rootRelativePath) { - console.error('No root relative path for dependency!', name); - return new LfxGraphDependency(result); + + if (containingEntry.kind === LfxGraphEntryKind.Project) { + // TODO: Here we assume it's a "workspace:" link and try to resolve it to another workspace project, + // but it could also be a link to an arbitrary folder (in which case this entryId will fail to resolve). + // In the future, we should distinguish these cases. + const selfRelativePath: string = lockfilePath.getAbsolute( + containingEntry.packageJsonFolderPath, + relativePath + ); + result.entryId = 'project:' + selfRelativePath.toString(); + } else { + // This could be a link to anywhere on the local computer, so we don't expect it to have a lockfile entry + result.entryId = ''; } - result.entryId = 'project:' + rootRelativePath.toString(); } else if (result.version.startsWith('/')) { result.entryId = version; } else if (result.dependencyType === LfxDependencyKind.Peer) { - if (node?.peerDependencies) { - result.peerDependencyMeta = { - name: result.name, - version: node.peerDependencies[result.name], - optional: - node.peerDependenciesMeta && node.peerDependenciesMeta[result.name] - ? node.peerDependenciesMeta[result.name].optional - : false - }; - result.entryId = 'Peer: ' + result.name; - } else { - console.error('Peer dependencies info missing!', node); - } + result.peerDependencyMeta = { + name: result.name, + version: version, + optional: peerDependenciesMeta?.[result.name] ? peerDependenciesMeta[result.name].optional : false + }; + result.entryId = 'Peer: ' + result.name; } else { - result.entryId = '/' + result.name + '/' + result.version; + // Version 5.4: /@rushstack/m/1.0.0: + // Version 6.0: /@rushstack/m@1.0.0: + // + // Version 5.4: /@rushstack/j/1.0.0_@rushstack+n@2.0.0 + // Version 6.0: /@rushstack/j@1.0.0(@rushstack/n@2.0.0) + const versionDelimiter: string = pnpmLockfileVersion === 54 ? '/' : '@'; + result.entryId = '/' + result.name + versionDelimiter + result.version; } return new LfxGraphDependency(result); } -// node is the yaml entry that we are trying to parse -function parseDependencies( +// v5.4 used this to parse projects ("importers") also +function parsePackageDependencies( dependencies: LfxGraphDependency[], lockfileEntry: LfxGraphEntry, - node: ILockfileNode + either: lockfileTypes.ProjectSnapshot | lockfileTypes.PackageSnapshot, + pnpmLockfileVersion: PnpmLockfileVersion, + workspace: IJsonLfxWorkspace ): void { + const node: Partial = + either as unknown as Partial; if (node.dependencies) { - for (const [pkgName, pkgVersion] of Object.entries(node.dependencies)) { + for (const [packageName, version] of Object.entries(node.dependencies)) { dependencies.push( - createLockfileDependency(pkgName, pkgVersion, LfxDependencyKind.Regular, lockfileEntry) + createPackageLockfileDependency({ + kind: LfxDependencyKind.Regular, + name: packageName, + version: version, + containingEntry: lockfileEntry, + pnpmLockfileVersion, + workspace + }) ); } } if (node.devDependencies) { - for (const [pkgName, pkgVersion] of Object.entries(node.devDependencies)) { - dependencies.push(createLockfileDependency(pkgName, pkgVersion, LfxDependencyKind.Dev, lockfileEntry)); + for (const [packageName, version] of Object.entries(node.devDependencies)) { + dependencies.push( + createPackageLockfileDependency({ + kind: LfxDependencyKind.Dev, + name: packageName, + version: version, + containingEntry: lockfileEntry, + pnpmLockfileVersion, + workspace + }) + ); } } if (node.peerDependencies) { - for (const [pkgName, pkgVersion] of Object.entries(node.peerDependencies)) { + for (const [packageName, version] of Object.entries(node.peerDependencies)) { dependencies.push( - createLockfileDependency(pkgName, pkgVersion, LfxDependencyKind.Peer, lockfileEntry, node) + createPackageLockfileDependency({ + kind: LfxDependencyKind.Peer, + name: packageName, + version: version, + containingEntry: lockfileEntry, + peerDependenciesMeta: node.peerDependenciesMeta, + pnpmLockfileVersion, + workspace + }) ); } } @@ -157,17 +144,53 @@ function parseDependencies( } } -function createLockfileEntry(options: { +function parseProjectDependencies60( + dependencies: LfxGraphDependency[], + lockfileEntry: LfxGraphEntry, + snapshot: lockfileTypes.LockfileFileProjectSnapshot, + pnpmLockfileVersion: PnpmLockfileVersion, + workspace: IJsonLfxWorkspace +): void { + if (snapshot.dependencies) { + for (const [packageName, specifierAndResolution] of Object.entries(snapshot.dependencies)) { + dependencies.push( + createPackageLockfileDependency({ + kind: LfxDependencyKind.Regular, + name: packageName, + version: specifierAndResolution.version, + containingEntry: lockfileEntry, + pnpmLockfileVersion, + workspace + }) + ); + } + } + if (snapshot.devDependencies) { + for (const [packageName, specifierAndResolution] of Object.entries(snapshot.devDependencies)) { + dependencies.push( + createPackageLockfileDependency({ + kind: LfxDependencyKind.Dev, + name: packageName, + version: specifierAndResolution.version, + containingEntry: lockfileEntry, + pnpmLockfileVersion, + workspace + }) + ); + } + } +} + +function createProjectLockfileEntry(options: { rawEntryId: string; - kind: LfxGraphEntryKind; - rawYamlData: ILockfileNode; duplicates?: Set; - subspaceName?: string; + workspace: IJsonLfxWorkspace; + pnpmLockfileVersion: PnpmLockfileVersion; }): LfxGraphEntry { - const { rawEntryId, kind, rawYamlData, duplicates, subspaceName } = options; + const { rawEntryId, duplicates, workspace } = options; const result: ILfxGraphEntryOptions = { - kind, + kind: LfxGraphEntryKind.Project, entryId: '', rawEntryId: '', packageJsonFolderPath: '', @@ -179,111 +202,163 @@ function createLockfileEntry(options: { result.rawEntryId = rawEntryId; - if (rawEntryId === '.') { - // Project Root - return new LfxGraphEntry(result); - } + // Example: pnpmLockfilePath = 'common/temp/my-subspace/pnpm-lock.yaml' + // Example: pnpmLockfileFolder = 'common/temp/my-subspace' + const pnpmLockfileFolder: string = workspace.pnpmLockfileFolder; - if (kind === LfxGraphEntryKind.Project) { - const rootPackageJsonFolderPath: '' | Path = - new Path(`common/temp/${subspaceName}/package.json`).dirname() || ''; - const packageJsonFolderPath: Path | null = new Path('.').relative( - new Path(rootPackageJsonFolderPath).concat(rawEntryId) - ); - const packageName: string | null = new Path(rawEntryId).basename(); - - if (!packageJsonFolderPath || !packageName) { - console.error('Could not construct path for entry: ', rawEntryId); - return new LfxGraphEntry(result); - } + // Example: rawEntryId = '../../../projects/a' + // Example: packageJsonFolderPath = 'projects/a' + result.packageJsonFolderPath = lockfilePath.getAbsolute(pnpmLockfileFolder, rawEntryId); + result.entryId = 'project:' + result.packageJsonFolderPath; - result.packageJsonFolderPath = packageJsonFolderPath.toString(); - result.entryId = 'project:' + result.packageJsonFolderPath; - result.entryPackageName = packageName.toString(); - if (duplicates?.has(result.entryPackageName)) { - const fullPath: string = new Path(rawEntryId).makeAbsolute('/').toString().substring(1); - result.displayText = `Project: ${result.entryPackageName} (${fullPath})`; - result.entryPackageName = `${result.entryPackageName} (${fullPath})`; - } else { - result.displayText = 'Project: ' + result.entryPackageName; - } + const projectFolderName: string = lockfilePath.getBaseNameOf(rawEntryId); + + if (!duplicates?.has(projectFolderName)) { + // TODO: The actual package.json name might not match its directory name, + // but we have to load package.json to determine it. + result.entryPackageName = projectFolderName; } else { - result.displayText = rawEntryId; + result.entryPackageName = `${projectFolderName} (${result.packageJsonFolderPath})`; + } + result.displayText = `Project: ${result.entryPackageName}`; + + const lockfileEntry: LfxGraphEntry = new LfxGraphEntry(result); + return lockfileEntry; +} - const match: RegExpExecArray | null = packageEntryIdRegex.exec(rawEntryId); +function createPackageLockfileEntry(options: { + rawEntryId: string; + rawYamlData: lockfileTypes.PackageSnapshot; + workspace: IJsonLfxWorkspace; + pnpmLockfileVersion: PnpmLockfileVersion; +}): LfxGraphEntry { + const { rawEntryId, rawYamlData, pnpmLockfileVersion, workspace } = options; - if (match) { - const [, packageName, versionPart] = match; - result.entryPackageName = packageName; + const result: ILfxGraphEntryOptions = { + kind: LfxGraphEntryKind.Package, + entryId: '', + rawEntryId: '', + packageJsonFolderPath: '', + entryPackageName: '', + displayText: '', + entryPackageVersion: '', + entrySuffix: '' + }; - const underscoreIndex: number = versionPart.indexOf('_'); - if (underscoreIndex >= 0) { - const version: string = versionPart.substring(0, underscoreIndex); - const suffix: string = versionPart.substring(underscoreIndex + 1); + result.rawEntryId = rawEntryId; - result.entryPackageVersion = version; - result.entrySuffix = suffix; + // Example: pnpmLockfilePath = 'common/temp/my-subspace/pnpm-lock.yaml' + // Example: pnpmLockfileFolder = 'common/temp/my-subspace' + const pnpmLockfileFolder: string = workspace.pnpmLockfileFolder; - // /@rushstack/eslint-config/3.0.1_eslint@8.21.0+typescript@4.7.4 - // --> @rushstack/eslint-config 3.0.1 (eslint@8.21.0+typescript@4.7.4) - result.displayText = packageName + ' ' + version + ' (' + suffix + ')'; - } else { - result.entryPackageVersion = versionPart; + result.displayText = rawEntryId; - // /@rushstack/eslint-config/3.0.1 - // --> @rushstack/eslint-config 3.0.1 - result.displayText = packageName + ' ' + versionPart; - } + if (!rawEntryId.startsWith('/')) { + throw new Error('Expecting leading "/" in path: ' + JSON.stringify(rawEntryId)); + } + + let dotPnpmSubfolder: string; + + if (pnpmLockfileVersion === 54) { + const lastSlashIndex: number = rawEntryId.lastIndexOf('/'); + if (lastSlashIndex < 0) { + throw new Error('Expecting "/" in path: ' + JSON.stringify(rawEntryId)); + } + const packageName: string = rawEntryId.substring(1, lastSlashIndex); + result.entryPackageName = packageName; + + // /@rushstack/eslint-config/3.0.1_eslint@8.21.0+typescript@4.7.4 + // --> @rushstack/eslint-config 3.0.1 (eslint@8.21.0+typescript@4.7.4) + const underscoreIndex: number = rawEntryId.indexOf('_', lastSlashIndex); + if (underscoreIndex > 0) { + const version: string = rawEntryId.substring(lastSlashIndex + 1, underscoreIndex); + const suffix: string = rawEntryId.substring(underscoreIndex + 1); + result.displayText = packageName + ' ' + version + ' (' + suffix + ')'; + result.entryPackageVersion = version; + result.entrySuffix = suffix; + } else { + // /@rushstack/eslint-config/3.0.1 + // --> @rushstack/eslint-config 3.0.1 + const version: string = rawEntryId.substring(lastSlashIndex + 1); + result.displayText = packageName + ' ' + version; + result.entryPackageVersion = version; } - // Example: - // common/temp/default/node_modules/.pnpm - // /@babel+register@7.17.7_@babel+core@7.17.12 - // /node_modules/@babel/register - result.packageJsonFolderPath = - `common/temp/${subspaceName}/node_modules/.pnpm/` + + // Example: @babel+register@7.17.7_@babel+core@7.17.12 + dotPnpmSubfolder = result.entryPackageName.replace('/', '+') + '@' + result.entryPackageVersion + - (result.entrySuffix ? `_${result.entrySuffix}` : '') + - '/node_modules/' + - result.entryPackageName; - } + (result.entrySuffix ? `_${result.entrySuffix}` : ''); + } else { + // Example inputs: + // /@rushstack/eslint-config@3.0.1 + // /@rushstack/l@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + let versionAtSignIndex: number; + if (rawEntryId.startsWith('/@')) { + versionAtSignIndex = rawEntryId.indexOf('@', 2); + } else { + versionAtSignIndex = rawEntryId.indexOf('@', 1); + } + const packageName: string = rawEntryId.substring(1, versionAtSignIndex); + result.entryPackageName = packageName; - const lockfileEntry: LfxGraphEntry = new LfxGraphEntry(result); - parseDependencies(lockfileEntry.dependencies, lockfileEntry, rawYamlData); - return lockfileEntry; -} + const leftParenIndex: number = rawEntryId.indexOf('(', versionAtSignIndex); + if (leftParenIndex < 0) { + const version: string = rawEntryId.substring(versionAtSignIndex + 1); + result.entryPackageVersion = version; -/** - * Transform any newer lockfile formats to the following format: - * [packageName]: - * specifier: ... - * version: ... - */ -function getImporterValue( - importerValue: ILockfileImporterV5 | ILockfileImporterV6, - pnpmLockfileVersion: PnpmLockfileVersion -): ILockfileImporterV5 { - if (pnpmLockfileVersion === PnpmLockfileVersion.V6) { - const v6ImporterValue: ILockfileImporterV6 = importerValue as ILockfileImporterV6; - const v5ImporterValue: ILockfileImporterV5 = { - specifiers: {}, - dependencies: {}, - devDependencies: {} - }; - for (const [depName, depDetails] of Object.entries(v6ImporterValue.dependencies ?? {})) { - v5ImporterValue.specifiers![depName] = depDetails.specifier; - v5ImporterValue.dependencies![depName] = depDetails.version; - } - for (const [depName, depDetails] of Object.entries(v6ImporterValue.devDependencies ?? {})) { - v5ImporterValue.specifiers![depName] = depDetails.specifier; - v5ImporterValue.devDependencies![depName] = depDetails.version; + // /@rushstack/eslint-config@3.0.1 + // --> @rushstack/eslint-config 3.0.1 + result.displayText = packageName + ' ' + version; + } else { + const version: string = rawEntryId.substring(versionAtSignIndex + 1, leftParenIndex); + result.entryPackageVersion = version; + + // "(@rushstack/m@1.0.0)(@rushstack/n@2.0.0)" + let suffix: string = rawEntryId.substring(leftParenIndex); + + // Rewrite to: + // "@rushstack/m@1.0.0; @rushstack/n@2.0.0" + suffix = Text.replaceAll(suffix, ')(', '; '); + suffix = Text.replaceAll(suffix, '(', ''); + suffix = Text.replaceAll(suffix, ')', ''); + result.entrySuffix = suffix; + + // /@rushstack/l@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + // --> @rushstack/l 1.0.0 [@rushstack/m@1.0.0; @rushstack/n@2.0.0] + result.displayText = packageName + ' ' + version + ' [' + suffix + ']'; } - return v5ImporterValue; - } else { - return importerValue as ILockfileImporterV5; + + // Example: /@rushstack/l@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + // --> @rushstack+l@1.0.0_@rushstack+m@1.0.0_@rushstack+n@2.0.0 + + // @rushstack/l 1.0.0 (@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + dotPnpmSubfolder = rawEntryId.substring(1); + dotPnpmSubfolder = Text.replaceAll(dotPnpmSubfolder, '/', '+'); + dotPnpmSubfolder = Text.replaceAll(dotPnpmSubfolder, ')(', '_'); + dotPnpmSubfolder = Text.replaceAll(dotPnpmSubfolder, '(', '_'); + dotPnpmSubfolder = Text.replaceAll(dotPnpmSubfolder, ')', ''); } + + // Example: + // common/temp/default/node_modules/.pnpm + // /@babel+register@7.17.7_@babel+core@7.17.12 + // /node_modules/@babel/register + result.packageJsonFolderPath = lockfilePath.join( + pnpmLockfileFolder, + `node_modules/.pnpm/` + dotPnpmSubfolder + '/node_modules/' + result.entryPackageName + ); + + const lockfileEntry: LfxGraphEntry = new LfxGraphEntry(result); + parsePackageDependencies( + lockfileEntry.dependencies, + lockfileEntry, + rawYamlData, + pnpmLockfileVersion, + workspace + ); + return lockfileEntry; } /** @@ -292,77 +367,110 @@ function getImporterValue( * * @returns A list of all the LockfileEntries in the lockfile. */ -export function generateLockfileGraph( - workspace: IJsonLfxWorkspace, - lockfile: ILockfilePackageType, - subspaceName?: string -): LfxGraph { - let pnpmLockfileVersion: PnpmLockfileVersion = PnpmLockfileVersion.V5; - if (parseInt(lockfile.lockfileVersion.toString(), 10) === 6) { - pnpmLockfileVersion = PnpmLockfileVersion.V6; - } - - if (lockfile.packages && pnpmLockfileVersion === PnpmLockfileVersion.V6) { - const updatedPackages: ILockfilePackageType['packages'] = {}; - for (const [dependencyPath, dependency] of Object.entries(lockfile.packages)) { - updatedPackages[convertLockfileV6DepPathToV5DepPath(dependencyPath)] = dependency; - } - lockfile.packages = updatedPackages; +export function generateLockfileGraph(lockfileJson: unknown, workspace: IJsonLfxWorkspace): LfxGraph { + const lockfile: lockfileTypes.LockfileObject | lockfileTypes.LockfileFile = lockfileJson as + | lockfileTypes.LockfileObject + | lockfileTypes.LockfileFile; + + let pnpmLockfileVersion: PnpmLockfileVersion; + switch (lockfile.lockfileVersion.toString()) { + case '5.4': + pnpmLockfileVersion = 54; + break; + case '6': + case '6.0': + pnpmLockfileVersion = 60; + break; + //case '9': + //case '9.0': + // pnpmLockfileVersion = 90; + // break; + default: + throw new Error('Unsupported PNPM lockfile version ' + JSON.stringify(lockfile.lockfileVersion)); } const lfxGraph: LfxGraph = new LfxGraph(workspace); const allEntries: LfxGraphEntry[] = lfxGraph.entries; - const allEntriesById: { [key: string]: LfxGraphEntry } = {}; + const allEntriesById: Map = new Map(); const allImporters: LfxGraphEntry[] = []; + + // "Importers" are the local workspace projects if (lockfile.importers) { - // Find duplicate importer names + // Normally the UX shows the concise project folder name. However in the case of duplicates + // (where two projects use the same folder name), then we will need to disambiguate. const baseNames: Set = new Set(); const duplicates: Set = new Set(); for (const importerKey of Object.keys(lockfile.importers)) { - const baseName: string | null = new Path(importerKey).basename(); - if (baseName) { - if (baseNames.has(baseName)) { - duplicates.add(baseName); - } - baseNames.add(baseName); + const baseName: string = lockfilePath.getBaseNameOf(importerKey); + if (baseNames.has(baseName)) { + duplicates.add(baseName); } + baseNames.add(baseName); } - for (const [importerKey, importerValue] of Object.entries(lockfile.importers)) { - // console.log('normalized importer key: ', new Path(importerKey).makeAbsolute('/').toString()); + const isRushWorkspace: boolean = workspace.rushConfig !== undefined; - // const normalizedPath = new Path(importerKey).makeAbsolute('/').toString(); - const importer: LfxGraphEntry = createLockfileEntry({ - // entryId: normalizedPath, + for (const importerKey of Object.keys(lockfile.importers)) { + if (isRushWorkspace && importerKey === '.') { + // Discard the synthetic package.json file created by Rush under common/temp + continue; + } + + const importer: LfxGraphEntry = createProjectLockfileEntry({ rawEntryId: importerKey, - kind: LfxGraphEntryKind.Project, - rawYamlData: getImporterValue(importerValue, pnpmLockfileVersion), duplicates, - subspaceName + workspace, + pnpmLockfileVersion }); + + if (pnpmLockfileVersion === 54) { + const lockfile54: lockfileTypes.LockfileObject = lockfileJson as lockfileTypes.LockfileObject; + const importerValue: lockfileTypes.ProjectSnapshot = + lockfile54.importers[importerKey as pnpmTypes.ProjectId]; + parsePackageDependencies( + importer.dependencies, + importer, + importerValue, + pnpmLockfileVersion, + workspace + ); + } else { + const lockfile60: lockfileTypes.LockfileFile = lockfileJson as lockfileTypes.LockfileFile; + if (lockfile60.importers) { + const importerValue: lockfileTypes.LockfileFileProjectSnapshot = + lockfile60.importers[importerKey as pnpmTypes.ProjectId]; + parseProjectDependencies60( + importer.dependencies, + importer, + importerValue, + pnpmLockfileVersion, + workspace + ); + } + } + allImporters.push(importer); allEntries.push(importer); - allEntriesById[importer.entryId] = importer; + allEntriesById.set(importer.entryId, importer); } } const allPackages: LfxGraphEntry[] = []; if (lockfile.packages) { - for (const [dependencyKey, dependencyValue] of Object.entries(lockfile.packages)) { + for (const [dependencyKey, dependencyValue] of Object.entries(lockfile.packages ?? {})) { // const normalizedPath = new Path(dependencyKey).makeAbsolute('/').toString(); - const currEntry: LfxGraphEntry = createLockfileEntry({ - // entryId: normalizedPath, + const currEntry: LfxGraphEntry = createPackageLockfileEntry({ rawEntryId: dependencyKey, - kind: LfxGraphEntryKind.Package, - rawYamlData: dependencyValue, - subspaceName + rawYamlData: dependencyValue as lockfileTypes.PackageSnapshot, + workspace, + pnpmLockfileVersion }); allPackages.push(currEntry); allEntries.push(currEntry); - allEntriesById[dependencyKey] = currEntry; + allEntriesById.set(dependencyKey, currEntry); } } @@ -374,7 +482,7 @@ export function generateLockfileGraph( continue; } - const matchedEntry: LfxGraphEntry = allEntriesById[dependency.entryId]; + const matchedEntry: LfxGraphEntry | undefined = allEntriesById.get(dependency.entryId); if (matchedEntry) { // Create a two-way link between the dependency and the entry dependency.resolvedEntry = matchedEntry; diff --git a/apps/lockfile-explorer/src/graph/lockfilePath.ts b/apps/lockfile-explorer/src/graph/lockfilePath.ts new file mode 100644 index 00000000000..1a03e4c58f3 --- /dev/null +++ b/apps/lockfile-explorer/src/graph/lockfilePath.ts @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * For example, retrieves `d` from `/a/b/c/d`. + */ +export function getBaseNameOf(importerPath: string): string { + if (importerPath.length === 0) { + return ''; + } + + const index: number = importerPath.lastIndexOf('/'); + if (index === importerPath.length - 1) { + throw new Error('Error: Path has a trailing slash'); + } + if (index >= 0) { + return importerPath.substring(index + 1); + } + return importerPath; +} + +/** + * For example, retrieves `/a/b/c` from `/a/b/c/d`. + */ +export function getParentOf(importerPath: string): string { + if (importerPath === '' || importerPath === '.' || importerPath === '/') { + throw new Error('Error: Path has no parent'); + } + + const index: number = importerPath.lastIndexOf('/'); + if (index === importerPath.length - 1) { + throw new Error('Error: Path has a trailing slash'); + } + if (index === 0) { + return '/'; + } + if (index < 0) { + return '.'; + } + return importerPath.substring(0, index); +} + +/** + * Cheaply resolves a relative path against a base path, assuming the paths are delimited by `/`, + * and assuming the basePath is already in normal form. An error occurs if the relative path + * goes above the root folder. + * + * @example + * ```ts + * getAbsolutePath(`a/b/c`, `d/e`) === `a/b/c/d/e` + * getAbsolutePath(`/a/b/c`, `d/e`) === `/a/b/c/d/e` + * getAbsolutePath(`/a/b/c`, `/d/e`) === `/d/e` + * getAbsolutePath(`a/b/c`, `../../f`) === `a/f` + * getAbsolutePath(`a/b/c`, `.././/f`) === `a/b/f` + * getAbsolutePath(`a/b/c`, `../../..`) === `.` + * getAbsolutePath(`C:/a/b`, `../d`) === `C:/a/d` + * getAbsolutePath(`a/b/c`, `../../../..`) === ERROR + * + * // Degenerate cases: + * getAbsolutePath(`a/b/c/`, `d/`) === `a/b/c/d` // trailing slashes are discarded + * getAbsolutePath(`./../c`, `d`) === `./../c/d` // basePath assumed to be normal form + * getAbsolutePath(`C:\\`, `\\a`) === `C:\\/\\a` // backslashes not supported + * ``` + */ +export function getAbsolute(basePath: string, relativePath: string): string { + let leadingSlash: boolean; + let stack: string[]; + + // Discard intermediary slashes + const relativeParts: string[] = relativePath.split('/').filter((part: string) => part.length > 0); + if (relativePath.startsWith('/')) { + stack = []; + leadingSlash = true; + } else { + // Discard intermediary slashes + stack = basePath.split('/').filter((part: string) => part.length > 0); + leadingSlash = basePath.startsWith('/'); + } + + for (const part of relativeParts) { + if (part === '.') { + // current directory, do nothing + continue; + } else if (part === '..') { + if (stack.length === 0) { + throw new Error('getAbsolutePath(): relativePath goes above the root folder'); + } + stack.pop(); + } else { + stack.push(part); + } + } + if (leadingSlash) { + return '/' + stack.join('/'); + } else { + return stack.length === 0 ? '.' : stack.join('/'); + } +} + +/** + * Returns the two parts joined by exactly one `/`, assuming the parts are already + * in normalized form. The `/` is not added if either part is an empty string. + */ +export function join(leftPart: string, rightPart: string): string { + if (leftPart.length === 0) { + return rightPart; + } + if (rightPart.length === 0) { + return leftPart; + } + + const leftEndsWithSlash: boolean = leftPart[leftPart.length - 1] === '/'; + const rightStartsWithSlash: boolean = rightPart[0] === '/'; + + if (leftEndsWithSlash && rightStartsWithSlash) { + return leftPart + rightPart.substring(1); + } + if (leftEndsWithSlash || rightStartsWithSlash) { + return leftPart + rightPart; + } + return leftPart + '/' + rightPart; +} diff --git a/apps/lockfile-explorer/src/graph/pnpmfileRunnerWorkerThread.ts b/apps/lockfile-explorer/src/graph/pnpmfileRunnerWorkerThread.ts new file mode 100644 index 00000000000..da87f43b115 --- /dev/null +++ b/apps/lockfile-explorer/src/graph/pnpmfileRunnerWorkerThread.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { parentPort, workerData, type MessagePort } from 'node:worker_threads'; +import * as path from 'node:path'; +import type { IPackageJson } from '@rushstack/node-core-library'; + +import type { IPnpmfileModule, IReadPackageContext } from './IPnpmfileModule'; + +export interface IRequestMessage { + id: number; + packageJson: IPackageJson; + packageJsonFullPath: string; +} + +export interface IResponseMessageLog { + kind: 'log'; + id: number; + log: string; +} +export interface IResponseMessageError { + kind: 'error'; + id: number; + error: string; +} +export interface IResponseMessageReturn { + kind: 'return'; + id: number; + result?: unknown; +} +export type ResponseMessage = IResponseMessageLog | IResponseMessageError | IResponseMessageReturn; + +// debugger; + +const { pnpmfilePath } = workerData; +const resolvedPath: string = path.resolve(pnpmfilePath); + +let pnpmfileModule: IPnpmfileModule | undefined = undefined; +let pnpmfileModuleError: Error | undefined = undefined; + +try { + pnpmfileModule = require(resolvedPath); +} catch (error) { + pnpmfileModuleError = error; +} + +// eslint-disable-next-line @rushstack/no-new-null +const threadParentPort: null | MessagePort = parentPort; + +if (!threadParentPort) { + throw new Error('Not running in a worker thread'); +} + +threadParentPort.on('message', async (message: IRequestMessage) => { + const { id, packageJson } = message; + + if (pnpmfileModuleError) { + threadParentPort.postMessage({ + kind: 'error', + id, + error: pnpmfileModuleError.message + } satisfies IResponseMessageError); + return; + } + + try { + if (!pnpmfileModule || !pnpmfileModule.hooks || typeof pnpmfileModule.hooks.readPackage !== 'function') { + // No transformation needed + threadParentPort.postMessage({ + kind: 'return', + id, + result: packageJson + } satisfies IResponseMessageReturn); + return; + } + + const pnpmContext: IReadPackageContext = { + log: (logMessage) => + threadParentPort.postMessage({ + kind: 'log', + id, + log: logMessage + } satisfies IResponseMessageLog) + }; + + const result: IPackageJson = await pnpmfileModule.hooks.readPackage({ ...packageJson }, pnpmContext); + + threadParentPort.postMessage({ kind: 'return', id, result } satisfies IResponseMessageReturn); + } catch (e) { + threadParentPort.postMessage({ + kind: 'error', + id, + error: (e as Error).message + } satisfies IResponseMessageError); + } +}); diff --git a/apps/lockfile-explorer/src/graph/test/PnpmfileRunner.test.ts b/apps/lockfile-explorer/src/graph/test/PnpmfileRunner.test.ts new file mode 100644 index 00000000000..118e75afbba --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/PnpmfileRunner.test.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import inspector from 'inspector'; +import { Path } from '@rushstack/node-core-library'; +import { PnpmfileRunner } from '../PnpmfileRunner'; + +const isDebuggerAttached: boolean = inspector.url() !== undefined; + +// Since we're spawning another thread, increase the timeout to 10s. +// For debugging, use an infinite timeout. +jest.setTimeout(isDebuggerAttached ? 1e9 : 10000); + +describe(PnpmfileRunner.name, () => { + it('transforms a package.json file', async () => { + const dirname: string = Path.convertToSlashes(__dirname); + const libIndex: number = dirname.lastIndexOf('/lib/'); + if (libIndex < 0) { + throw new Error('Unexpected file path'); + } + const srcDirname: string = + dirname.substring(0, libIndex) + '/src/' + dirname.substring(libIndex + '/lib/'.length); + + const pnpmfilePath: string = srcDirname + '/fixtures/PnpmfileRunner/.pnpmfile.cjs'; + const logMessages: string[] = []; + + const pnpmfileRunner: PnpmfileRunner = new PnpmfileRunner(pnpmfilePath); + try { + pnpmfileRunner.logger = (message) => { + logMessages.push(message); + }; + expect( + await pnpmfileRunner.transformPackageAsync( + { + name: '@types/karma', + version: '1.0.0', + dependencies: { + 'example-dependency': '1.0.0' + } + }, + pnpmfilePath + ) + ).toMatchInlineSnapshot(` +Object { + "dependencies": Object { + "example-dependency": "1.0.0", + "log4js": "0.6.38", + }, + "name": "@types/karma", + "version": "1.0.0", +} +`); + } finally { + await pnpmfileRunner.disposeAsync(); + } + + expect(logMessages).toMatchInlineSnapshot(` +Array [ + "Fixed up dependencies for @types/karma", +] +`); + + await expect( + pnpmfileRunner.transformPackageAsync({ name: 'name', version: '1.0.0' }, '') + ).rejects.toThrow('disposed'); + }); +}); diff --git a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap new file mode 100644 index 00000000000..ff8058f4180 --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v5.4.test.ts.snap @@ -0,0 +1,166 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` +"entries: + - dependencies: + - dependencyType: regular + entryId: /color/5.0.2 + name: color + peerDependencyMeta: {} + resolvedEntryJsonId: 6 + version: 5.0.2 + displayText: 'Project: duplicate (duplicate-1/duplicate)' + entryId: project:duplicate-1/duplicate + entryPackageName: duplicate (duplicate-1/duplicate) + entryPackageVersion: '' + entrySuffix: '' + jsonId: 0 + kind: 1 + packageJsonFolderPath: duplicate-1/duplicate + rawEntryId: duplicate-1/duplicate + referrerJsonIds: [] + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-string/2.1.2 + name: color-string + peerDependencyMeta: {} + resolvedEntryJsonId: 5 + version: 2.1.2 + displayText: 'Project: duplicate (duplicate-2/duplicate)' + entryId: project:duplicate-2/duplicate + entryPackageName: duplicate (duplicate-2/duplicate) + entryPackageVersion: '' + entrySuffix: '' + jsonId: 1 + kind: 1 + packageJsonFolderPath: duplicate-2/duplicate + rawEntryId: duplicate-2/duplicate + referrerJsonIds: [] + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /has-symbols/1.0.2 + name: has-symbols + peerDependencyMeta: {} + resolvedEntryJsonId: 7 + version: 1.0.2 + - dependencyType: regular + entryId: project:link-specifier/target-folder + name: target-folder + peerDependencyMeta: {} + version: link:../target-folder + displayText: 'Project: linker' + entryId: project:link-specifier/linker + entryPackageName: linker + entryPackageVersion: '' + entrySuffix: '' + jsonId: 2 + kind: 1 + packageJsonFolderPath: link-specifier/linker + rawEntryId: link-specifier/linker + referrerJsonIds: [] + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-name/2.0.2 + name: color-name + peerDependencyMeta: {} + resolvedEntryJsonId: 4 + version: 2.0.2 + displayText: color-convert 3.1.2 + entryId: '' + entryPackageName: color-convert + entryPackageVersion: 3.1.2 + entrySuffix: '' + jsonId: 3 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/color-convert@3.1.2/node_modules/color-convert + rawEntryId: /color-convert/3.1.2 + referrerJsonIds: + - 6 + transitivePeerDependencies: [] + - dependencies: [] + displayText: color-name 2.0.2 + entryId: '' + entryPackageName: color-name + entryPackageVersion: 2.0.2 + entrySuffix: '' + jsonId: 4 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/color-name@2.0.2/node_modules/color-name + rawEntryId: /color-name/2.0.2 + referrerJsonIds: + - 3 + - 5 + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-name/2.0.2 + name: color-name + peerDependencyMeta: {} + resolvedEntryJsonId: 4 + version: 2.0.2 + displayText: color-string 2.1.2 + entryId: '' + entryPackageName: color-string + entryPackageVersion: 2.1.2 + entrySuffix: '' + jsonId: 5 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/color-string@2.1.2/node_modules/color-string + rawEntryId: /color-string/2.1.2 + referrerJsonIds: + - 1 + - 6 + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-convert/3.1.2 + name: color-convert + peerDependencyMeta: {} + resolvedEntryJsonId: 3 + version: 3.1.2 + - dependencyType: regular + entryId: /color-string/2.1.2 + name: color-string + peerDependencyMeta: {} + resolvedEntryJsonId: 5 + version: 2.1.2 + displayText: color 5.0.2 + entryId: '' + entryPackageName: color + entryPackageVersion: 5.0.2 + entrySuffix: '' + jsonId: 6 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/color@5.0.2/node_modules/color + rawEntryId: /color/5.0.2 + referrerJsonIds: + - 0 + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: '' + name: target-folder + peerDependencyMeta: {} + version: link:link-specifier/target-folder + displayText: has-symbols 1.0.2 + entryId: '' + entryPackageName: has-symbols + entryPackageVersion: 1.0.2 + entrySuffix: '' + jsonId: 7 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/has-symbols@1.0.2/node_modules/has-symbols + rawEntryId: /has-symbols/1.0.2 + referrerJsonIds: + - 2 + transitivePeerDependencies: [] +workspace: + pnpmLockfileFolder: '' + pnpmLockfilePath: pnpm-lock.yaml + pnpmfilePath: .pnpmfile.cjs + workspaceRootFullPath: /repo +" +`; diff --git a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v6.0.test.ts.snap b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v6.0.test.ts.snap new file mode 100644 index 00000000000..ff8058f4180 --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-edge-cases-v6.0.test.ts.snap @@ -0,0 +1,166 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`lfxGraph-edge-cases-v5.4 loads a workspace 1`] = ` +"entries: + - dependencies: + - dependencyType: regular + entryId: /color/5.0.2 + name: color + peerDependencyMeta: {} + resolvedEntryJsonId: 6 + version: 5.0.2 + displayText: 'Project: duplicate (duplicate-1/duplicate)' + entryId: project:duplicate-1/duplicate + entryPackageName: duplicate (duplicate-1/duplicate) + entryPackageVersion: '' + entrySuffix: '' + jsonId: 0 + kind: 1 + packageJsonFolderPath: duplicate-1/duplicate + rawEntryId: duplicate-1/duplicate + referrerJsonIds: [] + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-string/2.1.2 + name: color-string + peerDependencyMeta: {} + resolvedEntryJsonId: 5 + version: 2.1.2 + displayText: 'Project: duplicate (duplicate-2/duplicate)' + entryId: project:duplicate-2/duplicate + entryPackageName: duplicate (duplicate-2/duplicate) + entryPackageVersion: '' + entrySuffix: '' + jsonId: 1 + kind: 1 + packageJsonFolderPath: duplicate-2/duplicate + rawEntryId: duplicate-2/duplicate + referrerJsonIds: [] + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /has-symbols/1.0.2 + name: has-symbols + peerDependencyMeta: {} + resolvedEntryJsonId: 7 + version: 1.0.2 + - dependencyType: regular + entryId: project:link-specifier/target-folder + name: target-folder + peerDependencyMeta: {} + version: link:../target-folder + displayText: 'Project: linker' + entryId: project:link-specifier/linker + entryPackageName: linker + entryPackageVersion: '' + entrySuffix: '' + jsonId: 2 + kind: 1 + packageJsonFolderPath: link-specifier/linker + rawEntryId: link-specifier/linker + referrerJsonIds: [] + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-name/2.0.2 + name: color-name + peerDependencyMeta: {} + resolvedEntryJsonId: 4 + version: 2.0.2 + displayText: color-convert 3.1.2 + entryId: '' + entryPackageName: color-convert + entryPackageVersion: 3.1.2 + entrySuffix: '' + jsonId: 3 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/color-convert@3.1.2/node_modules/color-convert + rawEntryId: /color-convert/3.1.2 + referrerJsonIds: + - 6 + transitivePeerDependencies: [] + - dependencies: [] + displayText: color-name 2.0.2 + entryId: '' + entryPackageName: color-name + entryPackageVersion: 2.0.2 + entrySuffix: '' + jsonId: 4 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/color-name@2.0.2/node_modules/color-name + rawEntryId: /color-name/2.0.2 + referrerJsonIds: + - 3 + - 5 + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-name/2.0.2 + name: color-name + peerDependencyMeta: {} + resolvedEntryJsonId: 4 + version: 2.0.2 + displayText: color-string 2.1.2 + entryId: '' + entryPackageName: color-string + entryPackageVersion: 2.1.2 + entrySuffix: '' + jsonId: 5 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/color-string@2.1.2/node_modules/color-string + rawEntryId: /color-string/2.1.2 + referrerJsonIds: + - 1 + - 6 + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: /color-convert/3.1.2 + name: color-convert + peerDependencyMeta: {} + resolvedEntryJsonId: 3 + version: 3.1.2 + - dependencyType: regular + entryId: /color-string/2.1.2 + name: color-string + peerDependencyMeta: {} + resolvedEntryJsonId: 5 + version: 2.1.2 + displayText: color 5.0.2 + entryId: '' + entryPackageName: color + entryPackageVersion: 5.0.2 + entrySuffix: '' + jsonId: 6 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/color@5.0.2/node_modules/color + rawEntryId: /color/5.0.2 + referrerJsonIds: + - 0 + transitivePeerDependencies: [] + - dependencies: + - dependencyType: regular + entryId: '' + name: target-folder + peerDependencyMeta: {} + version: link:link-specifier/target-folder + displayText: has-symbols 1.0.2 + entryId: '' + entryPackageName: has-symbols + entryPackageVersion: 1.0.2 + entrySuffix: '' + jsonId: 7 + kind: 2 + packageJsonFolderPath: node_modules/.pnpm/has-symbols@1.0.2/node_modules/has-symbols + rawEntryId: /has-symbols/1.0.2 + referrerJsonIds: + - 2 + transitivePeerDependencies: [] +workspace: + pnpmLockfileFolder: '' + pnpmLockfilePath: pnpm-lock.yaml + pnpmfilePath: .pnpmfile.cjs + workspaceRootFullPath: /repo +" +`; diff --git a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v5.4.test.ts.snap b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v5.4.test.ts.snap index f37d9721956..58b8bf4cb78 100644 --- a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v5.4.test.ts.snap +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v5.4.test.ts.snap @@ -2,166 +2,154 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` "entries: - - dependencies: [] - displayText: '' - entryId: '' - entryPackageName: '' - entryPackageVersion: '' - entrySuffix: '' - jsonId: 0 - kind: 1 - packageJsonFolderPath: '' - rawEntryId: . - referrerJsonIds: [] - transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/d + entryId: project:projects/d name: '@rushstack/d' peerDependencyMeta: {} - resolvedEntryJsonId: 4 + resolvedEntryJsonId: 3 version: link:../d displayText: 'Project: a' - entryId: project:./common/projects/a + entryId: project:projects/a entryPackageName: a entryPackageVersion: '' entrySuffix: '' - jsonId: 1 + jsonId: 0 kind: 1 - packageJsonFolderPath: ./common/projects/a + packageJsonFolderPath: projects/a rawEntryId: ../../projects/a referrerJsonIds: [] transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/d + entryId: project:projects/d name: '@rushstack/d' peerDependencyMeta: {} - resolvedEntryJsonId: 4 + resolvedEntryJsonId: 3 version: link:../d - dependencyType: regular entryId: /@rushstack/n/2.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 12 + resolvedEntryJsonId: 11 version: 2.0.0 displayText: 'Project: b' - entryId: project:./common/projects/b + entryId: project:projects/b entryPackageName: b entryPackageVersion: '' entrySuffix: '' - jsonId: 2 + jsonId: 1 kind: 1 - packageJsonFolderPath: ./common/projects/b + packageJsonFolderPath: projects/b rawEntryId: ../../projects/b referrerJsonIds: [] transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/e + entryId: project:projects/e name: '@rushstack/e' peerDependencyMeta: {} - resolvedEntryJsonId: 5 + resolvedEntryJsonId: 4 version: link:../e - dependencyType: regular entryId: /@rushstack/k/1.0.0_@rushstack+m@1.0.0 name: '@rushstack/k' peerDependencyMeta: {} - resolvedEntryJsonId: 7 + resolvedEntryJsonId: 6 version: 1.0.0_@rushstack+m@1.0.0 - dependencyType: regular entryId: /@rushstack/m/1.0.0 name: '@rushstack/m' peerDependencyMeta: {} - resolvedEntryJsonId: 11 + resolvedEntryJsonId: 10 version: 1.0.0 displayText: 'Project: c' - entryId: project:./common/projects/c + entryId: project:projects/c entryPackageName: c entryPackageVersion: '' entrySuffix: '' - jsonId: 3 + jsonId: 2 kind: 1 - packageJsonFolderPath: ./common/projects/c + packageJsonFolderPath: projects/c rawEntryId: ../../projects/c referrerJsonIds: [] transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/e + entryId: project:projects/e name: '@rushstack/e' peerDependencyMeta: {} - resolvedEntryJsonId: 5 + resolvedEntryJsonId: 4 version: link:../e - dependencyType: regular entryId: /@rushstack/j/1.0.0_@rushstack+n@2.0.0 name: '@rushstack/j' peerDependencyMeta: {} - resolvedEntryJsonId: 6 + resolvedEntryJsonId: 5 version: 1.0.0_@rushstack+n@2.0.0 - dependencyType: regular entryId: /@rushstack/n/2.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 12 + resolvedEntryJsonId: 11 version: 2.0.0 displayText: 'Project: d' - entryId: project:./common/projects/d + entryId: project:projects/d entryPackageName: d entryPackageVersion: '' entrySuffix: '' - jsonId: 4 + jsonId: 3 kind: 1 - packageJsonFolderPath: ./common/projects/d + packageJsonFolderPath: projects/d rawEntryId: ../../projects/d referrerJsonIds: + - 0 - 1 - - 2 transitivePeerDependencies: [] - dependencies: - dependencyType: regular entryId: /@rushstack/n/3.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 13 + resolvedEntryJsonId: 12 version: 3.0.0 displayText: 'Project: e' - entryId: project:./common/projects/e + entryId: project:projects/e entryPackageName: e entryPackageVersion: '' entrySuffix: '' - jsonId: 5 + jsonId: 4 kind: 1 - packageJsonFolderPath: ./common/projects/e + packageJsonFolderPath: projects/e rawEntryId: ../../projects/e referrerJsonIds: + - 2 - 3 - - 4 transitivePeerDependencies: [] - dependencies: - dependencyType: regular entryId: /@rushstack/k/1.0.0_wxpgugna4ivthu7yyu4fmciltu name: '@rushstack/k' peerDependencyMeta: {} - resolvedEntryJsonId: 8 + resolvedEntryJsonId: 7 version: 1.0.0_wxpgugna4ivthu7yyu4fmciltu - dependencyType: regular entryId: /@rushstack/m/1.0.0 name: '@rushstack/m' peerDependencyMeta: {} - resolvedEntryJsonId: 11 + resolvedEntryJsonId: 10 version: 1.0.0 displayText: '@rushstack/j 1.0.0 (@rushstack+n@2.0.0)' entryId: '' entryPackageName: '@rushstack/j' entryPackageVersion: 1.0.0 entrySuffix: '@rushstack+n@2.0.0' - jsonId: 6 + jsonId: 5 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+j@1.0.0_@rushstack+n@2.0.0/node_modules/@rushstack/j + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+j@1.0.0_@rushstack+n@2.0.0/node_modules/@rushstack/j rawEntryId: /@rushstack/j/1.0.0_@rushstack+n@2.0.0 referrerJsonIds: - - 4 + - 3 transitivePeerDependencies: - '@rushstack/n' - dependencies: @@ -169,19 +157,19 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryId: /@rushstack/l/1.0.0_@rushstack+m@1.0.0 name: '@rushstack/l' peerDependencyMeta: {} - resolvedEntryJsonId: 9 + resolvedEntryJsonId: 8 version: 1.0.0_@rushstack+m@1.0.0 displayText: '@rushstack/k 1.0.0 (@rushstack+m@1.0.0)' entryId: '' entryPackageName: '@rushstack/k' entryPackageVersion: 1.0.0 entrySuffix: '@rushstack+m@1.0.0' - jsonId: 7 + jsonId: 6 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+k@1.0.0_@rushstack+m@1.0.0/node_modules/@rushstack/k + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+k@1.0.0_@rushstack+m@1.0.0/node_modules/@rushstack/k rawEntryId: /@rushstack/k/1.0.0_@rushstack+m@1.0.0 referrerJsonIds: - - 3 + - 2 transitivePeerDependencies: - '@rushstack/m' - '@rushstack/n' @@ -190,20 +178,19 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryId: /@rushstack/l/1.0.0_wxpgugna4ivthu7yyu4fmciltu name: '@rushstack/l' peerDependencyMeta: {} - resolvedEntryJsonId: 10 + resolvedEntryJsonId: 9 version: 1.0.0_wxpgugna4ivthu7yyu4fmciltu displayText: '@rushstack/k 1.0.0 (wxpgugna4ivthu7yyu4fmciltu)' entryId: '' entryPackageName: '@rushstack/k' entryPackageVersion: 1.0.0 entrySuffix: wxpgugna4ivthu7yyu4fmciltu - jsonId: 8 + jsonId: 7 kind: 2 - packageJsonFolderPath: >- - common/temp/undefined/node_modules/.pnpm/@rushstack+k@1.0.0_wxpgugna4ivthu7yyu4fmciltu/node_modules/@rushstack/k + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+k@1.0.0_wxpgugna4ivthu7yyu4fmciltu/node_modules/@rushstack/k rawEntryId: /@rushstack/k/1.0.0_wxpgugna4ivthu7yyu4fmciltu referrerJsonIds: - - 6 + - 5 transitivePeerDependencies: - '@rushstack/m' - '@rushstack/n' @@ -212,7 +199,7 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryId: /@rushstack/m/1.0.0 name: '@rushstack/m' peerDependencyMeta: {} - resolvedEntryJsonId: 11 + resolvedEntryJsonId: 10 version: 1.0.0 - dependencyType: peer entryId: 'Peer: @rushstack/m' @@ -235,25 +222,25 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryPackageName: '@rushstack/l' entryPackageVersion: 1.0.0 entrySuffix: '@rushstack+m@1.0.0' - jsonId: 9 + jsonId: 8 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+l@1.0.0_@rushstack+m@1.0.0/node_modules/@rushstack/l + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+l@1.0.0_@rushstack+m@1.0.0/node_modules/@rushstack/l rawEntryId: /@rushstack/l/1.0.0_@rushstack+m@1.0.0 referrerJsonIds: - - 7 + - 6 transitivePeerDependencies: [] - dependencies: - dependencyType: regular entryId: /@rushstack/m/1.0.0 name: '@rushstack/m' peerDependencyMeta: {} - resolvedEntryJsonId: 11 + resolvedEntryJsonId: 10 version: 1.0.0 - dependencyType: regular entryId: /@rushstack/n/2.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 12 + resolvedEntryJsonId: 11 version: 2.0.0 - dependencyType: peer entryId: 'Peer: @rushstack/m' @@ -276,13 +263,12 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryPackageName: '@rushstack/l' entryPackageVersion: 1.0.0 entrySuffix: wxpgugna4ivthu7yyu4fmciltu - jsonId: 10 + jsonId: 9 kind: 2 - packageJsonFolderPath: >- - common/temp/undefined/node_modules/.pnpm/@rushstack+l@1.0.0_wxpgugna4ivthu7yyu4fmciltu/node_modules/@rushstack/l + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+l@1.0.0_wxpgugna4ivthu7yyu4fmciltu/node_modules/@rushstack/l rawEntryId: /@rushstack/l/1.0.0_wxpgugna4ivthu7yyu4fmciltu referrerJsonIds: - - 8 + - 7 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/m 1.0.0' @@ -290,15 +276,15 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryPackageName: '@rushstack/m' entryPackageVersion: 1.0.0 entrySuffix: '' - jsonId: 11 + jsonId: 10 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+m@1.0.0/node_modules/@rushstack/m + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+m@1.0.0/node_modules/@rushstack/m rawEntryId: /@rushstack/m/1.0.0 referrerJsonIds: - - 3 - - 6 + - 2 + - 5 + - 8 - 9 - - 10 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/n 2.0.0' @@ -306,14 +292,14 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryPackageName: '@rushstack/n' entryPackageVersion: 2.0.0 entrySuffix: '' - jsonId: 12 + jsonId: 11 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+n@2.0.0/node_modules/@rushstack/n + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+n@2.0.0/node_modules/@rushstack/n rawEntryId: /@rushstack/n/2.0.0 referrerJsonIds: - - 2 - - 4 - - 10 + - 1 + - 3 + - 9 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/n 3.0.0' @@ -321,18 +307,21 @@ exports[`lfxGraph-website-sample-1-v5.4 loads a workspace 1`] = ` entryPackageName: '@rushstack/n' entryPackageVersion: 3.0.0 entrySuffix: '' - jsonId: 13 + jsonId: 12 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+n@3.0.0/node_modules/@rushstack/n + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+n@3.0.0/node_modules/@rushstack/n rawEntryId: /@rushstack/n/3.0.0 referrerJsonIds: - - 5 + - 4 transitivePeerDependencies: [] workspace: + pnpmLockfileFolder: common/temp pnpmLockfilePath: common/temp/pnpm-lock.yaml + pnpmfilePath: common/temp/.pnpmfile.cjs rushConfig: + rushPnpmfilePath: common/config/.pnpmfile.cjs rushVersion: 5.83.3 subspaceName: '' - workspaceRootFolder: /repo + workspaceRootFullPath: /repo " `; diff --git a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap index a24bbbb326c..64f461236c5 100644 --- a/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap +++ b/apps/lockfile-explorer/src/graph/test/__snapshots__/lfxGraph-website-sample-1-v6.0.test.ts.snap @@ -2,204 +2,191 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` "entries: - - dependencies: [] - displayText: '' - entryId: '' - entryPackageName: '' - entryPackageVersion: '' - entrySuffix: '' - jsonId: 0 - kind: 1 - packageJsonFolderPath: '' - rawEntryId: . - referrerJsonIds: [] - transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/d + entryId: project:projects/d name: '@rushstack/d' peerDependencyMeta: {} - resolvedEntryJsonId: 4 + resolvedEntryJsonId: 3 version: link:../d displayText: 'Project: a' - entryId: project:./common/projects/a + entryId: project:projects/a entryPackageName: a entryPackageVersion: '' entrySuffix: '' - jsonId: 1 + jsonId: 0 kind: 1 - packageJsonFolderPath: ./common/projects/a + packageJsonFolderPath: projects/a rawEntryId: ../../projects/a referrerJsonIds: [] transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/d + entryId: project:projects/d name: '@rushstack/d' peerDependencyMeta: {} - resolvedEntryJsonId: 4 + resolvedEntryJsonId: 3 version: link:../d - dependencyType: regular - entryId: /@rushstack/n/2.0.0 + entryId: /@rushstack/n@2.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 10 + resolvedEntryJsonId: 9 version: 2.0.0 displayText: 'Project: b' - entryId: project:./common/projects/b + entryId: project:projects/b entryPackageName: b entryPackageVersion: '' entrySuffix: '' - jsonId: 2 + jsonId: 1 kind: 1 - packageJsonFolderPath: ./common/projects/b + packageJsonFolderPath: projects/b rawEntryId: ../../projects/b referrerJsonIds: [] transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/e + entryId: project:projects/e name: '@rushstack/e' peerDependencyMeta: {} - resolvedEntryJsonId: 5 + resolvedEntryJsonId: 4 version: link:../e - dependencyType: regular - entryId: /@rushstack/k/1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + entryId: /@rushstack/k@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) name: '@rushstack/k' peerDependencyMeta: {} - resolvedEntryJsonId: 7 + resolvedEntryJsonId: 6 version: 1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) - dependencyType: regular - entryId: /@rushstack/m/1.0.0 + entryId: /@rushstack/m@1.0.0 name: '@rushstack/m' peerDependencyMeta: {} - resolvedEntryJsonId: 9 + resolvedEntryJsonId: 8 version: 1.0.0 displayText: 'Project: c' - entryId: project:./common/projects/c + entryId: project:projects/c entryPackageName: c entryPackageVersion: '' entrySuffix: '' - jsonId: 3 + jsonId: 2 kind: 1 - packageJsonFolderPath: ./common/projects/c + packageJsonFolderPath: projects/c rawEntryId: ../../projects/c referrerJsonIds: [] transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: project:./common/projects/e + entryId: project:projects/e name: '@rushstack/e' peerDependencyMeta: {} - resolvedEntryJsonId: 5 + resolvedEntryJsonId: 4 version: link:../e - dependencyType: regular - entryId: /@rushstack/j/1.0.0(@rushstack/n@2.0.0) + entryId: /@rushstack/j@1.0.0(@rushstack/n@2.0.0) name: '@rushstack/j' peerDependencyMeta: {} - resolvedEntryJsonId: 6 + resolvedEntryJsonId: 5 version: 1.0.0(@rushstack/n@2.0.0) - dependencyType: regular - entryId: /@rushstack/n/2.0.0 + entryId: /@rushstack/n@2.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 10 + resolvedEntryJsonId: 9 version: 2.0.0 displayText: 'Project: d' - entryId: project:./common/projects/d + entryId: project:projects/d entryPackageName: d entryPackageVersion: '' entrySuffix: '' - jsonId: 4 + jsonId: 3 kind: 1 - packageJsonFolderPath: ./common/projects/d + packageJsonFolderPath: projects/d rawEntryId: ../../projects/d referrerJsonIds: + - 0 - 1 - - 2 transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: /@rushstack/n/3.0.0 + entryId: /@rushstack/n@3.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 11 + resolvedEntryJsonId: 10 version: 3.0.0 displayText: 'Project: e' - entryId: project:./common/projects/e + entryId: project:projects/e entryPackageName: e entryPackageVersion: '' entrySuffix: '' - jsonId: 5 + jsonId: 4 kind: 1 - packageJsonFolderPath: ./common/projects/e + packageJsonFolderPath: projects/e rawEntryId: ../../projects/e referrerJsonIds: + - 2 - 3 - - 4 transitivePeerDependencies: [] - dependencies: - dependencyType: regular - entryId: /@rushstack/k/1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + entryId: /@rushstack/k@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) name: '@rushstack/k' peerDependencyMeta: {} - resolvedEntryJsonId: 7 + resolvedEntryJsonId: 6 version: 1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) - dependencyType: regular - entryId: /@rushstack/m/1.0.0 + entryId: /@rushstack/m@1.0.0 name: '@rushstack/m' peerDependencyMeta: {} - resolvedEntryJsonId: 9 + resolvedEntryJsonId: 8 version: 1.0.0 - displayText: '@rushstack/j/1.0.0(@rushstack n@2.0.0)' + displayText: '@rushstack/j 1.0.0 [@rushstack/n@2.0.0]' entryId: '' - entryPackageName: '@rushstack/j/1.0.0(@rushstack' - entryPackageVersion: n@2.0.0) - entrySuffix: '' - jsonId: 6 + entryPackageName: '@rushstack/j' + entryPackageVersion: 1.0.0 + entrySuffix: '@rushstack/n@2.0.0' + jsonId: 5 kind: 2 - packageJsonFolderPath: >- - common/temp/undefined/node_modules/.pnpm/@rushstack+j/1.0.0(@rushstack@n@2.0.0)/node_modules/@rushstack/j/1.0.0(@rushstack - rawEntryId: /@rushstack/j/1.0.0(@rushstack/n@2.0.0) + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+j@1.0.0_@rushstack+n@2.0.0/node_modules/@rushstack/j + rawEntryId: /@rushstack/j@1.0.0(@rushstack/n@2.0.0) referrerJsonIds: - - 4 + - 3 transitivePeerDependencies: - '@rushstack/n' - dependencies: - dependencyType: regular - entryId: /@rushstack/l/1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + entryId: /@rushstack/l@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) name: '@rushstack/l' peerDependencyMeta: {} - resolvedEntryJsonId: 8 + resolvedEntryJsonId: 7 version: 1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) - displayText: '@rushstack/k/1.0.0(@rushstack/m@1.0.0)(@rushstack n@2.0.0)' + displayText: '@rushstack/k 1.0.0 [@rushstack/m@1.0.0; @rushstack/n@2.0.0]' entryId: '' - entryPackageName: '@rushstack/k/1.0.0(@rushstack/m@1.0.0)(@rushstack' - entryPackageVersion: n@2.0.0) - entrySuffix: '' - jsonId: 7 + entryPackageName: '@rushstack/k' + entryPackageVersion: 1.0.0 + entrySuffix: '@rushstack/m@1.0.0; @rushstack/n@2.0.0' + jsonId: 6 kind: 2 packageJsonFolderPath: >- - common/temp/undefined/node_modules/.pnpm/@rushstack+k/1.0.0(@rushstack/m@1.0.0)(@rushstack@n@2.0.0)/node_modules/@rushstack/k/1.0.0(@rushstack/m@1.0.0)(@rushstack - rawEntryId: /@rushstack/k/1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + common/temp/node_modules/.pnpm/@rushstack+k@1.0.0_@rushstack+m@1.0.0_@rushstack+n@2.0.0/node_modules/@rushstack/k + rawEntryId: /@rushstack/k@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) referrerJsonIds: - - 3 - - 6 + - 2 + - 5 transitivePeerDependencies: - '@rushstack/m' - '@rushstack/n' - dependencies: - dependencyType: regular - entryId: /@rushstack/m/1.0.0 + entryId: /@rushstack/m@1.0.0 name: '@rushstack/m' peerDependencyMeta: {} - resolvedEntryJsonId: 9 + resolvedEntryJsonId: 8 version: 1.0.0 - dependencyType: regular - entryId: /@rushstack/n/2.0.0 + entryId: /@rushstack/n@2.0.0 name: '@rushstack/n' peerDependencyMeta: {} - resolvedEntryJsonId: 10 + resolvedEntryJsonId: 9 version: 2.0.0 - dependencyType: peer entryId: 'Peer: @rushstack/m' @@ -217,18 +204,18 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` optional: true version: ^2.0.0 version: ^2.0.0 - displayText: '@rushstack/l/1.0.0(@rushstack/m@1.0.0)(@rushstack n@2.0.0)' + displayText: '@rushstack/l 1.0.0 [@rushstack/m@1.0.0; @rushstack/n@2.0.0]' entryId: '' - entryPackageName: '@rushstack/l/1.0.0(@rushstack/m@1.0.0)(@rushstack' - entryPackageVersion: n@2.0.0) - entrySuffix: '' - jsonId: 8 + entryPackageName: '@rushstack/l' + entryPackageVersion: 1.0.0 + entrySuffix: '@rushstack/m@1.0.0; @rushstack/n@2.0.0' + jsonId: 7 kind: 2 packageJsonFolderPath: >- - common/temp/undefined/node_modules/.pnpm/@rushstack+l/1.0.0(@rushstack/m@1.0.0)(@rushstack@n@2.0.0)/node_modules/@rushstack/l/1.0.0(@rushstack/m@1.0.0)(@rushstack - rawEntryId: /@rushstack/l/1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) + common/temp/node_modules/.pnpm/@rushstack+l@1.0.0_@rushstack+m@1.0.0_@rushstack+n@2.0.0/node_modules/@rushstack/l + rawEntryId: /@rushstack/l@1.0.0(@rushstack/m@1.0.0)(@rushstack/n@2.0.0) referrerJsonIds: - - 7 + - 6 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/m 1.0.0' @@ -236,14 +223,14 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` entryPackageName: '@rushstack/m' entryPackageVersion: 1.0.0 entrySuffix: '' - jsonId: 9 + jsonId: 8 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+m@1.0.0/node_modules/@rushstack/m - rawEntryId: /@rushstack/m/1.0.0 + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+m@1.0.0/node_modules/@rushstack/m + rawEntryId: /@rushstack/m@1.0.0 referrerJsonIds: - - 3 - - 6 - - 8 + - 2 + - 5 + - 7 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/n 2.0.0' @@ -251,14 +238,14 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` entryPackageName: '@rushstack/n' entryPackageVersion: 2.0.0 entrySuffix: '' - jsonId: 10 + jsonId: 9 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+n@2.0.0/node_modules/@rushstack/n - rawEntryId: /@rushstack/n/2.0.0 + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+n@2.0.0/node_modules/@rushstack/n + rawEntryId: /@rushstack/n@2.0.0 referrerJsonIds: - - 2 - - 4 - - 8 + - 1 + - 3 + - 7 transitivePeerDependencies: [] - dependencies: [] displayText: '@rushstack/n 3.0.0' @@ -266,18 +253,21 @@ exports[`lfxGraph-website-sample-1-v6.0 loads a workspace 1`] = ` entryPackageName: '@rushstack/n' entryPackageVersion: 3.0.0 entrySuffix: '' - jsonId: 11 + jsonId: 10 kind: 2 - packageJsonFolderPath: common/temp/undefined/node_modules/.pnpm/@rushstack+n@3.0.0/node_modules/@rushstack/n - rawEntryId: /@rushstack/n/3.0.0 + packageJsonFolderPath: common/temp/node_modules/.pnpm/@rushstack+n@3.0.0/node_modules/@rushstack/n + rawEntryId: /@rushstack/n@3.0.0 referrerJsonIds: - - 5 + - 4 transitivePeerDependencies: [] workspace: + pnpmLockfileFolder: common/temp pnpmLockfilePath: common/temp/pnpm-lock.yaml + pnpmfilePath: common/temp/.pnpmfile.cjs rushConfig: + rushPnpmfilePath: common/config/.pnpmcfile.cjs rushVersion: 5.158.1 subspaceName: '' - workspaceRootFolder: /repo + workspaceRootFullPath: /repo " `; diff --git a/apps/lockfile-explorer/src/graph/test/fixtures/PnpmfileRunner/.pnpmfile.cjs b/apps/lockfile-explorer/src/graph/test/fixtures/PnpmfileRunner/.pnpmfile.cjs new file mode 100644 index 00000000000..d7998d79e50 --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/fixtures/PnpmfileRunner/.pnpmfile.cjs @@ -0,0 +1,24 @@ +'use strict'; + +module.exports = { + hooks: { + readPackage + } +}; + +/** + * This hook is invoked during installation before a package's dependencies + * are selected. + * The `packageJson` parameter is the deserialized package.json + * contents for the package that is about to be installed. + * The `context` parameter provides a log() function. + * The return value is the updated object. + */ +function readPackage(packageJson, context) { + if (packageJson.name === '@types/karma') { + context.log('Fixed up dependencies for @types/karma'); + packageJson.dependencies['log4js'] = '0.6.38'; + } + + return packageJson; +} diff --git a/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v5.4.yaml b/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v5.4.yaml new file mode 100644 index 00000000000..4bf1414f29b --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v5.4.yaml @@ -0,0 +1,72 @@ +lockfileVersion: 5.4 + +importers: + duplicate-1/duplicate: + specifiers: + color: ^5.0.2 + dependencies: + color: 5.0.2 + + duplicate-2/duplicate: + specifiers: + color-string: ^2.1.2 + dependencies: + color-string: 2.1.2 + + link-specifier/linker: + specifiers: + has-symbols: 1.0.2 + target-folder: link:../target-folder + dependencies: + has-symbols: 1.0.2 + target-folder: link:../target-folder + +packages: + /color-convert/3.1.2: + resolution: + { + integrity: sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg== + } + engines: { node: '>=14.6' } + dependencies: + color-name: 2.0.2 + dev: false + + /color-name/2.0.2: + resolution: + { + integrity: sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A== + } + engines: { node: '>=12.20' } + dev: false + + /color-string/2.1.2: + resolution: + { + integrity: sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA== + } + engines: { node: '>=18' } + dependencies: + color-name: 2.0.2 + dev: false + + /color/5.0.2: + resolution: + { + integrity: sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA== + } + engines: { node: '>=18' } + dependencies: + color-convert: 3.1.2 + color-string: 2.1.2 + dev: false + + /has-symbols/1.0.2: + resolution: + { + integrity: sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + } + engines: { node: '>= 0.4' } + dependencies: + target-folder: link:link-specifier/target-folder + dev: false diff --git a/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v6.0.yaml b/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v6.0.yaml new file mode 100644 index 00000000000..47d4bd09ffe --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/pnpm-lock-v6.0.yaml @@ -0,0 +1,77 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + duplicate-1/duplicate: + dependencies: + color: + specifier: ^5.0.2 + version: 5.0.2 + + duplicate-2/duplicate: + dependencies: + color-string: + specifier: ^2.1.2 + version: 2.1.2 + + link-specifier/linker: + dependencies: + has-symbols: + specifier: 1.0.2 + version: 1.0.2 + target-folder: + specifier: link:../target-folder + version: link:../target-folder + +packages: + /color-convert@3.1.2: + resolution: + { + integrity: sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg== + } + engines: { node: '>=14.6' } + dependencies: + color-name: 2.0.2 + dev: false + + /color-name@2.0.2: + resolution: + { + integrity: sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A== + } + engines: { node: '>=12.20' } + dev: false + + /color-string@2.1.2: + resolution: + { + integrity: sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA== + } + engines: { node: '>=18' } + dependencies: + color-name: 2.0.2 + dev: false + + /color@5.0.2: + resolution: + { + integrity: sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA== + } + engines: { node: '>=18' } + dependencies: + color-convert: 3.1.2 + color-string: 2.1.2 + dev: false + + /has-symbols@1.0.2: + resolution: + { + integrity: sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + } + engines: { node: '>= 0.4' } + dependencies: + target-folder: link:link-specifier/target-folder + dev: false diff --git a/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/website-sample-1.md b/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/website-sample-1.md new file mode 100644 index 00000000000..ff328176fab --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/fixtures/edge-cases/website-sample-1.md @@ -0,0 +1,4 @@ +# fixtures/edge-cases + +This test fixture is a PNPM workspace crafted to reproduce interesting edge cases in the `lfxGraphLoader` algorithm. + diff --git a/apps/lockfile-explorer/src/graph/test/graphTestHelpers.ts b/apps/lockfile-explorer/src/graph/test/graphTestHelpers.ts index 52583dec6c2..74d99eb35de 100644 --- a/apps/lockfile-explorer/src/graph/test/graphTestHelpers.ts +++ b/apps/lockfile-explorer/src/graph/test/graphTestHelpers.ts @@ -23,8 +23,8 @@ export async function loadAndSerializeLFxGraphAsync(options: { FIXTURES_FOLDER + options.lockfilePathUnderFixtures, { convertLineEndings: NewlineKind.Lf } ); - const lockfileObject = yaml.load(lockfileYaml) as lfxGraphLoader.ILockfilePackageType; - const graph: LfxGraph = lfxGraphLoader.generateLockfileGraph(options.workspace, lockfileObject); + const lockfileObject = yaml.load(lockfileYaml); + const graph: LfxGraph = lfxGraphLoader.generateLockfileGraph(lockfileObject, options.workspace); const serializedObject: IJsonLfxGraph = lfxGraphSerializer.serializeToJson(graph); const serializedYaml: string = yaml.dump(serializedObject, { noRefs: true, diff --git a/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v5.4.test.ts b/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v5.4.test.ts new file mode 100644 index 00000000000..2aa4a13c230 --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v5.4.test.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { IJsonLfxWorkspace } from '../../../build/lfx-shared'; + +import * as graphTestHelpers from './graphTestHelpers'; + +export const workspace: IJsonLfxWorkspace = { + workspaceRootFullPath: '/repo', + pnpmLockfilePath: 'pnpm-lock.yaml', + pnpmLockfileFolder: '', + pnpmfilePath: '.pnpmfile.cjs', + rushConfig: undefined +}; + +describe('lfxGraph-edge-cases-v5.4', () => { + it('loads a workspace', async () => { + const serializedYaml: string = await graphTestHelpers.loadAndSerializeLFxGraphAsync({ + lockfilePathUnderFixtures: '/edge-cases/pnpm-lock-v5.4.yaml', + workspace: workspace + }); + expect(serializedYaml).toMatchSnapshot(); + }); +}); diff --git a/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v6.0.test.ts b/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v6.0.test.ts new file mode 100644 index 00000000000..2aa4a13c230 --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/lfxGraph-edge-cases-v6.0.test.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { IJsonLfxWorkspace } from '../../../build/lfx-shared'; + +import * as graphTestHelpers from './graphTestHelpers'; + +export const workspace: IJsonLfxWorkspace = { + workspaceRootFullPath: '/repo', + pnpmLockfilePath: 'pnpm-lock.yaml', + pnpmLockfileFolder: '', + pnpmfilePath: '.pnpmfile.cjs', + rushConfig: undefined +}; + +describe('lfxGraph-edge-cases-v5.4', () => { + it('loads a workspace', async () => { + const serializedYaml: string = await graphTestHelpers.loadAndSerializeLFxGraphAsync({ + lockfilePathUnderFixtures: '/edge-cases/pnpm-lock-v5.4.yaml', + workspace: workspace + }); + expect(serializedYaml).toMatchSnapshot(); + }); +}); diff --git a/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v5.4.test.ts b/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v5.4.test.ts index 3ca5deda4b8..7c74129e631 100644 --- a/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v5.4.test.ts +++ b/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v5.4.test.ts @@ -6,11 +6,14 @@ import type { IJsonLfxWorkspace } from '../../../build/lfx-shared'; import * as graphTestHelpers from './graphTestHelpers'; export const workspace: IJsonLfxWorkspace = { - workspaceRootFolder: '/repo', + workspaceRootFullPath: '/repo', pnpmLockfilePath: 'common/temp/pnpm-lock.yaml', + pnpmLockfileFolder: 'common/temp', + pnpmfilePath: 'common/temp/.pnpmfile.cjs', rushConfig: { rushVersion: '5.83.3', - subspaceName: '' + subspaceName: '', + rushPnpmfilePath: 'common/config/.pnpmfile.cjs' } }; diff --git a/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v6.0.test.ts b/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v6.0.test.ts index dd381d93a96..0cf7e0c2c25 100644 --- a/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v6.0.test.ts +++ b/apps/lockfile-explorer/src/graph/test/lfxGraph-website-sample-1-v6.0.test.ts @@ -6,11 +6,14 @@ import type { IJsonLfxWorkspace } from '../../../build/lfx-shared'; import * as graphTestHelpers from './graphTestHelpers'; export const workspace: IJsonLfxWorkspace = { - workspaceRootFolder: '/repo', + workspaceRootFullPath: '/repo', pnpmLockfilePath: 'common/temp/pnpm-lock.yaml', + pnpmLockfileFolder: 'common/temp', + pnpmfilePath: 'common/temp/.pnpmfile.cjs', rushConfig: { rushVersion: '5.158.1', - subspaceName: '' + subspaceName: '', + rushPnpmfilePath: 'common/config/.pnpmcfile.cjs' } }; diff --git a/apps/lockfile-explorer/src/graph/test/lockfile.test.ts b/apps/lockfile-explorer/src/graph/test/lockfile.test.ts index 8b89f8de00d..7e1a92a090c 100644 --- a/apps/lockfile-explorer/src/graph/test/lockfile.test.ts +++ b/apps/lockfile-explorer/src/graph/test/lockfile.test.ts @@ -8,7 +8,7 @@ import * as lfxGraphLoader from '../lfxGraphLoader'; describe('LockfileGeneration', () => { it('creates a valid bi-directional graph', () => { - const resolvedPackages = lfxGraphLoader.generateLockfileGraph(TEST_WORKSPACE, TEST_LOCKFILE).entries; + const resolvedPackages = lfxGraphLoader.generateLockfileGraph(TEST_LOCKFILE, TEST_WORKSPACE).entries; // Mapping of all the lockfile entries created by the lockfile const resolvedPackagesMap: { [key: string]: LfxGraphEntry } = {}; @@ -20,7 +20,7 @@ describe('LockfileGeneration', () => { // Ensure validity of the example lockfile entry expect(exampleLockfileImporter.rawEntryId).toBe('../../../apps/testApp1'); - expect(exampleLockfileImporter.entryId).toBe('project:./apps/testApp1'); + expect(exampleLockfileImporter.entryId).toBe('project:apps/testApp1'); // Test that dependencies are linked in the importer project expect(exampleLockfileImporter.dependencies.length).toBe(2); diff --git a/apps/lockfile-explorer/src/graph/test/lockfilePath.test.ts b/apps/lockfile-explorer/src/graph/test/lockfilePath.test.ts new file mode 100644 index 00000000000..008e3b848f1 --- /dev/null +++ b/apps/lockfile-explorer/src/graph/test/lockfilePath.test.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as lockfilePath from '../lockfilePath'; + +describe('lockfilePath', () => { + it('getBaseNameOf', () => { + expect(lockfilePath.getBaseNameOf('/a/b/c/d')).toBe('d'); + expect(lockfilePath.getBaseNameOf('.')).toBe('.'); + expect(lockfilePath.getBaseNameOf('')).toBe(''); + + expect(() => lockfilePath.getParentOf('/a/')).toThrowError('has a trailing slash'); + }); + + it('getParentOf', () => { + expect(lockfilePath.getParentOf('a/b/c/d')).toBe('a/b/c'); + expect(lockfilePath.getParentOf('/a/b/c')).toBe('/a/b'); + expect(lockfilePath.getParentOf('/a/b')).toBe('/a'); + expect(lockfilePath.getParentOf('/a')).toBe('/'); + expect(lockfilePath.getParentOf('a')).toBe('.'); + + expect(() => lockfilePath.getParentOf('')).toThrowError('has no parent'); + expect(() => lockfilePath.getParentOf('/')).toThrowError('has no parent'); + expect(() => lockfilePath.getParentOf('.')).toThrowError('has no parent'); + expect(() => lockfilePath.getParentOf('/a/')).toThrowError('has a trailing slash'); + }); + + it('getAbsolute', () => { + expect(lockfilePath.getAbsolute('a/b/c', 'd/e')).toBe('a/b/c/d/e'); + expect(lockfilePath.getAbsolute('/a/b/c', 'd/e')).toBe('/a/b/c/d/e'); + expect(lockfilePath.getAbsolute('/a/b/c', '/d/e')).toBe('/d/e'); + expect(lockfilePath.getAbsolute('a/b/c', '../../f')).toBe('a/f'); + expect(lockfilePath.getAbsolute('a/b/c', '.././/f')).toBe('a/b/f'); + expect(lockfilePath.getAbsolute('a/b/c', '../../..')).toBe('.'); + expect(lockfilePath.getAbsolute('C:/a/b', '../d')).toBe('C:/a/d'); + + // Error case + expect(() => lockfilePath.getAbsolute('a/b/c', '../../../..')).toThrowError('goes above the root folder'); + + // Degenerate cases + expect(lockfilePath.getAbsolute('a/b/c/', 'd/')).toBe('a/b/c/d'); + expect(lockfilePath.getAbsolute('./../c', 'd')).toBe('./../c/d'); + expect(lockfilePath.getAbsolute('C:\\', '\\a')).toBe('C:\\/\\a'); + }); + + it('join', () => { + expect(lockfilePath.join('', 'a')).toBe('a'); + expect(lockfilePath.join('b', '')).toBe('b'); + expect(lockfilePath.join('a', 'b')).toBe('a/b'); + expect(lockfilePath.join('a/', 'b')).toBe('a/b'); + expect(lockfilePath.join('a', '/b')).toBe('a/b'); + expect(lockfilePath.join('a/', '/b')).toBe('a/b'); + + // Degenerate cases + expect(lockfilePath.join('a//', '/b')).toBe('a//b'); + }); +}); diff --git a/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts b/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts index 977cae423be..d05769f2e81 100644 --- a/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts +++ b/apps/lockfile-explorer/src/graph/test/serializeToJson.test.ts @@ -8,25 +8,11 @@ import { TEST_WORKSPACE, TEST_LOCKFILE } from './testLockfile'; describe('serializeToJson', () => { it('serializes a simple graph', () => { - const graph = lfxGraphLoader.generateLockfileGraph(TEST_WORKSPACE, TEST_LOCKFILE); + const graph = lfxGraphLoader.generateLockfileGraph(TEST_LOCKFILE, TEST_WORKSPACE); expect(lfxGraphSerializer.serializeToJson(graph)).toMatchInlineSnapshot(` Object { "entries": Array [ - Object { - "dependencies": Array [], - "displayText": "", - "entryId": "", - "entryPackageName": "", - "entryPackageVersion": "", - "entrySuffix": "", - "jsonId": 0, - "kind": 1, - "packageJsonFolderPath": "", - "rawEntryId": ".", - "referrerJsonIds": Array [], - "transitivePeerDependencies": Array [], - }, Object { "dependencies": Array [ Object { @@ -38,7 +24,7 @@ Object { "optional": undefined, "version": undefined, }, - "resolvedEntryJsonId": 2, + "resolvedEntryJsonId": 1, "version": "1.7.1", }, Object { @@ -50,18 +36,18 @@ Object { "optional": undefined, "version": undefined, }, - "resolvedEntryJsonId": 3, + "resolvedEntryJsonId": 2, "version": "1.7.1", }, ], "displayText": "Project: testApp1", - "entryId": "project:./apps/testApp1", + "entryId": "project:apps/testApp1", "entryPackageName": "testApp1", "entryPackageVersion": "", "entrySuffix": "", - "jsonId": 1, + "jsonId": 0, "kind": 1, - "packageJsonFolderPath": "./apps/testApp1", + "packageJsonFolderPath": "apps/testApp1", "rawEntryId": "../../../apps/testApp1", "referrerJsonIds": Array [], "transitivePeerDependencies": Array [], @@ -73,12 +59,12 @@ Object { "entryPackageName": "@testPackage/core", "entryPackageVersion": "1.7.1", "entrySuffix": "", - "jsonId": 2, + "jsonId": 1, "kind": 2, - "packageJsonFolderPath": "common/temp/undefined/node_modules/.pnpm/@testPackage+core@1.7.1/node_modules/@testPackage/core", + "packageJsonFolderPath": "common/temp/my-subspace/node_modules/.pnpm/@testPackage+core@1.7.1/node_modules/@testPackage/core", "rawEntryId": "/@testPackage/core/1.7.1", "referrerJsonIds": Array [ - 1, + 0, ], "transitivePeerDependencies": Array [], }, @@ -89,27 +75,33 @@ Object { "entryPackageName": "@testPackage2/core", "entryPackageVersion": "1.7.1", "entrySuffix": "", - "jsonId": 3, + "jsonId": 2, "kind": 2, - "packageJsonFolderPath": "common/temp/undefined/node_modules/.pnpm/@testPackage2+core@1.7.1/node_modules/@testPackage2/core", + "packageJsonFolderPath": "common/temp/my-subspace/node_modules/.pnpm/@testPackage2+core@1.7.1/node_modules/@testPackage2/core", "rawEntryId": "/@testPackage2/core/1.7.1", "referrerJsonIds": Array [ - 1, + 0, ], "transitivePeerDependencies": Array [], }, ], "workspace": Object { - "pnpmLockfilePath": "/test/pnpm-lock.yaml", - "rushConfig": undefined, - "workspaceRootFolder": "/test", + "pnpmLockfileFolder": "common/temp/my-subspace", + "pnpmLockfilePath": "common/temp/my-subspace/pnpm-lock.yaml", + "pnpmfilePath": "common/temp/my-subspace/.pnpmfile.cjs", + "rushConfig": Object { + "rushPnpmfilePath": "common/config/subspaces/my-subspace/.pnpmfile.cjs", + "rushVersion": "0.0.0", + "subspaceName": "my-subspace", + }, + "workspaceRootFullPath": "/repo", }, } `); }); it('deserializes a simple graph', () => { - const originalGraph = lfxGraphLoader.generateLockfileGraph(TEST_WORKSPACE, TEST_LOCKFILE); + const originalGraph = lfxGraphLoader.generateLockfileGraph(TEST_LOCKFILE, TEST_WORKSPACE); const serialized: string = JSON.stringify( lfxGraphSerializer.serializeToJson(originalGraph), diff --git a/apps/lockfile-explorer/src/graph/test/testLockfile.ts b/apps/lockfile-explorer/src/graph/test/testLockfile.ts index 0ba13018fbc..038133be61c 100644 --- a/apps/lockfile-explorer/src/graph/test/testLockfile.ts +++ b/apps/lockfile-explorer/src/graph/test/testLockfile.ts @@ -4,13 +4,19 @@ import type { IJsonLfxWorkspace } from '../../../build/lfx-shared'; export const TEST_WORKSPACE: IJsonLfxWorkspace = { - workspaceRootFolder: '/test', - pnpmLockfilePath: '/test/pnpm-lock.yaml', - rushConfig: undefined + workspaceRootFullPath: '/repo', + pnpmLockfilePath: 'common/temp/my-subspace/pnpm-lock.yaml', + pnpmLockfileFolder: 'common/temp/my-subspace', + pnpmfilePath: 'common/temp/my-subspace/.pnpmfile.cjs', + rushConfig: { + rushVersion: '0.0.0', + subspaceName: 'my-subspace', + rushPnpmfilePath: 'common/config/subspaces/my-subspace/.pnpmfile.cjs' + } }; export const TEST_LOCKFILE = { - lockfileVersion: 5.3, + lockfileVersion: 5.4, importers: { '.': { specifiers: {} diff --git a/apps/lockfile-explorer/src/utils/init.ts b/apps/lockfile-explorer/src/utils/init.ts index 73856ea7943..fe7bf5e8f2d 100644 --- a/apps/lockfile-explorer/src/utils/init.ts +++ b/apps/lockfile-explorer/src/utils/init.ts @@ -9,6 +9,7 @@ import { RushConfiguration } from '@microsoft/rush-lib/lib/api/RushConfiguration import type { Subspace } from '@microsoft/rush-lib/lib/api/Subspace'; import path from 'path'; +import * as lockfilePath from '../graph/lockfilePath'; import type { IAppState } from '../state'; export const init = (options: { @@ -31,23 +32,37 @@ export const init = (options: { const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonPath); const subspace: Subspace = rushConfiguration.getSubspace(subspaceName); - const workspaceFolder: string = subspace.getSubspaceTempFolderPath(); + const commonTempFolder: string = subspace.getSubspaceTempFolderPath(); + const pnpmLockfileAbsolutePath: string = path.join(commonTempFolder, 'pnpm-lock.yaml'); + + const relativeCommonTempFolder: string = Path.convertToSlashes( + path.relative(currentFolder, subspace.getSubspaceTempFolderPath()) + ); + const pnpmLockfileRelativePath: string = lockfilePath.join(relativeCommonTempFolder, 'pnpm-lock.yaml'); + const pnpmFileRelativePath: string = lockfilePath.join(relativeCommonTempFolder, '.pnpmfile.cjs'); + + const relativeCommonConfigFolder: string = Path.convertToSlashes( + path.relative(currentFolder, subspace.getSubspaceConfigFolderPath()) + ); + const rushPnpmFileRelativePath: string = lockfilePath.join(relativeCommonConfigFolder, '.pnpmfile.cjs'); - const pnpmLockfileLocation: string = path.resolve(workspaceFolder, 'pnpm-lock.yaml'); appState = { currentWorkingDirectory, appVersion, debugMode, lockfileExplorerProjectRoot, - pnpmLockfileLocation, - pnpmfileLocation: workspaceFolder + '/.pnpmfile.cjs', + pnpmLockfileLocation: pnpmLockfileAbsolutePath, + pnpmfileLocation: commonTempFolder + '/.pnpmfile.cjs', projectRoot: currentFolder, lfxWorkspace: { - workspaceRootFolder: currentFolder, - pnpmLockfilePath: Path.convertToSlashes(path.relative(currentFolder, pnpmLockfileLocation)), + workspaceRootFullPath: currentFolder, + pnpmLockfilePath: Path.convertToSlashes(pnpmLockfileRelativePath), + pnpmLockfileFolder: Path.convertToSlashes(path.dirname(pnpmLockfileRelativePath)), + pnpmfilePath: Path.convertToSlashes(pnpmFileRelativePath), rushConfig: { rushVersion: rushConfiguration.rushConfigurationJson.rushVersion, - subspaceName: subspaceName ?? '' + subspaceName: subspaceName ?? '', + rushPnpmfilePath: rushPnpmFileRelativePath } } }; @@ -62,8 +77,10 @@ export const init = (options: { pnpmfileLocation: currentFolder + '/.pnpmfile.cjs', projectRoot: currentFolder, lfxWorkspace: { - workspaceRootFolder: currentFolder, + workspaceRootFullPath: currentFolder, pnpmLockfilePath: Path.convertToSlashes(path.relative(currentFolder, pnpmLockPath)), + pnpmLockfileFolder: '', + pnpmfilePath: '.pnpmfile.cjs', rushConfig: undefined } }; diff --git a/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run-rush-pnpm.js b/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run-rush-pnpm.js index 2356649f4e7..4b7aad5d586 100644 --- a/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run-rush-pnpm.js +++ b/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run-rush-pnpm.js @@ -14,18 +14,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See the @microsoft/rush package's LICENSE file for details. -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -var __webpack_exports__ = {}; -/*!*****************************************************!*\ +/******/ (() => { + // webpackBootstrap + /******/ 'use strict'; + var __webpack_exports__ = {}; + /*!*****************************************************!*\ !*** ./lib-esnext/scripts/install-run-rush-pnpm.js ***! \*****************************************************/ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. -require('./install-run-rush'); + // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. + // See LICENSE in the project root for license information. + require('./install-run-rush'); + //# sourceMappingURL=install-run-rush-pnpm.js.map + module.exports = __webpack_exports__; + /******/ +})(); //# sourceMappingURL=install-run-rush-pnpm.js.map -module.exports = __webpack_exports__; -/******/ })() -; -//# sourceMappingURL=install-run-rush-pnpm.js.map \ No newline at end of file diff --git a/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run-rush.js b/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run-rush.js index 9676fc718f9..48da5907f9d 100644 --- a/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run-rush.js +++ b/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run-rush.js @@ -12,207 +12,234 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See the @microsoft/rush package's LICENSE file for details. -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -/******/ var __webpack_modules__ = ({ - -/***/ 657147: -/*!*********************!*\ +/******/ (() => { + // webpackBootstrap + /******/ 'use strict'; + /******/ var __webpack_modules__ = { + /***/ 657147: + /*!*********************!*\ !*** external "fs" ***! \*********************/ -/***/ ((module) => { - -module.exports = require("fs"); + /***/ (module) => { + module.exports = require('fs'); -/***/ }), + /***/ + }, -/***/ 371017: -/*!***********************!*\ + /***/ 371017: + /*!***********************!*\ !*** external "path" ***! \***********************/ -/***/ ((module) => { - -module.exports = require("path"); + /***/ (module) => { + module.exports = require('path'); -/***/ }) + /***/ + } -/******/ }); -/************************************************************************/ -/******/ // The module cache -/******/ var __webpack_module_cache__ = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ // Check if module is in cache -/******/ var cachedModule = __webpack_module_cache__[moduleId]; -/******/ if (cachedModule !== undefined) { -/******/ return cachedModule.exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = __webpack_module_cache__[moduleId] = { -/******/ // no module.id needed -/******/ // no module.loaded needed -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/************************************************************************/ -/******/ /* webpack/runtime/compat get default export */ -/******/ (() => { -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = (module) => { -/******/ var getter = module && module.__esModule ? -/******/ () => (module['default']) : -/******/ () => (module); -/******/ __webpack_require__.d(getter, { a: getter }); -/******/ return getter; -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/define property getters */ -/******/ (() => { -/******/ // define getter functions for harmony exports -/******/ __webpack_require__.d = (exports, definition) => { -/******/ for(var key in definition) { -/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { -/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); -/******/ } -/******/ } -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/hasOwnProperty shorthand */ -/******/ (() => { -/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) -/******/ })(); -/******/ -/******/ /* webpack/runtime/make namespace object */ -/******/ (() => { -/******/ // define __esModule on exports -/******/ __webpack_require__.r = (exports) => { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ })(); -/******/ -/************************************************************************/ -var __webpack_exports__ = {}; -// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. -(() => { -/*!************************************************!*\ + /******/ + }; + /************************************************************************/ + /******/ // The module cache + /******/ var __webpack_module_cache__ = {}; + /******/ + /******/ // The require function + /******/ function __webpack_require__(moduleId) { + /******/ // Check if module is in cache + /******/ var cachedModule = __webpack_module_cache__[moduleId]; + /******/ if (cachedModule !== undefined) { + /******/ return cachedModule.exports; + /******/ + } + /******/ // Create a new module (and put it into the cache) + /******/ var module = (__webpack_module_cache__[moduleId] = { + /******/ // no module.id needed + /******/ // no module.loaded needed + /******/ exports: {} + /******/ + }); + /******/ + /******/ // Execute the module function + /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); + /******/ + /******/ // Return the exports of the module + /******/ return module.exports; + /******/ + } + /******/ + /************************************************************************/ + /******/ /* webpack/runtime/compat get default export */ + /******/ (() => { + /******/ // getDefaultExport function for compatibility with non-harmony modules + /******/ __webpack_require__.n = (module) => { + /******/ var getter = + module && module.__esModule ? /******/ () => module['default'] : /******/ () => module; + /******/ __webpack_require__.d(getter, { a: getter }); + /******/ return getter; + /******/ + }; + /******/ + })(); + /******/ + /******/ /* webpack/runtime/define property getters */ + /******/ (() => { + /******/ // define getter functions for harmony exports + /******/ __webpack_require__.d = (exports, definition) => { + /******/ for (var key in definition) { + /******/ if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { + /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); + /******/ + } + /******/ + } + /******/ + }; + /******/ + })(); + /******/ + /******/ /* webpack/runtime/hasOwnProperty shorthand */ + /******/ (() => { + /******/ __webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); + /******/ + })(); + /******/ + /******/ /* webpack/runtime/make namespace object */ + /******/ (() => { + /******/ // define __esModule on exports + /******/ __webpack_require__.r = (exports) => { + /******/ if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { + /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + /******/ + } + /******/ Object.defineProperty(exports, '__esModule', { value: true }); + /******/ + }; + /******/ + })(); + /******/ + /************************************************************************/ + var __webpack_exports__ = {}; + // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. + (() => { + /*!************************************************!*\ !*** ./lib-esnext/scripts/install-run-rush.js ***! \************************************************/ -__webpack_require__.r(__webpack_exports__); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! path */ 371017); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 657147); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. -/* eslint-disable no-console */ - + __webpack_require__.r(__webpack_exports__); + /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! path */ 371017); + /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/ __webpack_require__.n( + path__WEBPACK_IMPORTED_MODULE_0__ + ); + /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 657147); + /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/ __webpack_require__.n( + fs__WEBPACK_IMPORTED_MODULE_1__ + ); + // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. + // See LICENSE in the project root for license information. + /* eslint-disable no-console */ -const { installAndRun, findRushJsonFolder, RUSH_JSON_FILENAME, runWithErrorAndStatusCode } = require('./install-run'); -const PACKAGE_NAME = '@microsoft/rush'; -const RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION'; -const INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_RUSH_LOCKFILE_PATH'; -function _getRushVersion(logger) { - const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION]; - if (rushPreviewVersion !== undefined) { - logger.info(`Using Rush version from environment variable ${RUSH_PREVIEW_VERSION}=${rushPreviewVersion}`); + const { + installAndRun, + findRushJsonFolder, + RUSH_JSON_FILENAME, + runWithErrorAndStatusCode + } = require('./install-run'); + const PACKAGE_NAME = '@microsoft/rush'; + const RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION'; + const INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_RUSH_LOCKFILE_PATH'; + function _getRushVersion(logger) { + const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION]; + if (rushPreviewVersion !== undefined) { + logger.info( + `Using Rush version from environment variable ${RUSH_PREVIEW_VERSION}=${rushPreviewVersion}` + ); return rushPreviewVersion; - } - const rushJsonFolder = findRushJsonFolder(); - const rushJsonPath = path__WEBPACK_IMPORTED_MODULE_0__.join(rushJsonFolder, RUSH_JSON_FILENAME); - try { + } + const rushJsonFolder = findRushJsonFolder(); + const rushJsonPath = path__WEBPACK_IMPORTED_MODULE_0__.join(rushJsonFolder, RUSH_JSON_FILENAME); + try { const rushJsonContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(rushJsonPath, 'utf-8'); // Use a regular expression to parse out the rushVersion value because rush.json supports comments, // but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script. const rushJsonMatches = rushJsonContents.match(/\"rushVersion\"\s*\:\s*\"([0-9a-zA-Z.+\-]+)\"/); return rushJsonMatches[1]; - } - catch (e) { - throw new Error(`Unable to determine the required version of Rush from ${RUSH_JSON_FILENAME} (${rushJsonFolder}). ` + + } catch (e) { + throw new Error( + `Unable to determine the required version of Rush from ${RUSH_JSON_FILENAME} (${rushJsonFolder}). ` + `The 'rushVersion' field is either not assigned in ${RUSH_JSON_FILENAME} or was specified ` + - 'using an unexpected syntax.'); + 'using an unexpected syntax.' + ); + } } -} -function _getBin(scriptName) { - switch (scriptName.toLowerCase()) { + function _getBin(scriptName) { + switch (scriptName.toLowerCase()) { case 'install-run-rush-pnpm.js': - return 'rush-pnpm'; + return 'rush-pnpm'; case 'install-run-rushx.js': - return 'rushx'; + return 'rushx'; default: - return 'rush'; + return 'rush'; + } } -} -function _run() { - const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, ...packageBinArgs /* [build, --to, myproject] */] = process.argv; - // Detect if this script was directly invoked, or if the install-run-rushx script was invokved to select the - // appropriate binary inside the rush package to run - const scriptName = path__WEBPACK_IMPORTED_MODULE_0__.basename(scriptPath); - const bin = _getBin(scriptName); - if (!nodePath || !scriptPath) { + function _run() { + const [ + nodePath /* Ex: /bin/node */, + scriptPath /* /repo/common/scripts/install-run-rush.js */, + ...packageBinArgs /* [build, --to, myproject] */ + ] = process.argv; + // Detect if this script was directly invoked, or if the install-run-rushx script was invokved to select the + // appropriate binary inside the rush package to run + const scriptName = path__WEBPACK_IMPORTED_MODULE_0__.basename(scriptPath); + const bin = _getBin(scriptName); + if (!nodePath || !scriptPath) { throw new Error('Unexpected exception: could not detect node path or script path'); - } - let commandFound = false; - let logger = { info: console.log, error: console.error }; - for (const arg of packageBinArgs) { + } + let commandFound = false; + let logger = { info: console.log, error: console.error }; + for (const arg of packageBinArgs) { if (arg === '-q' || arg === '--quiet') { - // The -q/--quiet flag is supported by both `rush` and `rushx`, and will suppress - // any normal informational/diagnostic information printed during startup. - // - // To maintain the same user experience, the install-run* scripts pass along this - // flag but also use it to suppress any diagnostic information normally printed - // to stdout. - logger = { - info: () => { }, - error: console.error - }; + // The -q/--quiet flag is supported by both `rush` and `rushx`, and will suppress + // any normal informational/diagnostic information printed during startup. + // + // To maintain the same user experience, the install-run* scripts pass along this + // flag but also use it to suppress any diagnostic information normally printed + // to stdout. + logger = { + info: () => {}, + error: console.error + }; + } else if (!arg.startsWith('-') || arg === '-h' || arg === '--help') { + // We either found something that looks like a command (i.e. - doesn't start with a "-"), + // or we found the -h/--help flag, which can be run without a command + commandFound = true; } - else if (!arg.startsWith('-') || arg === '-h' || arg === '--help') { - // We either found something that looks like a command (i.e. - doesn't start with a "-"), - // or we found the -h/--help flag, which can be run without a command - commandFound = true; - } - } - if (!commandFound) { + } + if (!commandFound) { console.log(`Usage: ${scriptName} [args...]`); if (scriptName === 'install-run-rush-pnpm.js') { - console.log(`Example: ${scriptName} pnpm-command`); - } - else if (scriptName === 'install-run-rush.js') { - console.log(`Example: ${scriptName} build --to myproject`); - } - else { - console.log(`Example: ${scriptName} custom-command`); + console.log(`Example: ${scriptName} pnpm-command`); + } else if (scriptName === 'install-run-rush.js') { + console.log(`Example: ${scriptName} build --to myproject`); + } else { + console.log(`Example: ${scriptName} custom-command`); } process.exit(1); - } - runWithErrorAndStatusCode(logger, () => { + } + runWithErrorAndStatusCode(logger, () => { const version = _getRushVersion(logger); logger.info(`The ${RUSH_JSON_FILENAME} configuration requests Rush version ${version}`); const lockFilePath = process.env[INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE]; if (lockFilePath) { - logger.info(`Found ${INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE}="${lockFilePath}", installing with lockfile.`); + logger.info( + `Found ${INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE}="${lockFilePath}", installing with lockfile.` + ); } return installAndRun(logger, PACKAGE_NAME, version, bin, packageBinArgs, lockFilePath); - }); -} -_run(); -//# sourceMappingURL=install-run-rush.js.map -})(); + }); + } + _run(); + //# sourceMappingURL=install-run-rush.js.map + })(); -module.exports = __webpack_exports__; -/******/ })() -; -//# sourceMappingURL=install-run-rush.js.map \ No newline at end of file + module.exports = __webpack_exports__; + /******/ +})(); +//# sourceMappingURL=install-run-rush.js.map diff --git a/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run-rushx.js b/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run-rushx.js index 6581521f3c7..f865303a384 100644 --- a/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run-rushx.js +++ b/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run-rushx.js @@ -14,18 +14,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See the @microsoft/rush package's LICENSE file for details. -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -var __webpack_exports__ = {}; -/*!*************************************************!*\ +/******/ (() => { + // webpackBootstrap + /******/ 'use strict'; + var __webpack_exports__ = {}; + /*!*************************************************!*\ !*** ./lib-esnext/scripts/install-run-rushx.js ***! \*************************************************/ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. -require('./install-run-rush'); + // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. + // See LICENSE in the project root for license information. + require('./install-run-rush'); + //# sourceMappingURL=install-run-rushx.js.map + module.exports = __webpack_exports__; + /******/ +})(); //# sourceMappingURL=install-run-rushx.js.map -module.exports = __webpack_exports__; -/******/ })() -; -//# sourceMappingURL=install-run-rushx.js.map \ No newline at end of file diff --git a/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run.js b/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run.js index 9283c445267..580ebb343e9 100644 --- a/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run.js +++ b/build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/sharded-repo/common/scripts/install-run.js @@ -12,732 +12,810 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See the @microsoft/rush package's LICENSE file for details. -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -/******/ var __webpack_modules__ = ({ - -/***/ 679877: -/*!************************************************!*\ +/******/ (() => { + // webpackBootstrap + /******/ 'use strict'; + /******/ var __webpack_modules__ = { + /***/ 679877: + /*!************************************************!*\ !*** ./lib-esnext/utilities/npmrcUtilities.js ***! \************************************************/ -/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { - -__webpack_require__.r(__webpack_exports__); -/* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "isVariableSetInNpmrcFile": () => (/* binding */ isVariableSetInNpmrcFile), -/* harmony export */ "syncNpmrc": () => (/* binding */ syncNpmrc) -/* harmony export */ }); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! fs */ 657147); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! path */ 371017); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. -// IMPORTANT - do not use any non-built-in libraries in this file - + /***/ (__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + __webpack_require__.r(__webpack_exports__); + /* harmony export */ __webpack_require__.d(__webpack_exports__, { + /* harmony export */ isVariableSetInNpmrcFile: () => /* binding */ isVariableSetInNpmrcFile, + /* harmony export */ syncNpmrc: () => /* binding */ syncNpmrc + /* harmony export */ + }); + /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! fs */ 657147); + /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = + /*#__PURE__*/ __webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); + /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! path */ 371017); + /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = + /*#__PURE__*/ __webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); + // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. + // See LICENSE in the project root for license information. + // IMPORTANT - do not use any non-built-in libraries in this file -/** - * This function reads the content for given .npmrc file path, and also trims - * unusable lines from the .npmrc file. - * - * @returns - * The text of the the .npmrc. - */ -// create a global _combinedNpmrc for cache purpose -const _combinedNpmrcMap = new Map(); -function _trimNpmrcFile(options) { - const { sourceNpmrcPath, linesToPrepend, linesToAppend } = options; - const combinedNpmrcFromCache = _combinedNpmrcMap.get(sourceNpmrcPath); - if (combinedNpmrcFromCache !== undefined) { - return combinedNpmrcFromCache; - } - let npmrcFileLines = []; - if (linesToPrepend) { - npmrcFileLines.push(...linesToPrepend); - } - if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) { - npmrcFileLines.push(...fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(sourceNpmrcPath).toString().split('\n')); - } - if (linesToAppend) { - npmrcFileLines.push(...linesToAppend); - } - npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim()); - const resultLines = []; - // This finds environment variable tokens that look like "${VAR_NAME}" - const expansionRegExp = /\$\{([^\}]+)\}/g; - // Comment lines start with "#" or ";" - const commentRegExp = /^\s*[#;]/; - // Trim out lines that reference environment variables that aren't defined - for (let line of npmrcFileLines) { - let lineShouldBeTrimmed = false; - //remove spaces before or after key and value - line = line - .split('=') - .map((lineToTrim) => lineToTrim.trim()) - .join('='); - // Ignore comment lines - if (!commentRegExp.test(line)) { - const environmentVariables = line.match(expansionRegExp); - if (environmentVariables) { + /** + * This function reads the content for given .npmrc file path, and also trims + * unusable lines from the .npmrc file. + * + * @returns + * The text of the the .npmrc. + */ + // create a global _combinedNpmrc for cache purpose + const _combinedNpmrcMap = new Map(); + function _trimNpmrcFile(options) { + const { sourceNpmrcPath, linesToPrepend, linesToAppend } = options; + const combinedNpmrcFromCache = _combinedNpmrcMap.get(sourceNpmrcPath); + if (combinedNpmrcFromCache !== undefined) { + return combinedNpmrcFromCache; + } + let npmrcFileLines = []; + if (linesToPrepend) { + npmrcFileLines.push(...linesToPrepend); + } + if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) { + npmrcFileLines.push( + ...fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(sourceNpmrcPath).toString().split('\n') + ); + } + if (linesToAppend) { + npmrcFileLines.push(...linesToAppend); + } + npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim()); + const resultLines = []; + // This finds environment variable tokens that look like "${VAR_NAME}" + const expansionRegExp = /\$\{([^\}]+)\}/g; + // Comment lines start with "#" or ";" + const commentRegExp = /^\s*[#;]/; + // Trim out lines that reference environment variables that aren't defined + for (let line of npmrcFileLines) { + let lineShouldBeTrimmed = false; + //remove spaces before or after key and value + line = line + .split('=') + .map((lineToTrim) => lineToTrim.trim()) + .join('='); + // Ignore comment lines + if (!commentRegExp.test(line)) { + const environmentVariables = line.match(expansionRegExp); + if (environmentVariables) { for (const token of environmentVariables) { - // Remove the leading "${" and the trailing "}" from the token - const environmentVariableName = token.substring(2, token.length - 1); - // Is the environment variable defined? - if (!process.env[environmentVariableName]) { - // No, so trim this line - lineShouldBeTrimmed = true; - break; - } + // Remove the leading "${" and the trailing "}" from the token + const environmentVariableName = token.substring(2, token.length - 1); + // Is the environment variable defined? + if (!process.env[environmentVariableName]) { + // No, so trim this line + lineShouldBeTrimmed = true; + break; + } } + } } + if (lineShouldBeTrimmed) { + // Example output: + // "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}" + resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line); + } else { + resultLines.push(line); + } + } + const combinedNpmrc = resultLines.join('\n'); + //save the cache + _combinedNpmrcMap.set(sourceNpmrcPath, combinedNpmrc); + return combinedNpmrc; } - if (lineShouldBeTrimmed) { - // Example output: - // "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}" - resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line); - } - else { - resultLines.push(line); + function _copyAndTrimNpmrcFile(options) { + const { logger, sourceNpmrcPath, targetNpmrcPath, linesToPrepend, linesToAppend } = options; + logger.info(`Transforming ${sourceNpmrcPath}`); // Verbose + logger.info(` --> "${targetNpmrcPath}"`); + const combinedNpmrc = _trimNpmrcFile({ + sourceNpmrcPath, + linesToPrepend, + linesToAppend + }); + fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(targetNpmrcPath, combinedNpmrc); + return combinedNpmrc; } - } - const combinedNpmrc = resultLines.join('\n'); - //save the cache - _combinedNpmrcMap.set(sourceNpmrcPath, combinedNpmrc); - return combinedNpmrc; -} -function _copyAndTrimNpmrcFile(options) { - const { logger, sourceNpmrcPath, targetNpmrcPath, linesToPrepend, linesToAppend } = options; - logger.info(`Transforming ${sourceNpmrcPath}`); // Verbose - logger.info(` --> "${targetNpmrcPath}"`); - const combinedNpmrc = _trimNpmrcFile({ - sourceNpmrcPath, - linesToPrepend, - linesToAppend - }); - fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(targetNpmrcPath, combinedNpmrc); - return combinedNpmrc; -} -function syncNpmrc(options) { - const { sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish, logger = { - // eslint-disable-next-line no-console - info: console.log, - // eslint-disable-next-line no-console - error: console.error - }, createIfMissing = false, linesToAppend, linesToPrepend } = options; - const sourceNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish'); - const targetNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(targetNpmrcFolder, '.npmrc'); - try { - if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath) || createIfMissing) { - // Ensure the target folder exists - if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcFolder)) { + function syncNpmrc(options) { + const { + sourceNpmrcFolder, + targetNpmrcFolder, + useNpmrcPublish, + logger = { + // eslint-disable-next-line no-console + info: console.log, + // eslint-disable-next-line no-console + error: console.error + }, + createIfMissing = false, + linesToAppend, + linesToPrepend + } = options; + const sourceNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join( + sourceNpmrcFolder, + !useNpmrcPublish ? '.npmrc' : '.npmrc-publish' + ); + const targetNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(targetNpmrcFolder, '.npmrc'); + try { + if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath) || createIfMissing) { + // Ensure the target folder exists + if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcFolder)) { fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(targetNpmrcFolder, { recursive: true }); - } - return _copyAndTrimNpmrcFile({ + } + return _copyAndTrimNpmrcFile({ sourceNpmrcPath, targetNpmrcPath, logger, linesToAppend, linesToPrepend - }); + }); + } else if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcPath)) { + // If the source .npmrc doesn't exist and there is one in the target, delete the one in the target + logger.info(`Deleting ${targetNpmrcPath}`); // Verbose + fs__WEBPACK_IMPORTED_MODULE_0__.unlinkSync(targetNpmrcPath); + } + } catch (e) { + throw new Error(`Error syncing .npmrc file: ${e}`); + } } - else if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcPath)) { - // If the source .npmrc doesn't exist and there is one in the target, delete the one in the target - logger.info(`Deleting ${targetNpmrcPath}`); // Verbose - fs__WEBPACK_IMPORTED_MODULE_0__.unlinkSync(targetNpmrcPath); + function isVariableSetInNpmrcFile(sourceNpmrcFolder, variableKey) { + const sourceNpmrcPath = `${sourceNpmrcFolder}/.npmrc`; + //if .npmrc file does not exist, return false directly + if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) { + return false; + } + const trimmedNpmrcFile = _trimNpmrcFile({ sourceNpmrcPath }); + const variableKeyRegExp = new RegExp(`^${variableKey}=`, 'm'); + return trimmedNpmrcFile.match(variableKeyRegExp) !== null; } - } - catch (e) { - throw new Error(`Error syncing .npmrc file: ${e}`); - } -} -function isVariableSetInNpmrcFile(sourceNpmrcFolder, variableKey) { - const sourceNpmrcPath = `${sourceNpmrcFolder}/.npmrc`; - //if .npmrc file does not exist, return false directly - if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) { - return false; - } - const trimmedNpmrcFile = _trimNpmrcFile({ sourceNpmrcPath }); - const variableKeyRegExp = new RegExp(`^${variableKey}=`, 'm'); - return trimmedNpmrcFile.match(variableKeyRegExp) !== null; -} -//# sourceMappingURL=npmrcUtilities.js.map + //# sourceMappingURL=npmrcUtilities.js.map -/***/ }), + /***/ + }, -/***/ 532081: -/*!********************************!*\ + /***/ 532081: + /*!********************************!*\ !*** external "child_process" ***! \********************************/ -/***/ ((module) => { - -module.exports = require("child_process"); + /***/ (module) => { + module.exports = require('child_process'); -/***/ }), + /***/ + }, -/***/ 657147: -/*!*********************!*\ + /***/ 657147: + /*!*********************!*\ !*** external "fs" ***! \*********************/ -/***/ ((module) => { + /***/ (module) => { + module.exports = require('fs'); -module.exports = require("fs"); + /***/ + }, -/***/ }), - -/***/ 822037: -/*!*********************!*\ + /***/ 822037: + /*!*********************!*\ !*** external "os" ***! \*********************/ -/***/ ((module) => { - -module.exports = require("os"); + /***/ (module) => { + module.exports = require('os'); -/***/ }), + /***/ + }, -/***/ 371017: -/*!***********************!*\ + /***/ 371017: + /*!***********************!*\ !*** external "path" ***! \***********************/ -/***/ ((module) => { - -module.exports = require("path"); + /***/ (module) => { + module.exports = require('path'); -/***/ }) + /***/ + } -/******/ }); -/************************************************************************/ -/******/ // The module cache -/******/ var __webpack_module_cache__ = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ // Check if module is in cache -/******/ var cachedModule = __webpack_module_cache__[moduleId]; -/******/ if (cachedModule !== undefined) { -/******/ return cachedModule.exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = __webpack_module_cache__[moduleId] = { -/******/ // no module.id needed -/******/ // no module.loaded needed -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/************************************************************************/ -/******/ /* webpack/runtime/compat get default export */ -/******/ (() => { -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = (module) => { -/******/ var getter = module && module.__esModule ? -/******/ () => (module['default']) : -/******/ () => (module); -/******/ __webpack_require__.d(getter, { a: getter }); -/******/ return getter; -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/define property getters */ -/******/ (() => { -/******/ // define getter functions for harmony exports -/******/ __webpack_require__.d = (exports, definition) => { -/******/ for(var key in definition) { -/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { -/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); -/******/ } -/******/ } -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/hasOwnProperty shorthand */ -/******/ (() => { -/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) -/******/ })(); -/******/ -/******/ /* webpack/runtime/make namespace object */ -/******/ (() => { -/******/ // define __esModule on exports -/******/ __webpack_require__.r = (exports) => { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ })(); -/******/ -/************************************************************************/ -var __webpack_exports__ = {}; -// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. -(() => { -/*!*******************************************!*\ + /******/ + }; + /************************************************************************/ + /******/ // The module cache + /******/ var __webpack_module_cache__ = {}; + /******/ + /******/ // The require function + /******/ function __webpack_require__(moduleId) { + /******/ // Check if module is in cache + /******/ var cachedModule = __webpack_module_cache__[moduleId]; + /******/ if (cachedModule !== undefined) { + /******/ return cachedModule.exports; + /******/ + } + /******/ // Create a new module (and put it into the cache) + /******/ var module = (__webpack_module_cache__[moduleId] = { + /******/ // no module.id needed + /******/ // no module.loaded needed + /******/ exports: {} + /******/ + }); + /******/ + /******/ // Execute the module function + /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); + /******/ + /******/ // Return the exports of the module + /******/ return module.exports; + /******/ + } + /******/ + /************************************************************************/ + /******/ /* webpack/runtime/compat get default export */ + /******/ (() => { + /******/ // getDefaultExport function for compatibility with non-harmony modules + /******/ __webpack_require__.n = (module) => { + /******/ var getter = + module && module.__esModule ? /******/ () => module['default'] : /******/ () => module; + /******/ __webpack_require__.d(getter, { a: getter }); + /******/ return getter; + /******/ + }; + /******/ + })(); + /******/ + /******/ /* webpack/runtime/define property getters */ + /******/ (() => { + /******/ // define getter functions for harmony exports + /******/ __webpack_require__.d = (exports, definition) => { + /******/ for (var key in definition) { + /******/ if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { + /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); + /******/ + } + /******/ + } + /******/ + }; + /******/ + })(); + /******/ + /******/ /* webpack/runtime/hasOwnProperty shorthand */ + /******/ (() => { + /******/ __webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); + /******/ + })(); + /******/ + /******/ /* webpack/runtime/make namespace object */ + /******/ (() => { + /******/ // define __esModule on exports + /******/ __webpack_require__.r = (exports) => { + /******/ if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { + /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + /******/ + } + /******/ Object.defineProperty(exports, '__esModule', { value: true }); + /******/ + }; + /******/ + })(); + /******/ + /************************************************************************/ + var __webpack_exports__ = {}; + // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. + (() => { + /*!*******************************************!*\ !*** ./lib-esnext/scripts/install-run.js ***! \*******************************************/ -__webpack_require__.r(__webpack_exports__); -/* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "RUSH_JSON_FILENAME": () => (/* binding */ RUSH_JSON_FILENAME), -/* harmony export */ "findRushJsonFolder": () => (/* binding */ findRushJsonFolder), -/* harmony export */ "getNpmPath": () => (/* binding */ getNpmPath), -/* harmony export */ "installAndRun": () => (/* binding */ installAndRun), -/* harmony export */ "runWithErrorAndStatusCode": () => (/* binding */ runWithErrorAndStatusCode) -/* harmony export */ }); -/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! child_process */ 532081); -/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(child_process__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 657147); -/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! os */ 822037); -/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(os__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! path */ 371017); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); -/* harmony import */ var _utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../utilities/npmrcUtilities */ 679877); -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. -/* eslint-disable no-console */ - - - - + __webpack_require__.r(__webpack_exports__); + /* harmony export */ __webpack_require__.d(__webpack_exports__, { + /* harmony export */ RUSH_JSON_FILENAME: () => /* binding */ RUSH_JSON_FILENAME, + /* harmony export */ findRushJsonFolder: () => /* binding */ findRushJsonFolder, + /* harmony export */ getNpmPath: () => /* binding */ getNpmPath, + /* harmony export */ installAndRun: () => /* binding */ installAndRun, + /* harmony export */ runWithErrorAndStatusCode: () => /* binding */ runWithErrorAndStatusCode + /* harmony export */ + }); + /* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( + /*! child_process */ 532081 + ); + /* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0___default = + /*#__PURE__*/ __webpack_require__.n(child_process__WEBPACK_IMPORTED_MODULE_0__); + /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 657147); + /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/ __webpack_require__.n( + fs__WEBPACK_IMPORTED_MODULE_1__ + ); + /* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! os */ 822037); + /* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/ __webpack_require__.n( + os__WEBPACK_IMPORTED_MODULE_2__ + ); + /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! path */ 371017); + /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/ __webpack_require__.n( + path__WEBPACK_IMPORTED_MODULE_3__ + ); + /* harmony import */ var _utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__( + /*! ../utilities/npmrcUtilities */ 679877 + ); + // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. + // See LICENSE in the project root for license information. + /* eslint-disable no-console */ -const RUSH_JSON_FILENAME = 'rush.json'; -const RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME = 'RUSH_TEMP_FOLDER'; -const INSTALL_RUN_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_LOCKFILE_PATH'; -const INSTALLED_FLAG_FILENAME = 'installed.flag'; -const NODE_MODULES_FOLDER_NAME = 'node_modules'; -const PACKAGE_JSON_FILENAME = 'package.json'; -/** - * Parse a package specifier (in the form of name\@version) into name and version parts. - */ -function _parsePackageSpecifier(rawPackageSpecifier) { - rawPackageSpecifier = (rawPackageSpecifier || '').trim(); - const separatorIndex = rawPackageSpecifier.lastIndexOf('@'); - let name; - let version = undefined; - if (separatorIndex === 0) { + const RUSH_JSON_FILENAME = 'rush.json'; + const RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME = 'RUSH_TEMP_FOLDER'; + const INSTALL_RUN_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_LOCKFILE_PATH'; + const INSTALLED_FLAG_FILENAME = 'installed.flag'; + const NODE_MODULES_FOLDER_NAME = 'node_modules'; + const PACKAGE_JSON_FILENAME = 'package.json'; + /** + * Parse a package specifier (in the form of name\@version) into name and version parts. + */ + function _parsePackageSpecifier(rawPackageSpecifier) { + rawPackageSpecifier = (rawPackageSpecifier || '').trim(); + const separatorIndex = rawPackageSpecifier.lastIndexOf('@'); + let name; + let version = undefined; + if (separatorIndex === 0) { // The specifier starts with a scope and doesn't have a version specified name = rawPackageSpecifier; - } - else if (separatorIndex === -1) { + } else if (separatorIndex === -1) { // The specifier doesn't have a version name = rawPackageSpecifier; - } - else { + } else { name = rawPackageSpecifier.substring(0, separatorIndex); version = rawPackageSpecifier.substring(separatorIndex + 1); - } - if (!name) { + } + if (!name) { throw new Error(`Invalid package specifier: ${rawPackageSpecifier}`); - } - return { name, version }; -} -let _npmPath = undefined; -/** - * Get the absolute path to the npm executable - */ -function getNpmPath() { - if (!_npmPath) { + } + return { name, version }; + } + let _npmPath = undefined; + /** + * Get the absolute path to the npm executable + */ + function getNpmPath() { + if (!_npmPath) { try { - if (_isWindows()) { - // We're on Windows - const whereOutput = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('where npm', { stdio: [] }).toString(); - const lines = whereOutput.split(os__WEBPACK_IMPORTED_MODULE_2__.EOL).filter((line) => !!line); - // take the last result, we are looking for a .cmd command - // see https://github.com/microsoft/rushstack/issues/759 - _npmPath = lines[lines.length - 1]; - } - else { - // We aren't on Windows - assume we're on *NIX or Darwin - _npmPath = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('command -v npm', { stdio: [] }).toString(); - } - } - catch (e) { - throw new Error(`Unable to determine the path to the NPM tool: ${e}`); + if (_isWindows()) { + // We're on Windows + const whereOutput = child_process__WEBPACK_IMPORTED_MODULE_0__ + .execSync('where npm', { stdio: [] }) + .toString(); + const lines = whereOutput.split(os__WEBPACK_IMPORTED_MODULE_2__.EOL).filter((line) => !!line); + // take the last result, we are looking for a .cmd command + // see https://github.com/microsoft/rushstack/issues/759 + _npmPath = lines[lines.length - 1]; + } else { + // We aren't on Windows - assume we're on *NIX or Darwin + _npmPath = child_process__WEBPACK_IMPORTED_MODULE_0__ + .execSync('command -v npm', { stdio: [] }) + .toString(); + } + } catch (e) { + throw new Error(`Unable to determine the path to the NPM tool: ${e}`); } _npmPath = _npmPath.trim(); if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(_npmPath)) { - throw new Error('The NPM executable does not exist'); + throw new Error('The NPM executable does not exist'); } + } + return _npmPath; } - return _npmPath; -} -function _ensureFolder(folderPath) { - if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(folderPath)) { + function _ensureFolder(folderPath) { + if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(folderPath)) { const parentDir = path__WEBPACK_IMPORTED_MODULE_3__.dirname(folderPath); _ensureFolder(parentDir); fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(folderPath); - } -} -/** - * Create missing directories under the specified base directory, and return the resolved directory. - * - * Does not support "." or ".." path segments. - * Assumes the baseFolder exists. - */ -function _ensureAndJoinPath(baseFolder, ...pathSegments) { - let joinedPath = baseFolder; - try { + } + } + /** + * Create missing directories under the specified base directory, and return the resolved directory. + * + * Does not support "." or ".." path segments. + * Assumes the baseFolder exists. + */ + function _ensureAndJoinPath(baseFolder, ...pathSegments) { + let joinedPath = baseFolder; + try { for (let pathSegment of pathSegments) { - pathSegment = pathSegment.replace(/[\\\/]/g, '+'); - joinedPath = path__WEBPACK_IMPORTED_MODULE_3__.join(joinedPath, pathSegment); - if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(joinedPath)) { - fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(joinedPath); - } + pathSegment = pathSegment.replace(/[\\\/]/g, '+'); + joinedPath = path__WEBPACK_IMPORTED_MODULE_3__.join(joinedPath, pathSegment); + if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(joinedPath)) { + fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(joinedPath); + } } - } - catch (e) { - throw new Error(`Error building local installation folder (${path__WEBPACK_IMPORTED_MODULE_3__.join(baseFolder, ...pathSegments)}): ${e}`); - } - return joinedPath; -} -function _getRushTempFolder(rushCommonFolder) { - const rushTempFolder = process.env[RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME]; - if (rushTempFolder !== undefined) { + } catch (e) { + throw new Error( + `Error building local installation folder (${path__WEBPACK_IMPORTED_MODULE_3__.join(baseFolder, ...pathSegments)}): ${e}` + ); + } + return joinedPath; + } + function _getRushTempFolder(rushCommonFolder) { + const rushTempFolder = process.env[RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME]; + if (rushTempFolder !== undefined) { _ensureFolder(rushTempFolder); return rushTempFolder; - } - else { + } else { return _ensureAndJoinPath(rushCommonFolder, 'temp'); - } -} -/** - * Compare version strings according to semantic versioning. - * Returns a positive integer if "a" is a later version than "b", - * a negative integer if "b" is later than "a", - * and 0 otherwise. - */ -function _compareVersionStrings(a, b) { - const aParts = a.split(/[.-]/); - const bParts = b.split(/[.-]/); - const numberOfParts = Math.max(aParts.length, bParts.length); - for (let i = 0; i < numberOfParts; i++) { + } + } + /** + * Compare version strings according to semantic versioning. + * Returns a positive integer if "a" is a later version than "b", + * a negative integer if "b" is later than "a", + * and 0 otherwise. + */ + function _compareVersionStrings(a, b) { + const aParts = a.split(/[.-]/); + const bParts = b.split(/[.-]/); + const numberOfParts = Math.max(aParts.length, bParts.length); + for (let i = 0; i < numberOfParts; i++) { if (aParts[i] !== bParts[i]) { - return (Number(aParts[i]) || 0) - (Number(bParts[i]) || 0); + return (Number(aParts[i]) || 0) - (Number(bParts[i]) || 0); } - } - return 0; -} -/** - * Resolve a package specifier to a static version - */ -function _resolvePackageVersion(logger, rushCommonFolder, { name, version }) { - if (!version) { + } + return 0; + } + /** + * Resolve a package specifier to a static version + */ + function _resolvePackageVersion(logger, rushCommonFolder, { name, version }) { + if (!version) { version = '*'; // If no version is specified, use the latest version - } - if (version.match(/^[a-zA-Z0-9\-\+\.]+$/)) { + } + if (version.match(/^[a-zA-Z0-9\-\+\.]+$/)) { // If the version contains only characters that we recognize to be used in static version specifiers, // pass the version through return version; - } - else { + } else { // version resolves to try { - const rushTempFolder = _getRushTempFolder(rushCommonFolder); - const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush'); - (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({ - sourceNpmrcFolder, - targetNpmrcFolder: rushTempFolder, - logger - }); - const npmPath = getNpmPath(); - // This returns something that looks like: - // ``` - // [ - // "3.0.0", - // "3.0.1", - // ... - // "3.0.20" - // ] - // ``` - // - // if multiple versions match the selector, or - // - // ``` - // "3.0.0" - // ``` - // - // if only a single version matches. - const spawnSyncOptions = { - cwd: rushTempFolder, - stdio: [], - shell: _isWindows() - }; - const platformNpmPath = _getPlatformPath(npmPath); - const npmVersionSpawnResult = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier', '--json'], spawnSyncOptions); - if (npmVersionSpawnResult.status !== 0) { - throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`); - } - const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString(); - const parsedVersionOutput = JSON.parse(npmViewVersionOutput); - const versions = Array.isArray(parsedVersionOutput) - ? parsedVersionOutput - : [parsedVersionOutput]; - let latestVersion = versions[0]; - for (let i = 1; i < versions.length; i++) { - const latestVersionCandidate = versions[i]; - if (_compareVersionStrings(latestVersionCandidate, latestVersion) > 0) { - latestVersion = latestVersionCandidate; - } - } - if (!latestVersion) { - throw new Error('No versions found for the specified version range.'); + const rushTempFolder = _getRushTempFolder(rushCommonFolder); + const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join( + rushCommonFolder, + 'config', + 'rush' + ); + (0, _utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({ + sourceNpmrcFolder, + targetNpmrcFolder: rushTempFolder, + logger + }); + const npmPath = getNpmPath(); + // This returns something that looks like: + // ``` + // [ + // "3.0.0", + // "3.0.1", + // ... + // "3.0.20" + // ] + // ``` + // + // if multiple versions match the selector, or + // + // ``` + // "3.0.0" + // ``` + // + // if only a single version matches. + const spawnSyncOptions = { + cwd: rushTempFolder, + stdio: [], + shell: _isWindows() + }; + const platformNpmPath = _getPlatformPath(npmPath); + const npmVersionSpawnResult = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync( + platformNpmPath, + ['view', `${name}@${version}`, 'version', '--no-update-notifier', '--json'], + spawnSyncOptions + ); + if (npmVersionSpawnResult.status !== 0) { + throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`); + } + const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString(); + const parsedVersionOutput = JSON.parse(npmViewVersionOutput); + const versions = Array.isArray(parsedVersionOutput) ? parsedVersionOutput : [parsedVersionOutput]; + let latestVersion = versions[0]; + for (let i = 1; i < versions.length; i++) { + const latestVersionCandidate = versions[i]; + if (_compareVersionStrings(latestVersionCandidate, latestVersion) > 0) { + latestVersion = latestVersionCandidate; } - return latestVersion; - } - catch (e) { - throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`); + } + if (!latestVersion) { + throw new Error('No versions found for the specified version range.'); + } + return latestVersion; + } catch (e) { + throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`); } - } -} -let _rushJsonFolder; -/** - * Find the absolute path to the folder containing rush.json - */ -function findRushJsonFolder() { - if (!_rushJsonFolder) { + } + } + let _rushJsonFolder; + /** + * Find the absolute path to the folder containing rush.json + */ + function findRushJsonFolder() { + if (!_rushJsonFolder) { let basePath = __dirname; let tempPath = __dirname; do { - const testRushJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(basePath, RUSH_JSON_FILENAME); - if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(testRushJsonPath)) { - _rushJsonFolder = basePath; - break; - } - else { - basePath = tempPath; - } + const testRushJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(basePath, RUSH_JSON_FILENAME); + if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(testRushJsonPath)) { + _rushJsonFolder = basePath; + break; + } else { + basePath = tempPath; + } } while (basePath !== (tempPath = path__WEBPACK_IMPORTED_MODULE_3__.dirname(basePath))); // Exit the loop when we hit the disk root if (!_rushJsonFolder) { - throw new Error(`Unable to find ${RUSH_JSON_FILENAME}.`); + throw new Error(`Unable to find ${RUSH_JSON_FILENAME}.`); } - } - return _rushJsonFolder; -} -/** - * Detects if the package in the specified directory is installed - */ -function _isPackageAlreadyInstalled(packageInstallFolder) { - try { - const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME); + } + return _rushJsonFolder; + } + /** + * Detects if the package in the specified directory is installed + */ + function _isPackageAlreadyInstalled(packageInstallFolder) { + try { + const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join( + packageInstallFolder, + INSTALLED_FLAG_FILENAME + ); if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(flagFilePath)) { - return false; + return false; } const fileContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(flagFilePath).toString(); return fileContents.trim() === process.version; - } - catch (e) { + } catch (e) { return false; + } } -} -/** - * Delete a file. Fail silently if it does not exist. - */ -function _deleteFile(file) { - try { + /** + * Delete a file. Fail silently if it does not exist. + */ + function _deleteFile(file) { + try { fs__WEBPACK_IMPORTED_MODULE_1__.unlinkSync(file); - } - catch (err) { + } catch (err) { if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') { - throw err; + throw err; } - } -} -/** - * Removes the following files and directories under the specified folder path: - * - installed.flag - * - - * - node_modules - */ -function _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath) { - try { - const flagFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME); + } + } + /** + * Removes the following files and directories under the specified folder path: + * - installed.flag + * - + * - node_modules + */ + function _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath) { + try { + const flagFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve( + packageInstallFolder, + INSTALLED_FLAG_FILENAME + ); _deleteFile(flagFile); - const packageLockFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, 'package-lock.json'); + const packageLockFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve( + packageInstallFolder, + 'package-lock.json' + ); if (lockFilePath) { - fs__WEBPACK_IMPORTED_MODULE_1__.copyFileSync(lockFilePath, packageLockFile); - } - else { - // Not running `npm ci`, so need to cleanup - _deleteFile(packageLockFile); - const nodeModulesFolder = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME); - if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(nodeModulesFolder)) { - const rushRecyclerFolder = _ensureAndJoinPath(rushTempFolder, 'rush-recycler'); - fs__WEBPACK_IMPORTED_MODULE_1__.renameSync(nodeModulesFolder, path__WEBPACK_IMPORTED_MODULE_3__.join(rushRecyclerFolder, `install-run-${Date.now().toString()}`)); - } + fs__WEBPACK_IMPORTED_MODULE_1__.copyFileSync(lockFilePath, packageLockFile); + } else { + // Not running `npm ci`, so need to cleanup + _deleteFile(packageLockFile); + const nodeModulesFolder = path__WEBPACK_IMPORTED_MODULE_3__.resolve( + packageInstallFolder, + NODE_MODULES_FOLDER_NAME + ); + if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(nodeModulesFolder)) { + const rushRecyclerFolder = _ensureAndJoinPath(rushTempFolder, 'rush-recycler'); + fs__WEBPACK_IMPORTED_MODULE_1__.renameSync( + nodeModulesFolder, + path__WEBPACK_IMPORTED_MODULE_3__.join( + rushRecyclerFolder, + `install-run-${Date.now().toString()}` + ) + ); + } } - } - catch (e) { + } catch (e) { throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`); + } } -} -function _createPackageJson(packageInstallFolder, name, version) { - try { + function _createPackageJson(packageInstallFolder, name, version) { + try { const packageJsonContents = { - name: 'ci-rush', - version: '0.0.0', - dependencies: { - [name]: version - }, - description: "DON'T WARN", - repository: "DON'T WARN", - license: 'MIT' + name: 'ci-rush', + version: '0.0.0', + dependencies: { + [name]: version + }, + description: "DON'T WARN", + repository: "DON'T WARN", + license: 'MIT' }; - const packageJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, PACKAGE_JSON_FILENAME); - fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContents, undefined, 2)); - } - catch (e) { + const packageJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join( + packageInstallFolder, + PACKAGE_JSON_FILENAME + ); + fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync( + packageJsonPath, + JSON.stringify(packageJsonContents, undefined, 2) + ); + } catch (e) { throw new Error(`Unable to create package.json: ${e}`); + } } -} -/** - * Run "npm install" in the package install folder. - */ -function _installPackage(logger, packageInstallFolder, name, version, command) { - try { + /** + * Run "npm install" in the package install folder. + */ + function _installPackage(logger, packageInstallFolder, name, version, command) { + try { logger.info(`Installing ${name}...`); const npmPath = getNpmPath(); const platformNpmPath = _getPlatformPath(npmPath); const result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, [command], { - stdio: 'inherit', - cwd: packageInstallFolder, - env: process.env, - shell: _isWindows() + stdio: 'inherit', + cwd: packageInstallFolder, + env: process.env, + shell: _isWindows() }); if (result.status !== 0) { - throw new Error(`"npm ${command}" encountered an error`); + throw new Error(`"npm ${command}" encountered an error`); } logger.info(`Successfully installed ${name}@${version}`); - } - catch (e) { + } catch (e) { throw new Error(`Unable to install package: ${e}`); - } -} -/** - * Get the ".bin" path for the package. - */ -function _getBinPath(packageInstallFolder, binName) { - const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin'); - const resolvedBinName = _isWindows() ? `${binName}.cmd` : binName; - return path__WEBPACK_IMPORTED_MODULE_3__.resolve(binFolderPath, resolvedBinName); -} -/** - * Returns a cross-platform path - windows must enclose any path containing spaces within double quotes. - */ -function _getPlatformPath(platformPath) { - return _isWindows() && platformPath.includes(' ') ? `"${platformPath}"` : platformPath; -} -function _isWindows() { - return os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32'; -} -/** - * Write a flag file to the package's install directory, signifying that the install was successful. - */ -function _writeFlagFile(packageInstallFolder) { - try { - const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME); + } + } + /** + * Get the ".bin" path for the package. + */ + function _getBinPath(packageInstallFolder, binName) { + const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve( + packageInstallFolder, + NODE_MODULES_FOLDER_NAME, + '.bin' + ); + const resolvedBinName = _isWindows() ? `${binName}.cmd` : binName; + return path__WEBPACK_IMPORTED_MODULE_3__.resolve(binFolderPath, resolvedBinName); + } + /** + * Returns a cross-platform path - windows must enclose any path containing spaces within double quotes. + */ + function _getPlatformPath(platformPath) { + return _isWindows() && platformPath.includes(' ') ? `"${platformPath}"` : platformPath; + } + function _isWindows() { + return os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32'; + } + /** + * Write a flag file to the package's install directory, signifying that the install was successful. + */ + function _writeFlagFile(packageInstallFolder) { + try { + const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join( + packageInstallFolder, + INSTALLED_FLAG_FILENAME + ); fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(flagFilePath, process.version); - } - catch (e) { + } catch (e) { throw new Error(`Unable to create installed.flag file in ${packageInstallFolder}`); - } -} -function installAndRun(logger, packageName, packageVersion, packageBinName, packageBinArgs, lockFilePath = process.env[INSTALL_RUN_LOCKFILE_PATH_VARIABLE]) { - const rushJsonFolder = findRushJsonFolder(); - const rushCommonFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushJsonFolder, 'common'); - const rushTempFolder = _getRushTempFolder(rushCommonFolder); - const packageInstallFolder = _ensureAndJoinPath(rushTempFolder, 'install-run', `${packageName}@${packageVersion}`); - if (!_isPackageAlreadyInstalled(packageInstallFolder)) { + } + } + function installAndRun( + logger, + packageName, + packageVersion, + packageBinName, + packageBinArgs, + lockFilePath = process.env[INSTALL_RUN_LOCKFILE_PATH_VARIABLE] + ) { + const rushJsonFolder = findRushJsonFolder(); + const rushCommonFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushJsonFolder, 'common'); + const rushTempFolder = _getRushTempFolder(rushCommonFolder); + const packageInstallFolder = _ensureAndJoinPath( + rushTempFolder, + 'install-run', + `${packageName}@${packageVersion}` + ); + if (!_isPackageAlreadyInstalled(packageInstallFolder)) { // The package isn't already installed _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath); const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush'); - (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({ - sourceNpmrcFolder, - targetNpmrcFolder: packageInstallFolder, - logger + (0, _utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({ + sourceNpmrcFolder, + targetNpmrcFolder: packageInstallFolder, + logger }); _createPackageJson(packageInstallFolder, packageName, packageVersion); const command = lockFilePath ? 'ci' : 'install'; _installPackage(logger, packageInstallFolder, packageName, packageVersion, command); _writeFlagFile(packageInstallFolder); - } - const statusMessage = `Invoking "${packageBinName} ${packageBinArgs.join(' ')}"`; - const statusMessageLine = new Array(statusMessage.length + 1).join('-'); - logger.info('\n' + statusMessage + '\n' + statusMessageLine + '\n'); - const binPath = _getBinPath(packageInstallFolder, packageBinName); - const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin'); - // Windows environment variables are case-insensitive. Instead of using SpawnSyncOptions.env, we need to - // assign via the process.env proxy to ensure that we append to the right PATH key. - const originalEnvPath = process.env.PATH || ''; - let result; - try { + } + const statusMessage = `Invoking "${packageBinName} ${packageBinArgs.join(' ')}"`; + const statusMessageLine = new Array(statusMessage.length + 1).join('-'); + logger.info('\n' + statusMessage + '\n' + statusMessageLine + '\n'); + const binPath = _getBinPath(packageInstallFolder, packageBinName); + const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve( + packageInstallFolder, + NODE_MODULES_FOLDER_NAME, + '.bin' + ); + // Windows environment variables are case-insensitive. Instead of using SpawnSyncOptions.env, we need to + // assign via the process.env proxy to ensure that we append to the right PATH key. + const originalEnvPath = process.env.PATH || ''; + let result; + try { // `npm` bin stubs on Windows are `.cmd` files // Node.js will not directly invoke a `.cmd` file unless `shell` is set to `true` const platformBinPath = _getPlatformPath(binPath); process.env.PATH = [binFolderPath, originalEnvPath].join(path__WEBPACK_IMPORTED_MODULE_3__.delimiter); result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformBinPath, packageBinArgs, { - stdio: 'inherit', - windowsVerbatimArguments: false, - shell: _isWindows(), - cwd: process.cwd(), - env: process.env + stdio: 'inherit', + windowsVerbatimArguments: false, + shell: _isWindows(), + cwd: process.cwd(), + env: process.env }); - } - finally { + } finally { process.env.PATH = originalEnvPath; - } - if (result.status !== null) { + } + if (result.status !== null) { return result.status; - } - else { + } else { throw result.error || new Error('An unknown error occurred.'); + } } -} -function runWithErrorAndStatusCode(logger, fn) { - process.exitCode = 1; - try { + function runWithErrorAndStatusCode(logger, fn) { + process.exitCode = 1; + try { const exitCode = fn(); process.exitCode = exitCode; - } - catch (e) { + } catch (e) { logger.error('\n\n' + e.toString() + '\n\n'); - } -} -function _run() { - const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, rawPackageSpecifier /* qrcode@^1.2.0 */, packageBinName /* qrcode */, ...packageBinArgs /* [-f, myproject/lib] */] = process.argv; - if (!nodePath) { + } + } + function _run() { + const [ + nodePath /* Ex: /bin/node */, + scriptPath /* /repo/common/scripts/install-run-rush.js */, + rawPackageSpecifier /* qrcode@^1.2.0 */, + packageBinName /* qrcode */, + ...packageBinArgs /* [-f, myproject/lib] */ + ] = process.argv; + if (!nodePath) { throw new Error('Unexpected exception: could not detect node path'); - } - if (path__WEBPACK_IMPORTED_MODULE_3__.basename(scriptPath).toLowerCase() !== 'install-run.js') { + } + if (path__WEBPACK_IMPORTED_MODULE_3__.basename(scriptPath).toLowerCase() !== 'install-run.js') { // If install-run.js wasn't directly invoked, don't execute the rest of this function. Return control // to the script that (presumably) imported this file return; - } - if (process.argv.length < 4) { + } + if (process.argv.length < 4) { console.log('Usage: install-run.js @ [args...]'); console.log('Example: install-run.js qrcode@1.2.2 qrcode https://rushjs.io'); process.exit(1); - } - const logger = { info: console.log, error: console.error }; - runWithErrorAndStatusCode(logger, () => { + } + const logger = { info: console.log, error: console.error }; + runWithErrorAndStatusCode(logger, () => { const rushJsonFolder = findRushJsonFolder(); const rushCommonFolder = _ensureAndJoinPath(rushJsonFolder, 'common'); const packageSpecifier = _parsePackageSpecifier(rawPackageSpecifier); const name = packageSpecifier.name; const version = _resolvePackageVersion(logger, rushCommonFolder, packageSpecifier); if (packageSpecifier.version !== version) { - console.log(`Resolved to ${name}@${version}`); + console.log(`Resolved to ${name}@${version}`); } return installAndRun(logger, name, version, packageBinName, packageBinArgs); - }); -} -_run(); -//# sourceMappingURL=install-run.js.map -})(); + }); + } + _run(); + //# sourceMappingURL=install-run.js.map + })(); -module.exports = __webpack_exports__; -/******/ })() -; -//# sourceMappingURL=install-run.js.map \ No newline at end of file + module.exports = __webpack_exports__; + /******/ +})(); +//# sourceMappingURL=install-run.js.map diff --git a/common/autoinstallers/rush-prettier/package.json b/common/autoinstallers/rush-prettier/package.json index fbe65e10c09..2b3a8597862 100644 --- a/common/autoinstallers/rush-prettier/package.json +++ b/common/autoinstallers/rush-prettier/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "dependencies": { - "pretty-quick": "4.0.0", - "prettier": "3.2.5" + "pretty-quick": "4.2.2", + "prettier": "3.6.2" } } diff --git a/common/autoinstallers/rush-prettier/pnpm-lock.yaml b/common/autoinstallers/rush-prettier/pnpm-lock.yaml index f5ea674ea1c..94e04d1885c 100644 --- a/common/autoinstallers/rush-prettier/pnpm-lock.yaml +++ b/common/autoinstallers/rush-prettier/pnpm-lock.yaml @@ -6,195 +6,65 @@ settings: dependencies: prettier: - specifier: 3.2.5 - version: 3.2.5 + specifier: 3.6.2 + version: 3.6.2 pretty-quick: - specifier: 4.0.0 - version: 4.0.0(prettier@3.2.5) + specifier: 4.2.2 + version: 4.2.2(prettier@3.6.2) packages: - /cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - dev: false - - /execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - dev: false - - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 + /@pkgr/core@0.2.9: + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} dev: false - /get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - dev: false - - /human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - dev: false - - /ignore@5.3.1: - resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + /ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} dev: false - /is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - dev: false - - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: false - - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - dependencies: - p-locate: 5.0.0 - dev: false - - /merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - dev: false - - /mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - dev: false - /mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} dev: false - /npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - dependencies: - path-key: 3.1.1 + /picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} dev: false - /onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - dependencies: - mimic-fn: 2.1.0 + /picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} dev: false - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - dependencies: - yocto-queue: 0.1.0 - dev: false - - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - dependencies: - p-limit: 3.1.0 - dev: false - - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - dev: false - - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - dev: false - - /picocolors@1.0.1: - resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} - dev: false - - /picomatch@3.0.1: - resolution: {integrity: sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==} - engines: {node: '>=10'} - dev: false - - /prettier@3.2.5: - resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + /prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} engines: {node: '>=14'} hasBin: true dev: false - /pretty-quick@4.0.0(prettier@3.2.5): - resolution: {integrity: sha512-M+2MmeufXb/M7Xw3Afh1gxcYpj+sK0AxEfnfF958ktFeAyi5MsKY5brymVURQLgPLV1QaF5P4pb2oFJ54H3yzQ==} + /pretty-quick@4.2.2(prettier@3.6.2): + resolution: {integrity: sha512-uAh96tBW1SsD34VhhDmWuEmqbpfYc/B3j++5MC/6b3Cb8Ow7NJsvKFhg0eoGu2xXX+o9RkahkTK6sUdd8E7g5w==} engines: {node: '>=14'} hasBin: true peerDependencies: prettier: ^3.0.0 dependencies: - execa: 5.1.1 - find-up: 5.0.0 - ignore: 5.3.1 + '@pkgr/core': 0.2.9 + ignore: 7.0.5 mri: 1.2.0 - picocolors: 1.0.1 - picomatch: 3.0.1 - prettier: 3.2.5 - tslib: 2.6.2 + picocolors: 1.1.1 + picomatch: 4.0.3 + prettier: 3.6.2 + tinyexec: 0.3.2 + tslib: 2.8.1 dev: false - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - dependencies: - shebang-regex: 3.0.0 - dev: false - - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - dev: false - - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: false - - /strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - dev: false - - /tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - dev: false - - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - dependencies: - isexe: 2.0.0 + /tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} dev: false - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} + /tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} dev: false diff --git a/common/changes/@microsoft/rush/copilot-fix-7b47235e-8acb-4963-a955-6ee097ae90bc_2025-09-16-21-58.json b/common/changes/@microsoft/rush/copilot-fix-7b47235e-8acb-4963-a955-6ee097ae90bc_2025-09-16-21-58.json new file mode 100644 index 00000000000..8b220b0dc7a --- /dev/null +++ b/common/changes/@microsoft/rush/copilot-fix-7b47235e-8acb-4963-a955-6ee097ae90bc_2025-09-16-21-58.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Replace uuid package dependency with Node.js built-in crypto.randomUUID", + "type": "none", + "packageName": "@microsoft/rush" + } + ], + "packageName": "@microsoft/rush", + "email": "198982749+Copilot@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@microsoft/rush/eb-concurrency-bug-fix_2025-09-16-19-06.json b/common/changes/@microsoft/rush/eb-concurrency-bug-fix_2025-09-16-19-06.json new file mode 100644 index 00000000000..12fe1d7e9c5 --- /dev/null +++ b/common/changes/@microsoft/rush/eb-concurrency-bug-fix_2025-09-16-19-06.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add an `allowOversubscription` option to the command definitions in `common/config/rush/command-line.json` to prevent running tasks from exceeding concurrency.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/changes/@microsoft/rush/fix-rush-lib-external_2025-09-17-20-55.json b/common/changes/@microsoft/rush/fix-rush-lib-external_2025-09-17-20-55.json new file mode 100644 index 00000000000..05af2435f26 --- /dev/null +++ b/common/changes/@microsoft/rush/fix-rush-lib-external_2025-09-17-20-55.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "[rush-resolver-cache] Ensure that the correct version of rush-lib is loaded when the global version doesn't match the repository version.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/changes/@microsoft/rush/octogonz-lfx-fixes4_2025-09-21-19-46.json b/common/changes/@microsoft/rush/octogonz-lfx-fixes4_2025-09-21-19-46.json new file mode 100644 index 00000000000..bd7ff97cb34 --- /dev/null +++ b/common/changes/@microsoft/rush/octogonz-lfx-fixes4_2025-09-21-19-46.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/changes/@microsoft/rush/rush-serve-dependencies_2025-09-16-23-56.json b/common/changes/@microsoft/rush/rush-serve-dependencies_2025-09-16-23-56.json new file mode 100644 index 00000000000..d4fa959660d --- /dev/null +++ b/common/changes/@microsoft/rush/rush-serve-dependencies_2025-09-16-23-56.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "[rush-serve-plugin] Support aborting execution via Web Socket. Include information about the dependencies of operations in messages to the client..", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-isolated-typescript-transpile-plugin/octogonz-lfx-fixes4_2025-09-21-19-46.json b/common/changes/@rushstack/heft-isolated-typescript-transpile-plugin/octogonz-lfx-fixes4_2025-09-21-19-46.json new file mode 100644 index 00000000000..fbd4c12b6b9 --- /dev/null +++ b/common/changes/@rushstack/heft-isolated-typescript-transpile-plugin/octogonz-lfx-fixes4_2025-09-21-19-46.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-isolated-typescript-transpile-plugin", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/heft-isolated-typescript-transpile-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes3_2025-09-16-11-01.json b/common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes3_2025-09-16-11-01.json new file mode 100644 index 00000000000..7b288ff1b92 --- /dev/null +++ b/common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes3_2025-09-16-11-01.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/lockfile-explorer", + "comment": "Improve support for PNPM lockfile format V6.0", + "type": "patch" + } + ], + "packageName": "@rushstack/lockfile-explorer" +} \ No newline at end of file diff --git a/common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes4_2025-09-17-17-57.json b/common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes4_2025-09-17-17-57.json new file mode 100644 index 00000000000..873badc1a71 --- /dev/null +++ b/common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes4_2025-09-17-17-57.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/lockfile-explorer", + "comment": "Add syntax highlighter", + "type": "minor" + } + ], + "packageName": "@rushstack/lockfile-explorer" +} \ No newline at end of file diff --git a/common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes4_2025-09-17-17-58.json b/common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes4_2025-09-17-17-58.json new file mode 100644 index 00000000000..8c6e6ab35a0 --- /dev/null +++ b/common/changes/@rushstack/lockfile-explorer/octogonz-lfx-fixes4_2025-09-17-17-58.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/lockfile-explorer", + "comment": "Isolate .pnpmcfile.cjs evaluation", + "type": "minor" + } + ], + "packageName": "@rushstack/lockfile-explorer" +} \ No newline at end of file diff --git a/common/changes/@rushstack/node-core-library/eb-concurrency-bug-fix_2025-09-11-15-24.json b/common/changes/@rushstack/node-core-library/eb-concurrency-bug-fix_2025-09-11-15-24.json new file mode 100644 index 00000000000..6b24dd9f761 --- /dev/null +++ b/common/changes/@rushstack/node-core-library/eb-concurrency-bug-fix_2025-09-11-15-24.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/node-core-library", + "comment": "Add an `allowOversubscription` option to the `Async` API functions which prevents running tasks from exceeding concurrency. Change its default to `false`.", + "type": "minor" + } + ], + "packageName": "@rushstack/node-core-library" +} \ No newline at end of file diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json index 15e248ddeba..103baa6d943 100644 --- a/common/config/rush/browser-approved-packages.json +++ b/common/config/rush/browser-approved-packages.json @@ -62,6 +62,10 @@ "name": "office-ui-fabric-core", "allowedCategories": [ "libraries" ] }, + { + "name": "prism-react-renderer", + "allowedCategories": [ "libraries" ] + }, { "name": "react", "allowedCategories": [ "libraries", "tests", "vscode-extensions" ] diff --git a/common/config/rush/nonbrowser-approved-packages.json b/common/config/rush/nonbrowser-approved-packages.json index 9f344afc501..b2dde4e55ed 100644 --- a/common/config/rush/nonbrowser-approved-packages.json +++ b/common/config/rush/nonbrowser-approved-packages.json @@ -114,6 +114,10 @@ "name": "@pnpm/logger", "allowedCategories": [ "libraries" ] }, + { + "name": "@pnpm/types", + "allowedCategories": [ "libraries" ] + }, { "name": "@redis/client", "allowedCategories": [ "libraries" ] diff --git a/common/config/subspaces/build-tests-subspace/repo-state.json b/common/config/subspaces/build-tests-subspace/repo-state.json index df8d3168341..c0a266ac12c 100644 --- a/common/config/subspaces/build-tests-subspace/repo-state.json +++ b/common/config/subspaces/build-tests-subspace/repo-state.json @@ -2,5 +2,5 @@ { "pnpmShrinkwrapHash": "f89693a88037554bf0c35db4f2295ef771cd2a71", "preferredVersionsHash": "550b4cee0bef4e97db6c6aad726df5149d20e7d9", - "packageJsonInjectedDependenciesHash": "364e001eac655a92be31ddb4bbf0d8b291d1e9cc" + "packageJsonInjectedDependenciesHash": "2fad9cbc4726f383da294e793c5b891d8775fca6" } diff --git a/common/config/subspaces/default/common-versions.json b/common/config/subspaces/default/common-versions.json index bdf76be499b..080e192c513 100644 --- a/common/config/subspaces/default/common-versions.json +++ b/common/config/subspaces/default/common-versions.json @@ -78,6 +78,9 @@ * This design avoids unnecessary churn in this file. */ "allowedAlternativeVersions": { + // Allow Lockfile Explorer to support PNPM 9.x + // TODO: Remove this after Rush adds support for PNPM 9.x + "@pnpm/lockfile.types": ["1002.0.1"], "@typescript-eslint/parser": [ "~6.19.0" // Used by build-tests/eslint-7(-*)-test / build-tests/eslint-bulk-suppressions-test-legacy ], diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index b19882175df..fa35e21fefc 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -193,9 +193,6 @@ importers: ../../../apps/lockfile-explorer: dependencies: - '@lifaon/path': - specifier: ~2.1.0 - version: 2.1.0 '@microsoft/rush-lib': specifier: workspace:* version: link:../../libraries/rush-lib @@ -236,9 +233,12 @@ importers: specifier: ~5.1.0 version: 5.1.0 devDependencies: - '@pnpm/lockfile-types': - specifier: ^5.1.5 - version: 5.1.5 + '@pnpm/lockfile.types': + specifier: 1002.0.1 + version: 1002.0.1 + '@pnpm/types': + specifier: 1000.8.0 + version: 1000.8.0 '@rushstack/heft': specifier: workspace:* version: link:../heft @@ -275,6 +275,9 @@ importers: '@rushstack/rush-themed-ui': specifier: workspace:* version: link:../../libraries/rush-themed-ui + prism-react-renderer: + specifier: ~2.4.1 + version: 2.4.1(react@17.0.2) react: specifier: ~17.0.2 version: 17.0.2 @@ -3668,9 +3671,6 @@ importers: true-case-path: specifier: ~2.2.1 version: 2.2.1 - uuid: - specifier: ~8.3.2 - version: 8.3.2 devDependencies: '@pnpm/lockfile.types': specifier: ~1.0.3 @@ -3720,9 +3720,6 @@ importers: '@types/tar': specifier: 6.1.6 version: 6.1.6 - '@types/uuid': - specifier: ~8.3.4 - version: 8.3.4 '@types/webpack-env': specifier: 1.18.8 version: 1.18.8 @@ -10171,10 +10168,6 @@ packages: resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==} dev: false - /@lifaon/path@2.1.0: - resolution: {integrity: sha512-E+eJpDdwenIQCaYMMuCnteR34qAvXtHhHKjZOPB+hK4+R1yGcmWLLAEl2aklxCHx6w5VCKc8imx9AT05FGHhBw==} - dev: false - /@mdx-js/loader@1.6.22(react@17.0.2): resolution: {integrity: sha512-9CjGwy595NaxAYp0hF9B/A0lH6C8Rms97e2JS9d3jVUtILn6pT5i5IV965ra3lIWc7Rs1GG1tBdVF7dCowYe6Q==} dependencies: @@ -10491,13 +10484,6 @@ packages: ramda: 0.27.2 dev: false - /@pnpm/lockfile-types@5.1.5: - resolution: {integrity: sha512-02FP0HynzX+2DcuPtuMy7PH+kLIC0pevAydAOK+zug2bwdlSLErlvSkc+4+3dw60eRWgUXUqyfO2eR/Ansdbng==} - engines: {node: '>=16.14'} - dependencies: - '@pnpm/types': 9.4.2 - dev: true - /@pnpm/lockfile.types@1.0.3: resolution: {integrity: sha512-A7vUWktnhDkrIs+WmXm7AdffJVyVYJpQUEouya/DYhB+Y+tQ3BXjZ6CV0KybqLgI/8AZErgCJqFxA0GJH6QDjA==} engines: {node: '>=18.12'} @@ -10505,6 +10491,15 @@ packages: '@pnpm/patching.types': 1.0.0 '@pnpm/types': 12.2.0 + /@pnpm/lockfile.types@1002.0.1: + resolution: {integrity: sha512-anzBtzb78rf2KRExS8R38v4nyiU7b9ZMUsyzRdWpo+rfCmLUupjIxvasVlDgsf5pV7tbcBPASOamQ2G5V8IGAQ==} + engines: {node: '>=18.12'} + dependencies: + '@pnpm/patching.types': 1000.1.0 + '@pnpm/resolver-base': 1005.0.1 + '@pnpm/types': 1000.8.0 + dev: true + /@pnpm/logger@4.0.0: resolution: {integrity: sha512-SIShw+k556e7S7tLZFVSIHjCdiVog1qWzcKW2RbLEHPItdisAFVNIe34kYd9fMSswTlSRLS/qRjw3ZblzWmJ9Q==} engines: {node: '>=12.17'} @@ -10526,6 +10521,11 @@ packages: resolution: {integrity: sha512-juCdQCC1USqLcOhVPl1tYReoTO9YH4fTullMnFXXcmpsDM7Dkn3tzuOQKC3oPoJ2ozv+0EeWWMtMGqn2+IM3pQ==} engines: {node: '>=18.12'} + /@pnpm/patching.types@1000.1.0: + resolution: {integrity: sha512-Zib2ysLctRnWM4KXXlljR44qSKwyEqYmLk+8VPBDBEK3l5Gp5mT3N4ix9E4qjYynvFqahumsxzOfxOYQhUGMGw==} + engines: {node: '>=18.12'} + dev: true + /@pnpm/read-modules-dir@2.0.3: resolution: {integrity: sha512-i9OgRvSlxrTS9a2oXokhDxvQzDtfqtsooJ9jaGoHkznue5aFCTSrNZFQ6M18o8hC03QWfnxaKi0BtOvNkKu2+A==} engines: {node: '>=10.13'} @@ -10561,11 +10561,23 @@ packages: strip-bom: 4.0.0 dev: false + /@pnpm/resolver-base@1005.0.1: + resolution: {integrity: sha512-NBha12KjFMKwaG1BWTCtgr/RprNQhXItCBkzc8jZuVU0itAHRQhEykexna9K8XjAtYxZ9rhvir0T5a7fTB23yQ==} + engines: {node: '>=18.12'} + dependencies: + '@pnpm/types': 1000.8.0 + dev: true + /@pnpm/types@1000.6.0: resolution: {integrity: sha512-6PsMNe98VKPGcg6LnXSW/LE3YfJ77nj+bPKiRjYRWAQLZ+xXjEQRaR0dAuyjCmchlv4wR/hpnMVRS21/fCod5w==} engines: {node: '>=18.12'} dev: false + /@pnpm/types@1000.8.0: + resolution: {integrity: sha512-yx86CGHHquWAI0GgKIuV/RnYewcf5fVFZemC45C/K2cX0uV8GB8TUP541ZrokWola2fZx5sn1vL7xzbceRZfoQ==} + engines: {node: '>=18.12'} + dev: true + /@pnpm/types@12.2.0: resolution: {integrity: sha512-5RtwWhX39j89/Tmyv2QSlpiNjErA357T/8r1Dkg+2lD3P7RuS7Xi2tChvmOC3VlezEFNcWnEGCOeKoGRkDuqFA==} engines: {node: '>=18.12'} @@ -10583,6 +10595,7 @@ packages: /@pnpm/types@9.4.2: resolution: {integrity: sha512-g1hcF8Nv4gd76POilz9gD4LITAPXOe5nX4ijgr8ixCbLQZfcpYiMfJ+C1RlMNRUDo8vhlNB4O3bUlxmT6EAQXA==} engines: {node: '>=16.14'} + dev: false /@pnpm/write-project-manifest@1.1.7: resolution: {integrity: sha512-OLkDZSqkA1mkoPNPvLFXyI6fb0enCuFji6Zfditi/CLAo9kmIhQFmEUDu4krSB8i908EljG8YwL5Xjxzm5wsWA==} @@ -14035,6 +14048,10 @@ packages: resolution: {integrity: sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==} dev: true + /@types/prismjs@1.26.5: + resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} + dev: false + /@types/prop-types@15.7.11: resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} @@ -14190,10 +14207,6 @@ packages: resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} dev: false - /@types/uuid@8.3.4: - resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} - dev: true - /@types/vscode@1.103.0: resolution: {integrity: sha512-o4hanZAQdNfsKecexq9L3eHICd0AAvdbLk6hA60UzGXbGH/q8b/9xv2RgR7vV3ZcHuyKVq7b37IGd/+gM4Tu+Q==} dev: true @@ -17239,6 +17252,11 @@ packages: engines: {node: '>=6'} dev: true + /clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + dev: false + /cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} @@ -26146,6 +26164,16 @@ packages: engines: {node: '>= 0.8'} dev: true + /prism-react-renderer@2.4.1(react@17.0.2): + resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==} + peerDependencies: + react: '>=16.0.0' + dependencies: + '@types/prismjs': 1.26.5 + clsx: 2.1.1 + react: 17.0.2 + dev: false + /prismjs@1.27.0: resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==} engines: {node: '>=6'} diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index 3c852d397ea..711e482da42 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "43e8674ca74b9c3f20cf12f03de5ce2968017331", + "pnpmShrinkwrapHash": "b3b0018c5869d606a645e2b69ef6c53f9d2bf483", "preferredVersionsHash": "61cd419c533464b580f653eb5f5a7e27fe7055ca" } diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index a0fc83dccdc..b5d7827f474 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -237,6 +237,7 @@ export type FolderItem = nodeFs.Dirent; // @public export interface IAsyncParallelismOptions { + allowOversubscription?: boolean; concurrency?: number; weighted?: boolean; } diff --git a/heft-plugins/heft-isolated-typescript-transpile-plugin/src/SwcIsolatedTranspilePlugin.ts b/heft-plugins/heft-isolated-typescript-transpile-plugin/src/SwcIsolatedTranspilePlugin.ts index 4d281846216..4ac859ef33c 100644 --- a/heft-plugins/heft-isolated-typescript-transpile-plugin/src/SwcIsolatedTranspilePlugin.ts +++ b/heft-plugins/heft-isolated-typescript-transpile-plugin/src/SwcIsolatedTranspilePlugin.ts @@ -290,10 +290,10 @@ async function transpileProjectAsync( // https://github.com/swc-project/swc-node/blob/e6cd8b83d1ce76a0abf770f52425704e5d2872c6/packages/register/read-default-tsconfig.ts#L131C7-L139C20 const react: Partial | undefined = - tsConfigOptions.jsxFactory ?? + (tsConfigOptions.jsxFactory ?? tsConfigOptions.jsxFragmentFactory ?? tsConfigOptions.jsx ?? - tsConfigOptions.jsxImportSource + tsConfigOptions.jsxImportSource) ? { pragma: tsConfigOptions.jsxFactory, pragmaFrag: tsConfigOptions.jsxFragmentFactory, diff --git a/libraries/node-core-library/src/Async.ts b/libraries/node-core-library/src/Async.ts index 85d0b0fcdb0..9fb6bb9fd08 100644 --- a/libraries/node-core-library/src/Async.ts +++ b/libraries/node-core-library/src/Async.ts @@ -19,10 +19,28 @@ export interface IAsyncParallelismOptions { concurrency?: number; /** - * Optionally used with the {@link (Async:class).(forEachAsync:2)} to enable weighted operations where an operation can - * take up more or less than one concurrency unit. + * Optionally used with the {@link (Async:class).(forEachAsync:2)} to enable weighted operations where an + * operation can take up more or less than one concurrency unit. */ weighted?: boolean; + + /** + * This option affects the handling of task weights, applying a softer policy that favors maximizing parallelism + * instead of avoiding overload. + * + * @remarks + * By default, a new task cannot start executing if doing so would push the total weight above the concurrency limit. + * Set `allowOversubscription` to true to relax this rule, allowing a new task to start as long as the current + * total weight is below the concurrency limit. Either way, a task cannot start if the total weight already equals + * the concurrency limit; therefore, `allowOversubscription` has no effect when all tasks have weight 1. + * + * Example: Suppose the concurrency limit is 8, and seven tasks are running whose weights are 1, so the current + * total weight is 7. If an available task has weight 2, that would push the total weight to 9, exceeding + * the limit. This task can start only if `allowOversubscription` is true. + * + * @defaultValue false + */ + allowOversubscription?: boolean; } /** @@ -201,6 +219,8 @@ export class Async { let arrayIndex: number = 0; let iteratorIsComplete: boolean = false; let promiseHasResolvedOrRejected: boolean = false; + // iterator that is stored when the loop exits early due to not enough concurrency + let nextIterator: IteratorResult | undefined = undefined; async function queueOperationsAsync(): Promise { while ( @@ -213,7 +233,7 @@ export class Async { // there will be effectively no cap on the number of operations waiting. const limitedConcurrency: number = !Number.isFinite(concurrency) ? 1 : concurrency; concurrentUnitsInProgress += limitedConcurrency; - const currentIteratorResult: IteratorResult = await iterator.next(); + const currentIteratorResult: IteratorResult = nextIterator ?? (await iterator.next()); // eslint-disable-next-line require-atomic-updates iteratorIsComplete = !!currentIteratorResult.done; @@ -225,9 +245,21 @@ export class Async { // Remove the "lock" from the concurrency check and only apply the current weight. // This should allow other operations to execute. - concurrentUnitsInProgress += weight; concurrentUnitsInProgress -= limitedConcurrency; + // Wait until there's enough capacity to run this job, this function will be re-entered as tasks call `onOperationCompletionAsync` + const wouldExceedConcurrency: boolean = concurrentUnitsInProgress + weight > concurrency; + const allowOversubscription: boolean = options?.allowOversubscription ?? false; + if (!allowOversubscription && wouldExceedConcurrency) { + // eslint-disable-next-line require-atomic-updates + nextIterator = currentIteratorResult; + break; + } + + // eslint-disable-next-line require-atomic-updates + nextIterator = undefined; + concurrentUnitsInProgress += weight; + Promise.resolve(callback(currentIteratorValue.element, arrayIndex++)) .then(async () => { // Remove the operation completely from the in progress units. @@ -306,6 +338,7 @@ export class Async { * number of concurrency units that can be in progress at once. The weight of each operation * determines how many concurrency units it takes up. For example, if the concurrency is 2 * and the first operation has a weight of 2, then only one more operation can be in progress. + * Operations may exceed the concurrency limit based on the `allowOversubscription` option. * * If `callback` throws a synchronous exception, or if it returns a promise that rejects, * then the loop stops immediately. Any remaining array items will be skipped, and diff --git a/libraries/node-core-library/src/test/Async.test.ts b/libraries/node-core-library/src/test/Async.test.ts index 5442723e8b2..c7c5468bc9d 100644 --- a/libraries/node-core-library/src/test/Async.test.ts +++ b/libraries/node-core-library/src/test/Async.test.ts @@ -3,6 +3,11 @@ import { Async, AsyncQueue } from '../Async'; +interface INumberWithWeight { + n: number; + weight: number; +} + describe(Async.name, () => { describe(Async.mapAsync.name, () => { it('handles an empty array correctly', async () => { @@ -27,13 +32,6 @@ describe(Async.name, () => { expect(fn).toHaveBeenNthCalledWith(3, 3, 2); }); - it('returns the same result as built-in Promise.all', async () => { - const array: number[] = [1, 2, 3, 4, 5, 6, 7, 8]; - const fn: (item: number) => Promise = async (item) => `result ${item}`; - - expect(await Async.mapAsync(array, fn)).toEqual(await Promise.all(array.map(fn))); - }); - it('if concurrency is set, ensures no more than N operations occur in parallel', async () => { let running: number = 0; let maxRunning: number = 0; @@ -61,6 +59,31 @@ describe(Async.name, () => { expect(maxRunning).toEqual(3); }); + it('respects concurrency limit with allowOversubscription=false in mapAsync', async () => { + const array: INumberWithWeight[] = [ + { n: 1, weight: 2 }, + { n: 2, weight: 2 } + ]; + + let running = 0; + let maxRunning = 0; + + const result = await Async.mapAsync( + array, + async (item) => { + running++; + maxRunning = Math.max(maxRunning, running); + await Async.sleepAsync(0); + running--; + return `result-${item.n}`; + }, + { concurrency: 3, weighted: true, allowOversubscription: false } + ); + + expect(result).toEqual(['result-1', 'result-2']); + expect(maxRunning).toEqual(1); + }); + it('rejects if a sync iterator throws an error', async () => { const expectedError: Error = new Error('iterator error'); let iteratorIndex: number = 0; @@ -314,11 +337,6 @@ describe(Async.name, () => { ).rejects.toThrow(expectedError); }); - interface INumberWithWeight { - n: number; - weight: number; - } - it('handles an empty array correctly', async () => { let running: number = 0; let maxRunning: number = 0; @@ -469,7 +487,7 @@ describe(Async.name, () => { running--; }); - await Async.forEachAsync(array, fn, { concurrency: 3, weighted: true }); + await Async.forEachAsync(array, fn, { concurrency: 3, weighted: true, allowOversubscription: true }); expect(fn).toHaveBeenCalledTimes(8); expect(maxRunning).toEqual(2); }); @@ -542,6 +560,10 @@ describe(Async.name, () => { }); describe(Async.runWithRetriesAsync.name, () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + it('Correctly handles a sync function that succeeds the first time', async () => { const expectedResult: string = 'RESULT'; const result: string = await Async.runWithRetriesAsync({ action: () => expectedResult, maxRetries: 0 }); @@ -688,6 +710,142 @@ describe(Async.name, () => { expect(sleepSpy).toHaveBeenCalledTimes(1); expect(sleepSpy).toHaveBeenLastCalledWith(5); }); + + describe('allowOversubscription=false operations', () => { + it.each([ + { + concurrency: 4, + weight: 4, + expectedConcurrency: 1, + numberOfTasks: 4 + }, + { + concurrency: 4, + weight: 1, + expectedConcurrency: 4, + numberOfTasks: 4 + }, + { + concurrency: 4, + weight: 5, + expectedConcurrency: 1, + numberOfTasks: 2 + } + ])( + 'enforces strict concurrency limits when allowOversubscription=false: concurrency=$concurrency, weight=$weight, expects max $expectedConcurrency concurrent operations', + async ({ concurrency, weight, expectedConcurrency, numberOfTasks }) => { + let running: number = 0; + let maxRunning: number = 0; + + const array: INumberWithWeight[] = Array.from({ length: numberOfTasks }, (v, i) => i).map((n) => ({ + n, + weight + })); + + const fn: (item: INumberWithWeight) => Promise = jest.fn(async () => { + running++; + await Async.sleepAsync(0); + maxRunning = Math.max(maxRunning, running); + running--; + }); + + await Async.forEachAsync(array, fn, { concurrency, weighted: true, allowOversubscription: false }); + expect(fn).toHaveBeenCalledTimes(numberOfTasks); + expect(maxRunning).toEqual(expectedConcurrency); + } + ); + + it('waits for a small and large operation to finish before scheduling more', async () => { + let running: number = 0; + let maxRunning: number = 0; + + const array: INumberWithWeight[] = [ + { n: 1, weight: 1 }, + { n: 2, weight: 10 }, + { n: 3, weight: 1 }, + { n: 4, weight: 10 }, + { n: 5, weight: 1 }, + { n: 6, weight: 10 }, + { n: 7, weight: 1 }, + { n: 8, weight: 10 } + ]; + + const fn: (item: INumberWithWeight) => Promise = jest.fn(async (item) => { + running++; + await Async.sleepAsync(0); + maxRunning = Math.max(maxRunning, running); + running--; + }); + + await Async.forEachAsync(array, fn, { concurrency: 3, weighted: true, allowOversubscription: false }); + expect(fn).toHaveBeenCalledTimes(8); + expect(maxRunning).toEqual(1); + }); + + it('handles operation with mixed weights', async () => { + const concurrency: number = 3; + let running: number = 0; + let maxRunning: number = 0; + const taskToMaxConcurrency: Record = {}; + + const array: INumberWithWeight[] = [ + { n: 1, weight: 1 }, + { n: 2, weight: 2 }, + { n: 3, weight: concurrency }, + { n: 4, weight: 1 }, + { n: 5, weight: 1 } + ]; + + const fn: (item: INumberWithWeight) => Promise = jest.fn(async (item) => { + running++; + taskToMaxConcurrency[item.n] = running; + await Async.sleepAsync(0); + maxRunning = Math.max(maxRunning, running); + running--; + }); + + await Async.forEachAsync(array, fn, { concurrency, weighted: true, allowOversubscription: false }); + expect(fn).toHaveBeenCalledTimes(5); + expect(maxRunning).toEqual(2); + + expect(taskToMaxConcurrency[1]).toEqual(1); // task 1 + expect(taskToMaxConcurrency[2]).toEqual(2); // task 1 + 2 + expect(taskToMaxConcurrency[3]).toEqual(1); // task 3 + expect(taskToMaxConcurrency[4]).toEqual(1); // task 4 + expect(taskToMaxConcurrency[5]).toEqual(2); // task 4 + 5 + }); + + it('allows operations with weight 0 to be picked up when system is at max concurrency', async () => { + let running: number = 0; + let maxRunning: number = 0; + const taskToMaxConcurrency: Record = {}; + + const array: INumberWithWeight[] = [ + { n: 1, weight: 1 }, + { n: 2, weight: 0 }, + { n: 3, weight: 3 }, + { n: 4, weight: 1 } + ]; + + const fn: (item: INumberWithWeight) => Promise = jest.fn(async (item) => { + running++; + taskToMaxConcurrency[item.n] = running; + maxRunning = Math.max(maxRunning, running); + await Async.sleepAsync(0); + running--; + }); + + await Async.forEachAsync(array, fn, { concurrency: 3, weighted: true, allowOversubscription: false }); + + expect(fn).toHaveBeenCalledTimes(4); + expect(maxRunning).toEqual(2); + + expect(taskToMaxConcurrency[1]).toEqual(1); // task 1 + expect(taskToMaxConcurrency[2]).toEqual(2); // task 1 + 2 + expect(taskToMaxConcurrency[3]).toEqual(2); // task 2 + 3 + expect(taskToMaxConcurrency[4]).toEqual(1); // task 4 + }); + }); }); }); diff --git a/libraries/rush-lib/assets/rush-init/common/config/rush/command-line.json b/libraries/rush-lib/assets/rush-init/common/config/rush/command-line.json index 8b972ba7734..3760029c00d 100644 --- a/libraries/rush-lib/assets/rush-init/common/config/rush/command-line.json +++ b/libraries/rush-lib/assets/rush-init/common/config/rush/command-line.json @@ -77,6 +77,14 @@ */ "enableParallelism": false, + /** + * Controls whether weighted operations can start when the total weight would exceed the limit + * but is currently below the limit. This setting only applies when "enableParallelism" is true + * and operations have a "weight" property configured in their rush-project.json "operationSettings". + * Choose true (the default) to favor parallelism. Choose false to strictly stay under the limit. + */ + "allowOversubscription": false, + /** * Normally projects will be processed according to their dependency order: a given project will not start * processing the command until all of its dependencies have completed. This restriction doesn't apply for diff --git a/libraries/rush-lib/package.json b/libraries/rush-lib/package.json index 1c5090d7350..0ce5842b6a6 100644 --- a/libraries/rush-lib/package.json +++ b/libraries/rush-lib/package.json @@ -65,8 +65,7 @@ "strict-uri-encode": "~2.0.0", "tapable": "2.2.1", "tar": "~6.2.1", - "true-case-path": "~2.2.1", - "uuid": "~8.3.2" + "true-case-path": "~2.2.1" }, "devDependencies": { "@pnpm/lockfile.types": "~1.0.3", @@ -87,7 +86,6 @@ "@types/ssri": "~7.1.0", "@types/strict-uri-encode": "2.0.0", "@types/tar": "6.1.6", - "@types/uuid": "~8.3.4", "@types/webpack-env": "1.18.8", "webpack": "~5.98.0" }, diff --git a/libraries/rush-lib/src/api/CobuildConfiguration.ts b/libraries/rush-lib/src/api/CobuildConfiguration.ts index 0f169a2966a..8e7da0a6ec6 100644 --- a/libraries/rush-lib/src/api/CobuildConfiguration.ts +++ b/libraries/rush-lib/src/api/CobuildConfiguration.ts @@ -3,7 +3,7 @@ import { FileSystem, JsonFile, JsonSchema } from '@rushstack/node-core-library'; import type { ITerminal } from '@rushstack/terminal'; -import { v4 as uuidv4 } from 'uuid'; +import { randomUUID } from 'node:crypto'; import { EnvironmentConfiguration } from './EnvironmentConfiguration'; import type { CobuildLockProviderFactory, RushSession } from '../pluginFramework/RushSession'; @@ -84,7 +84,7 @@ export class CobuildConfiguration { this.cobuildContextId = EnvironmentConfiguration.cobuildContextId; this.cobuildFeatureEnabled = this.cobuildContextId ? cobuildJson.cobuildFeatureEnabled : false; - this.cobuildRunnerId = EnvironmentConfiguration.cobuildRunnerId || uuidv4(); + this.cobuildRunnerId = EnvironmentConfiguration.cobuildRunnerId || randomUUID(); this.cobuildLeafProjectLogOnlyAllowed = EnvironmentConfiguration.cobuildLeafProjectLogOnlyAllowed ?? false; this.cobuildWithoutCacheAllowed = diff --git a/libraries/rush-lib/src/api/CommandLineJson.ts b/libraries/rush-lib/src/api/CommandLineJson.ts index ba4f412176f..e6507e49633 100644 --- a/libraries/rush-lib/src/api/CommandLineJson.ts +++ b/libraries/rush-lib/src/api/CommandLineJson.ts @@ -23,6 +23,7 @@ export interface IBaseCommandJson { export interface IBulkCommandJson extends IBaseCommandJson { commandKind: 'bulk'; enableParallelism: boolean; + allowOversubscription?: boolean; ignoreDependencyOrder?: boolean; ignoreMissingScript?: boolean; incremental?: boolean; @@ -38,6 +39,7 @@ export interface IBulkCommandJson extends IBaseCommandJson { export interface IPhasedCommandWithoutPhasesJson extends IBaseCommandJson { commandKind: 'phased'; enableParallelism: boolean; + allowOversubscription?: boolean; incremental?: boolean; } diff --git a/libraries/rush-lib/src/api/VersionPolicy.ts b/libraries/rush-lib/src/api/VersionPolicy.ts index f2a18967836..8d357bbaeb7 100644 --- a/libraries/rush-lib/src/api/VersionPolicy.ts +++ b/libraries/rush-lib/src/api/VersionPolicy.ts @@ -218,7 +218,7 @@ export class LockStepVersionPolicy extends VersionPolicy { /** * @internal */ - public declare readonly _json: ILockStepVersionJson; + declare public readonly _json: ILockStepVersionJson; private _version: semver.SemVer; /** @@ -340,7 +340,7 @@ export class IndividualVersionPolicy extends VersionPolicy { /** * @internal */ - public declare readonly _json: IIndividualVersionJson; + declare public readonly _json: IIndividualVersionJson; /** * The major version that has been locked diff --git a/libraries/rush-lib/src/cli/RushCommandLineParser.ts b/libraries/rush-lib/src/cli/RushCommandLineParser.ts index d21b637f931..19e6a095f34 100644 --- a/libraries/rush-lib/src/cli/RushCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushCommandLineParser.ts @@ -471,6 +471,11 @@ export class RushCommandLineParser extends CommandLineParser { incremental: command.incremental || false, disableBuildCache: command.disableBuildCache || false, + // The Async.forEachAsync() API defaults allowOversubscription=false, whereas Rush historically + // defaults allowOversubscription=true to favor faster builds rather than strictly staying below + // the CPU limit. + allowOversubscription: command.allowOversubscription ?? true, + initialPhases: command.phases, originalPhases: command.originalPhases, watchPhases: command.watchPhases, diff --git a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts index 038e2386f31..906a3ca89ea 100644 --- a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts @@ -68,6 +68,7 @@ const PERF_PREFIX: 'rush:phasedScriptAction' = 'rush:phasedScriptAction'; */ export interface IPhasedScriptActionOptions extends IBaseScriptActionOptions { enableParallelism: boolean; + allowOversubscription: boolean; incremental: boolean; disableBuildCache: boolean; @@ -140,6 +141,7 @@ export class PhasedScriptAction extends BaseScriptAction i public readonly sessionAbortController: AbortController; private readonly _enableParallelism: boolean; + private readonly _allowOversubscription: boolean; private readonly _isIncrementalBuildAllowed: boolean; private readonly _disableBuildCache: boolean; private readonly _originalPhases: ReadonlySet; @@ -171,6 +173,7 @@ export class PhasedScriptAction extends BaseScriptAction i public constructor(options: IPhasedScriptActionOptions) { super(options); this._enableParallelism = options.enableParallelism; + this._allowOversubscription = options.allowOversubscription; this._isIncrementalBuildAllowed = options.incremental; this._disableBuildCache = options.disableBuildCache; this._originalPhases = options.originalPhases; @@ -583,6 +586,7 @@ export class PhasedScriptAction extends BaseScriptAction i quietMode: isQuietMode, debugMode: this.parser.isDebug, parallelism, + allowOversubscription: this._allowOversubscription, beforeExecuteOperationAsync: async (record: OperationExecutionRecord) => { return await this.hooks.beforeExecuteOperation.promise(record); }, diff --git a/libraries/rush-lib/src/logic/operations/OperationExecutionManager.ts b/libraries/rush-lib/src/logic/operations/OperationExecutionManager.ts index b88211eabb7..b0f2a3e0cb2 100644 --- a/libraries/rush-lib/src/logic/operations/OperationExecutionManager.ts +++ b/libraries/rush-lib/src/logic/operations/OperationExecutionManager.ts @@ -25,6 +25,7 @@ export interface IOperationExecutionManagerOptions { quietMode: boolean; debugMode: boolean; parallelism: number; + allowOversubscription: boolean; inputsSnapshot?: IInputsSnapshot; destination?: TerminalWritable; @@ -69,6 +70,7 @@ export class OperationExecutionManager { private readonly _executionRecords: Map; private readonly _quietMode: boolean; private readonly _parallelism: number; + private readonly _allowOversubscription: boolean; private readonly _totalOperations: number; private readonly _outputWritable: TerminalWritable; @@ -99,6 +101,7 @@ export class OperationExecutionManager { quietMode, debugMode, parallelism, + allowOversubscription, inputsSnapshot, beforeExecuteOperationAsync: beforeExecuteOperation, afterExecuteOperationAsync: afterExecuteOperation, @@ -112,6 +115,7 @@ export class OperationExecutionManager { this._hasAnyNonAllowedWarnings = false; this._hasAnyAborted = false; this._parallelism = parallelism; + this._allowOversubscription = allowOversubscription; this._beforeExecuteOperation = beforeExecuteOperation; this._afterExecuteOperation = afterExecuteOperation; @@ -304,6 +308,7 @@ export class OperationExecutionManager { } }, { + allowOversubscription: this._allowOversubscription, concurrency: maxParallelism, weighted: true } diff --git a/libraries/rush-lib/src/logic/operations/test/OperationExecutionManager.test.ts b/libraries/rush-lib/src/logic/operations/test/OperationExecutionManager.test.ts index d86f2bc7f7b..b8168a8e232 100644 --- a/libraries/rush-lib/src/logic/operations/test/OperationExecutionManager.test.ts +++ b/libraries/rush-lib/src/logic/operations/test/OperationExecutionManager.test.ts @@ -103,6 +103,7 @@ describe(OperationExecutionManager.name, () => { quietMode: false, debugMode: false, parallelism: 1, + allowOversubscription: true, destination: mockWritable }; }); @@ -185,6 +186,7 @@ describe(OperationExecutionManager.name, () => { quietMode: false, debugMode: false, parallelism: 1, + allowOversubscription: true, destination: mockWritable } ); @@ -229,6 +231,7 @@ describe(OperationExecutionManager.name, () => { quietMode: false, debugMode: false, parallelism: 1, + allowOversubscription: true, destination: mockWritable } ); @@ -250,6 +253,7 @@ describe(OperationExecutionManager.name, () => { quietMode: false, debugMode: false, parallelism: 1, + allowOversubscription: true, destination: mockWritable }; }); @@ -287,6 +291,7 @@ describe(OperationExecutionManager.name, () => { quietMode: false, debugMode: false, parallelism: 1, + allowOversubscription: true, destination: mockWritable }; }); diff --git a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/inconsistent-dep-devDep.yaml b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/inconsistent-dep-devDep.yaml index 7f26989631c..08f6420eaf5 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/inconsistent-dep-devDep.yaml +++ b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/inconsistent-dep-devDep.yaml @@ -5,7 +5,6 @@ settings: excludeLinksFromLockfile: false importers: - .: {} ../../apps/bar: @@ -15,12 +14,13 @@ importers: version: 2.3.2 packages: - prettier@2.3.2: - resolution: {integrity: sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== + } + engines: { node: '>=10.13.0' } hasBin: true snapshots: - prettier@2.3.2: {} diff --git a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/modified.yaml b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/modified.yaml index 71509e89efe..7f156d05a28 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/modified.yaml +++ b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/modified.yaml @@ -5,7 +5,6 @@ settings: excludeLinksFromLockfile: false importers: - .: {} ../../apps/foo: @@ -19,17 +18,21 @@ importers: version: 5.0.4 packages: - tslib@2.3.1: - resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} + resolution: + { + integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + } typescript@5.0.4: - resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} - engines: {node: '>=12.20'} + resolution: + { + integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + } + engines: { node: '>=12.20' } hasBin: true snapshots: - tslib@2.3.1: {} typescript@5.0.4: {} diff --git a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/not-modified.yaml b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/not-modified.yaml index 47ef29262c3..f17060bc2eb 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/not-modified.yaml +++ b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/not-modified.yaml @@ -5,7 +5,6 @@ settings: excludeLinksFromLockfile: false importers: - .: {} ../../apps/foo: @@ -19,17 +18,21 @@ importers: version: 5.0.4 packages: - tslib@2.3.1: - resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} + resolution: + { + integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + } typescript@5.0.4: - resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} - engines: {node: '>=12.20'} + resolution: + { + integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + } + engines: { node: '>=12.20' } hasBin: true snapshots: - tslib@2.3.1: {} typescript@5.0.4: {} diff --git a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/overrides-not-modified.yaml b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/overrides-not-modified.yaml index d21630b1d81..69e66401e1e 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/overrides-not-modified.yaml +++ b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/overrides-not-modified.yaml @@ -8,7 +8,6 @@ overrides: typescript: 5.0.4 importers: - .: {} ../../apps/foo: @@ -22,17 +21,21 @@ importers: version: 5.0.4 packages: - tslib@2.3.1: - resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} + resolution: + { + integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + } typescript@5.0.4: - resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} - engines: {node: '>=12.20'} + resolution: + { + integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + } + engines: { node: '>=12.20' } hasBin: true snapshots: - tslib@2.3.1: {} typescript@5.0.4: {} diff --git a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/pnpm-lock-v9.yaml b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/pnpm-lock-v9.yaml index 9fe144e1b91..91130e28c33 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/pnpm-lock-v9.yaml +++ b/libraries/rush-lib/src/logic/pnpm/test/yamlFiles/pnpm-lock-v9/pnpm-lock-v9.yaml @@ -5,7 +5,6 @@ settings: excludeLinksFromLockfile: false importers: - .: dependencies: jquery: @@ -16,20 +15,27 @@ importers: version: 2.1.0 packages: - jquery@3.7.1: - resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==} + resolution: + { + integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg== + } pad-left@2.1.0: - resolution: {integrity: sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA== + } + engines: { node: '>=0.10.0' } repeat-string@1.6.1: - resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} - engines: {node: '>=0.10'} + resolution: + { + integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + } + engines: { node: '>=0.10' } snapshots: - jquery@3.7.1: {} pad-left@2.1.0: diff --git a/libraries/rush-lib/src/schemas/command-line.schema.json b/libraries/rush-lib/src/schemas/command-line.schema.json index dd2655f30b2..0091e7bb7ae 100644 --- a/libraries/rush-lib/src/schemas/command-line.schema.json +++ b/libraries/rush-lib/src/schemas/command-line.schema.json @@ -66,6 +66,11 @@ "description": "If true then this command can be run in parallel, i.e. executed simultaneously for multiple projects.", "type": "boolean" }, + "allowOversubscription": { + "title": "allowOversubscription", + "type": "boolean", + "description": "Controls whether weighted operations can start when the total weight would exceed the limit but is currently below the limit. This setting only applies when \"enableParallelism\" is true and operations have a \"weight\" property configured in their rush-project.json \"operationSettings\". Choose true (the default) to favor parallelism. Choose false to strictly stay under the limit." + }, "ignoreDependencyOrder": { "title": "ignoreDependencyOrder", "description": "Normally projects will be processed according to their dependency order: a given project will not start processing the command until all of its dependencies have completed. This restriction doesn't apply for certain operations, for example, a \"clean\" task that deletes output files. In this case you can set \"ignoreDependencyOrder\" to true to increase parallelism.", @@ -110,6 +115,7 @@ "shellCommand": { "$ref": "#/definitions/anything" }, "enableParallelism": { "$ref": "#/definitions/anything" }, + "allowOversubscription": { "$ref": "#/definitions/anything" }, "ignoreDependencyOrder": { "$ref": "#/definitions/anything" }, "ignoreMissingScript": { "$ref": "#/definitions/anything" }, "incremental": { "$ref": "#/definitions/anything" }, @@ -181,6 +187,11 @@ "description": "If true then this command can be run in parallel, i.e. executed simultaneously for multiple projects.", "type": "boolean" }, + "allowOversubscription": { + "title": "allowOversubscription", + "type": "boolean", + "description": "Controls whether weighted operations can start when the total weight would exceed the limit but is currently below the limit. This setting only applies when \"enableParallelism\" is true and operations have a \"weight\" property configured in their rush-project.json \"operationSettings\". Choose true (the default) to favor parallelism. Choose false to strictly stay under the limit." + }, "incremental": { "title": "Incremental", "description": "If true then this command's phases will be incremental and support caching.", @@ -253,6 +264,7 @@ "safeForSimultaneousRushProcesses": { "$ref": "#/definitions/anything" }, "enableParallelism": { "$ref": "#/definitions/anything" }, + "allowOversubscription": { "$ref": "#/definitions/anything" }, "incremental": { "$ref": "#/definitions/anything" }, "phases": { "$ref": "#/definitions/anything" }, "watchOptions": { "$ref": "#/definitions/anything" }, diff --git a/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts b/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts index 72b9c631e76..47977f1ee7c 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts @@ -41,7 +41,7 @@ function getPlatformInfo(): IPlatformInfo { } const END_TOKEN: string = '/package.json":'; -const RESOLVER_CACHE_FILE_VERSION: 1 = 1; +const RESOLVER_CACHE_FILE_VERSION: 2 = 2; interface IExtendedResolverCacheFile extends IResolverCacheFile { /** @@ -94,6 +94,12 @@ export async function afterInstallAsync( throw new Error(`Failed to load shrinkwrap file: ${lockFilePath}`); } + if (!lockFile.hash) { + throw new Error( + `Shrinkwrap file does not have a hash. This indicates linking to an old version of Rush.` + ); + } + try { const oldCacheFileContent: string = await FileSystem.readFileAsync(cacheFilePath); const oldCache: IExtendedResolverCacheFile = JSON.parse(oldCacheFileContent); diff --git a/rush-plugins/rush-resolver-cache-plugin/src/externals.ts b/rush-plugins/rush-resolver-cache-plugin/src/externals.ts index 942c7d2dd89..4638cf2578c 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/externals.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/externals.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import type Module from 'node:module'; - import type { Operation as OperationType, OperationStatus as OperationStatusType } from '@rushstack/rush-sdk'; import type { PnpmShrinkwrapFile as PnpmShrinkwrapFileType } from '@rushstack/rush-sdk/lib/logic/pnpm/PnpmShrinkwrapFile'; import type * as rushSdkType from '@rushstack/rush-sdk'; @@ -22,27 +20,30 @@ export { Operation, OperationStatus }; // Support this plugin being webpacked. const req: typeof require = typeof __non_webpack_require__ === 'function' ? __non_webpack_require__ : require; -const entryModule: Module | undefined = req.main; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function getExternal(name: string): TResult { +const rushLibPath: string | undefined = process.env._RUSH_LIB_PATH; + +function importDependency(name: string): TResult { + if (!rushLibPath) { + throw new Error(`_RUSH_LIB_PATH variable is not set, cannot resolve rush-lib.`); + } const externalPath: string = req.resolve(name, { - paths: entryModule?.paths + paths: [rushLibPath] }); return req(externalPath); } // Private Rush APIs -export const { PnpmShrinkwrapFile } = getExternal< +export const { PnpmShrinkwrapFile } = importDependency< typeof import('@rushstack/rush-sdk/lib/logic/pnpm/PnpmShrinkwrapFile') >('@microsoft/rush-lib/lib/logic/pnpm/PnpmShrinkwrapFile'); // eslint-disable-next-line @typescript-eslint/no-redeclare export type PnpmShrinkwrapFile = PnpmShrinkwrapFileType; // Avoid bundling expensive stuff that's already part of Rush. -export const { Async } = getExternal( +export const { Async } = importDependency( `@rushstack/node-core-library/lib/Async` ); -export const { FileSystem } = getExternal( +export const { FileSystem } = importDependency( `@rushstack/node-core-library/lib/FileSystem` ); diff --git a/rush-plugins/rush-serve-plugin/src/RushProjectServeConfigFile.ts b/rush-plugins/rush-serve-plugin/src/RushProjectServeConfigFile.ts index 0126fcd5d30..cc7cb2d8220 100644 --- a/rush-plugins/rush-serve-plugin/src/RushProjectServeConfigFile.ts +++ b/rush-plugins/rush-serve-plugin/src/RushProjectServeConfigFile.ts @@ -8,35 +8,9 @@ import { Async } from '@rushstack/node-core-library'; import type { ITerminal } from '@rushstack/terminal'; import { RigConfig } from '@rushstack/rig-package'; import type { RushConfigurationProject } from '@rushstack/rush-sdk'; -import rushProjectServeSchema from './schemas/rush-project-serve.schema.json'; - -export interface IRushProjectServeJson { - routing: IRoutingRuleJson[]; -} - -export interface IBaseRoutingRuleJson { - servePath: string; - immutable?: boolean; -} - -export interface IRoutingFolderRuleJson extends IBaseRoutingRuleJson { - projectRelativeFile: undefined; - projectRelativeFolder: string; -} - -export interface IRoutingFileRuleJson extends IBaseRoutingRuleJson { - projectRelativeFile: string; - projectRelativeFolder: undefined; -} -export type IRoutingRuleJson = IRoutingFileRuleJson | IRoutingFolderRuleJson; - -export interface IRoutingRule { - type: 'file' | 'folder'; - diskPath: string; - servePath: string; - immutable: boolean; -} +import rushProjectServeSchema from './schemas/rush-project-serve.schema.json'; +import type { IRushProjectServeJson, IRoutingRule } from './types'; export class RushServeConfiguration { private readonly _loader: ProjectConfigurationFile; diff --git a/rush-plugins/rush-serve-plugin/src/RushServePlugin.ts b/rush-plugins/rush-serve-plugin/src/RushServePlugin.ts index 08be11d31fb..1a4ab89a099 100644 --- a/rush-plugins/rush-serve-plugin/src/RushServePlugin.ts +++ b/rush-plugins/rush-serve-plugin/src/RushServePlugin.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import type { IRushPlugin, RushSession, RushConfiguration, IPhasedCommand } from '@rushstack/rush-sdk'; import { PLUGIN_NAME } from './constants'; -import type { IBaseRoutingRuleJson, IRoutingRule } from './RushProjectServeConfigFile'; +import type { IBaseRoutingRuleJson, IRoutingRule } from './types'; export interface IGlobalRoutingFolderRuleJson extends IBaseRoutingRuleJson { workspaceRelativeFile: undefined; diff --git a/rush-plugins/rush-serve-plugin/src/api.types.ts b/rush-plugins/rush-serve-plugin/src/api.types.ts index 2afcfc7a98d..99b5fcb012f 100644 --- a/rush-plugins/rush-serve-plugin/src/api.types.ts +++ b/rush-plugins/rush-serve-plugin/src/api.types.ts @@ -34,6 +34,11 @@ export interface IOperationInfo { */ name: string; + /** + * The names of the dependencies of the operation. + */ + dependencies: string[]; + /** * The npm package name of the containing Rush Project. */ @@ -151,6 +156,13 @@ export interface IWebSocketSyncCommandMessage { command: 'sync'; } +/** + * Message received from a WebSocket client to request abortion of the current execution pass. + */ +export interface IWebSocketAbortExecutionCommandMessage { + command: 'abort-execution'; +} + /** * Message received from a WebSocket client to request invalidation of one or more operations. */ @@ -162,7 +174,7 @@ export interface IWebSocketInvalidateCommandMessage { /** * The set of possible operation enabled states. */ -export type OperationEnabledState = 'never' | 'changed' | 'affected'; +export type OperationEnabledState = 'never' | 'changed' | 'affected' | 'default'; /** * Message received from a WebSocket client to change the enabled states of operations. @@ -177,5 +189,6 @@ export interface IWebSocketSetEnabledStatesCommandMessage { */ export type IWebSocketCommandMessage = | IWebSocketSyncCommandMessage + | IWebSocketAbortExecutionCommandMessage | IWebSocketInvalidateCommandMessage | IWebSocketSetEnabledStatesCommandMessage; diff --git a/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts b/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts index e949bf14490..b4805180bf2 100644 --- a/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts +++ b/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts @@ -2,62 +2,35 @@ // See LICENSE in the project root for license information. import { once } from 'node:events'; -import type { Server as HTTPSecureServer } from 'node:https'; -import http2, { type Http2SecureServer } from 'node:http2'; +import http2 from 'node:http2'; import type { AddressInfo } from 'node:net'; -import os from 'node:os'; import express, { type Application } from 'express'; import http2express from 'http2-express-bridge'; import cors from 'cors'; import compression from 'compression'; -import { WebSocketServer, type WebSocket, type MessageEvent } from 'ws'; import { CertificateManager, type ICertificate } from '@rushstack/debug-certificate-manager'; -import { AlreadyReportedError, Sort } from '@rushstack/node-core-library'; +import { AlreadyReportedError } from '@rushstack/node-core-library'; import { type ILogger, - type RushConfiguration, type RushConfigurationProject, - type RushSession, - type IPhasedCommand, type Operation, type ICreateOperationsContext, - type IOperationExecutionResult, - OperationStatus, - type IExecutionResult, - type ILogFilePaths, RushConstants } from '@rushstack/rush-sdk'; import { getProjectLogFolders } from '@rushstack/rush-sdk/lib/logic/operations/ProjectLogWritable'; import { type CommandLineIntegerParameter, CommandLineParameterKind } from '@rushstack/ts-command-line'; import { PLUGIN_NAME } from './constants'; -import { type IRoutingRule, RushServeConfiguration } from './RushProjectServeConfigFile'; - -import type { - IOperationInfo, - IWebSocketAfterExecuteEventMessage, - IWebSocketBeforeExecuteEventMessage, - IWebSocketEventMessage, - IWebSocketBatchStatusChangeEventMessage, - IWebSocketSyncEventMessage, - ReadableOperationStatus, - IWebSocketCommandMessage, - IRushSessionInfo, - ILogFileURLs, - OperationEnabledState -} from './api.types'; - -export interface IPhasedCommandHandlerOptions { - rushSession: RushSession; - rushConfiguration: RushConfiguration; - command: IPhasedCommand; - portParameterLongName: string | undefined; - logServePath: string | undefined; - globalRoutingRules: IRoutingRule[]; - buildStatusWebSocketPath: string | undefined; -} +import { RushServeConfiguration } from './RushProjectServeConfigFile'; +import type { IRoutingRule, IPhasedCommandHandlerOptions } from './types'; + +import { + getLogServePathForProject, + tryEnableBuildStatusWebSocketServer, + type WebSocketServerUpgrader +} from './tryEnableBuildStatusWebSocketServer'; export async function phasedCommandHandler(options: IPhasedCommandHandlerOptions): Promise { const { rushSession, command, portParameterLongName, globalRoutingRules } = options; @@ -273,297 +246,3 @@ export async function phasedCommandHandler(options: IPhasedCommandHandlerOptions command.hooks.waitingForChanges.tap(PLUGIN_NAME, logHost); } - -type WebSocketServerUpgrader = (server: Http2SecureServer) => void; - -/** - * - */ -function tryEnableBuildStatusWebSocketServer( - options: IPhasedCommandHandlerOptions -): WebSocketServerUpgrader | undefined { - const { buildStatusWebSocketPath } = options; - if (!buildStatusWebSocketPath) { - return; - } - - let operationStates: Map | undefined; - let buildStatus: ReadableOperationStatus = 'Ready'; - - const webSockets: Set = new Set(); - - // Map from OperationStatus enum values back to the names of the constants - const readableStatusFromStatus: { [K in OperationStatus]: ReadableOperationStatus } = { - [OperationStatus.Waiting]: 'Waiting', - [OperationStatus.Ready]: 'Ready', - [OperationStatus.Queued]: 'Queued', - [OperationStatus.Executing]: 'Executing', - [OperationStatus.Success]: 'Success', - [OperationStatus.SuccessWithWarning]: 'SuccessWithWarning', - [OperationStatus.Skipped]: 'Skipped', - [OperationStatus.FromCache]: 'FromCache', - [OperationStatus.Failure]: 'Failure', - [OperationStatus.Blocked]: 'Blocked', - [OperationStatus.NoOp]: 'NoOp', - [OperationStatus.Aborted]: 'Aborted' - }; - - const { logServePath } = options; - - function convertToLogFileUrls( - logFilePaths: ILogFilePaths | undefined, - packageName: string - ): ILogFileURLs | undefined { - if (!logFilePaths || !logServePath) { - return; - } - - const projectLogServePath: string = getLogServePathForProject(logServePath, packageName); - - const logFileUrls: ILogFileURLs = { - text: `${projectLogServePath}${logFilePaths.text.slice(logFilePaths.textFolder.length)}`, - error: `${projectLogServePath}${logFilePaths.error.slice(logFilePaths.textFolder.length)}`, - jsonl: `${projectLogServePath}${logFilePaths.jsonl.slice(logFilePaths.jsonlFolder.length)}` - }; - - return logFileUrls; - } - - /** - * Maps the internal Rush record down to a subset that is JSON-friendly and human readable. - */ - function convertToOperationInfo(record: IOperationExecutionResult): IOperationInfo | undefined { - const { operation } = record; - const { name, associatedPhase, associatedProject, runner, enabled } = operation; - - if (!name || !runner) { - return; - } - - const { packageName } = associatedProject; - - return { - name, - packageName, - phaseName: associatedPhase.name, - - enabled, - silent: record.silent, - noop: !!runner.isNoOp, - - status: readableStatusFromStatus[record.status], - startTime: record.stopwatch.startTime, - endTime: record.stopwatch.endTime, - - logFileURLs: convertToLogFileUrls(record.logFilePaths, packageName) - }; - } - - function convertToOperationInfoArray(records: Iterable): IOperationInfo[] { - const operations: IOperationInfo[] = []; - - for (const record of records) { - const info: IOperationInfo | undefined = convertToOperationInfo(record); - - if (info) { - operations.push(info); - } - } - - Sort.sortBy(operations, (x) => x.name); - return operations; - } - - function sendWebSocketMessage(message: IWebSocketEventMessage): void { - const stringifiedMessage: string = JSON.stringify(message); - for (const socket of webSockets) { - socket.send(stringifiedMessage); - } - } - - const { command } = options; - const sessionInfo: IRushSessionInfo = { - actionName: command.actionName, - repositoryIdentifier: getRepositoryIdentifier(options.rushConfiguration) - }; - - function sendSyncMessage(webSocket: WebSocket): void { - const syncMessage: IWebSocketSyncEventMessage = { - event: 'sync', - operations: convertToOperationInfoArray(operationStates?.values() ?? []), - sessionInfo, - status: buildStatus - }; - - webSocket.send(JSON.stringify(syncMessage)); - } - - const { hooks } = command; - - let invalidateOperation: ((operation: Operation, reason: string) => void) | undefined; - - const operationEnabledStates: Map = new Map(); - hooks.createOperations.tap( - { - name: PLUGIN_NAME, - stage: Infinity - }, - (operations: Set, context: ICreateOperationsContext) => { - const potentiallyAffectedOperations: Set = new Set(); - for (const operation of operations) { - const { associatedProject } = operation; - if (context.projectsInUnknownState.has(associatedProject)) { - potentiallyAffectedOperations.add(operation); - } - } - for (const operation of potentiallyAffectedOperations) { - for (const consumer of operation.consumers) { - potentiallyAffectedOperations.add(consumer); - } - - const { name } = operation; - const expectedState: OperationEnabledState | undefined = operationEnabledStates.get(name); - switch (expectedState) { - case 'affected': - operation.enabled = true; - break; - case 'never': - operation.enabled = false; - break; - case 'changed': - operation.enabled = context.projectsInUnknownState.has(operation.associatedProject); - break; - case undefined: - // Use the original value. - break; - } - } - - invalidateOperation = context.invalidateOperation; - - return operations; - } - ); - - hooks.beforeExecuteOperations.tap( - PLUGIN_NAME, - (operationsToExecute: Map): void => { - operationStates = operationsToExecute; - - const beforeExecuteMessage: IWebSocketBeforeExecuteEventMessage = { - event: 'before-execute', - operations: convertToOperationInfoArray(operationsToExecute.values()) - }; - buildStatus = 'Executing'; - sendWebSocketMessage(beforeExecuteMessage); - } - ); - - hooks.afterExecuteOperations.tap(PLUGIN_NAME, (result: IExecutionResult): void => { - buildStatus = readableStatusFromStatus[result.status]; - const infos: IOperationInfo[] = convertToOperationInfoArray(result.operationResults.values() ?? []); - const afterExecuteMessage: IWebSocketAfterExecuteEventMessage = { - event: 'after-execute', - operations: infos, - status: buildStatus - }; - sendWebSocketMessage(afterExecuteMessage); - }); - - const pendingStatusChanges: Map = new Map(); - let statusChangeTimeout: NodeJS.Immediate | undefined; - function sendBatchedStatusChange(): void { - statusChangeTimeout = undefined; - const infos: IOperationInfo[] = convertToOperationInfoArray(pendingStatusChanges.values()); - pendingStatusChanges.clear(); - const message: IWebSocketBatchStatusChangeEventMessage = { - event: 'status-change', - operations: infos - }; - sendWebSocketMessage(message); - } - - hooks.onOperationStatusChanged.tap(PLUGIN_NAME, (record: IOperationExecutionResult): void => { - pendingStatusChanges.set(record.operation, record); - if (!statusChangeTimeout) { - statusChangeTimeout = setImmediate(sendBatchedStatusChange); - } - }); - - const connector: WebSocketServerUpgrader = (server: Http2SecureServer) => { - const wss: WebSocketServer = new WebSocketServer({ - server: server as unknown as HTTPSecureServer, - path: buildStatusWebSocketPath - }); - wss.addListener('connection', (webSocket: WebSocket): void => { - webSockets.add(webSocket); - - sendSyncMessage(webSocket); - - webSocket.addEventListener('message', (ev: MessageEvent) => { - const parsedMessage: IWebSocketCommandMessage = JSON.parse(ev.data.toString()); - switch (parsedMessage.command) { - case 'sync': { - sendSyncMessage(webSocket); - break; - } - - case 'set-enabled-states': { - const { enabledStateByOperationName } = parsedMessage; - for (const [name, state] of Object.entries(enabledStateByOperationName)) { - operationEnabledStates.set(name, state); - } - break; - } - - case 'invalidate': { - const { operationNames } = parsedMessage; - const operationNameSet: Set = new Set(operationNames); - if (invalidateOperation && operationStates) { - for (const operation of operationStates.keys()) { - if (operationNameSet.has(operation.name)) { - invalidateOperation(operation, 'WebSocket'); - } - } - } - break; - } - - default: { - // Unknown message. Ignore. - } - } - }); - - webSocket.addEventListener( - 'close', - () => { - webSockets.delete(webSocket); - }, - { once: true } - ); - }); - }; - - return connector; -} - -function getRepositoryIdentifier(rushConfiguration: RushConfiguration): string { - const { env } = process; - const { CODESPACE_NAME: codespaceName, GITHUB_USER: githubUserName } = env; - - if (codespaceName) { - const usernamePrefix: string | undefined = githubUserName?.replace(/_|$/g, '-'); - const startIndex: number = - usernamePrefix && codespaceName.startsWith(usernamePrefix) ? usernamePrefix.length : 0; - const endIndex: number = codespaceName.lastIndexOf('-'); - const normalizedName: string = codespaceName.slice(startIndex, endIndex).replace(/-/g, ' '); - return `Codespace "${normalizedName}"`; - } - - return `${os.hostname()} - ${rushConfiguration.rushJsonFolder}`; -} - -function getLogServePathForProject(logServePath: string, packageName: string): string { - return `${logServePath}/${packageName}`; -} diff --git a/rush-plugins/rush-serve-plugin/src/tryEnableBuildStatusWebSocketServer.ts b/rush-plugins/rush-serve-plugin/src/tryEnableBuildStatusWebSocketServer.ts new file mode 100644 index 00000000000..1ecf5f10063 --- /dev/null +++ b/rush-plugins/rush-serve-plugin/src/tryEnableBuildStatusWebSocketServer.ts @@ -0,0 +1,363 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { Http2SecureServer } from 'node:http2'; +import type { Server as HTTPSecureServer } from 'node:https'; +import os from 'node:os'; + +import { type WebSocket, WebSocketServer, type MessageEvent } from 'ws'; + +import { Sort } from '@rushstack/node-core-library/lib/Sort'; +import { + type Operation, + type IOperationExecutionResult, + OperationStatus, + type ILogFilePaths, + type ICreateOperationsContext, + type IExecutionResult, + type RushConfiguration, + type IExecuteOperationsContext +} from '@rushstack/rush-sdk'; + +import type { + ReadableOperationStatus, + ILogFileURLs, + IOperationInfo, + IWebSocketEventMessage, + IRushSessionInfo, + IWebSocketSyncEventMessage, + OperationEnabledState, + IWebSocketBeforeExecuteEventMessage, + IWebSocketAfterExecuteEventMessage, + IWebSocketBatchStatusChangeEventMessage, + IWebSocketCommandMessage +} from './api.types'; +import { PLUGIN_NAME } from './constants'; +import type { IPhasedCommandHandlerOptions } from './types'; + +export type WebSocketServerUpgrader = (server: Http2SecureServer) => void; + +/** + * Returns a string that identifies the repository, based on the Rush configuration and environment. + * @param rushConfiguration - The Rush configuration object. + * @returns A string identifier for the repository. + */ +export function getRepositoryIdentifier(rushConfiguration: RushConfiguration): string { + const { env } = process; + const { CODESPACE_NAME: codespaceName, GITHUB_USER: githubUserName } = env; + + if (codespaceName) { + const usernamePrefix: string | undefined = githubUserName?.replace(/_|$/g, '-'); + const startIndex: number = + usernamePrefix && codespaceName.startsWith(usernamePrefix) ? usernamePrefix.length : 0; + const endIndex: number = codespaceName.lastIndexOf('-'); + const normalizedName: string = codespaceName.slice(startIndex, endIndex).replace(/-/g, ' '); + return `Codespace "${normalizedName}"`; + } + + return `${os.hostname()} - ${rushConfiguration.rushJsonFolder}`; +} + +/** + * @param logServePath - The base URL path where logs are being served. + * @param packageName - The npm package name of the project. + * @returns The base URL path for serving logs of the specified project. + */ +export function getLogServePathForProject(logServePath: string, packageName: string): string { + return `${logServePath}/${packageName}`; +} + +/** + * If the `buildStatusWebSocketPath` option is configured, this function returns a `WebSocketServerUpgrader` callback + * that can be used to add a WebSocket server to the HTTPS server. The WebSocket server sends messages + * about operation status changes to connected clients. + * + */ +export function tryEnableBuildStatusWebSocketServer( + options: IPhasedCommandHandlerOptions +): WebSocketServerUpgrader | undefined { + const { buildStatusWebSocketPath } = options; + if (!buildStatusWebSocketPath) { + return; + } + + const operationStates: Map = new Map(); + let buildStatus: ReadableOperationStatus = 'Ready'; + let executionAbortController: AbortController | undefined; + + const webSockets: Set = new Set(); + + // Map from OperationStatus enum values back to the names of the constants + const readableStatusFromStatus: { + [K in OperationStatus]: ReadableOperationStatus; + } = { + [OperationStatus.Waiting]: 'Waiting', + [OperationStatus.Ready]: 'Ready', + [OperationStatus.Queued]: 'Queued', + [OperationStatus.Executing]: 'Executing', + [OperationStatus.Success]: 'Success', + [OperationStatus.SuccessWithWarning]: 'SuccessWithWarning', + [OperationStatus.Skipped]: 'Skipped', + [OperationStatus.FromCache]: 'FromCache', + [OperationStatus.Failure]: 'Failure', + [OperationStatus.Blocked]: 'Blocked', + [OperationStatus.NoOp]: 'NoOp', + [OperationStatus.Aborted]: 'Aborted' + }; + + const { logServePath } = options; + + function convertToLogFileUrls( + logFilePaths: ILogFilePaths | undefined, + packageName: string + ): ILogFileURLs | undefined { + if (!logFilePaths || !logServePath) { + return; + } + + const projectLogServePath: string = getLogServePathForProject(logServePath, packageName); + + const logFileUrls: ILogFileURLs = { + text: `${projectLogServePath}${logFilePaths.text.slice(logFilePaths.textFolder.length)}`, + error: `${projectLogServePath}${logFilePaths.error.slice(logFilePaths.textFolder.length)}`, + jsonl: `${projectLogServePath}${logFilePaths.jsonl.slice(logFilePaths.jsonlFolder.length)}` + }; + + return logFileUrls; + } + + /** + * Maps the internal Rush record down to a subset that is JSON-friendly and human readable. + */ + function convertToOperationInfo(record: IOperationExecutionResult): IOperationInfo | undefined { + const { operation } = record; + const { name, associatedPhase, associatedProject, runner, enabled } = operation; + + if (!name || !runner) { + return; + } + + const { packageName } = associatedProject; + + return { + name, + dependencies: Array.from(operation.dependencies, (dep) => dep.name), + packageName, + phaseName: associatedPhase.name, + + enabled, + silent: runner.silent, + noop: !!runner.isNoOp, + + status: readableStatusFromStatus[record.status], + startTime: record.stopwatch.startTime, + endTime: record.stopwatch.endTime, + + logFileURLs: convertToLogFileUrls(record.logFilePaths, packageName) + }; + } + + function convertToOperationInfoArray(records: Iterable): IOperationInfo[] { + const operations: IOperationInfo[] = []; + + for (const record of records) { + const info: IOperationInfo | undefined = convertToOperationInfo(record); + + if (info) { + operations.push(info); + } + } + + Sort.sortBy(operations, (x) => x.name); + return operations; + } + + function sendWebSocketMessage(message: IWebSocketEventMessage): void { + const stringifiedMessage: string = JSON.stringify(message); + for (const socket of webSockets) { + socket.send(stringifiedMessage); + } + } + + const { command } = options; + const sessionInfo: IRushSessionInfo = { + actionName: command.actionName, + repositoryIdentifier: getRepositoryIdentifier(options.rushConfiguration) + }; + + function sendSyncMessage(webSocket: WebSocket): void { + const syncMessage: IWebSocketSyncEventMessage = { + event: 'sync', + operations: convertToOperationInfoArray(operationStates?.values() ?? []), + sessionInfo, + status: buildStatus + }; + + webSocket.send(JSON.stringify(syncMessage)); + } + + const { hooks } = command; + + let invalidateOperation: ((operation: Operation, reason: string) => void) | undefined; + + const operationEnabledStates: Map = new Map(); + hooks.createOperations.tap( + { + name: PLUGIN_NAME, + stage: Infinity + }, + (operations: Set, context: ICreateOperationsContext) => { + const potentiallyAffectedOperations: Set = new Set(); + for (const operation of operations) { + const { associatedProject } = operation; + if (context.projectsInUnknownState.has(associatedProject)) { + potentiallyAffectedOperations.add(operation); + } + } + for (const operation of potentiallyAffectedOperations) { + for (const consumer of operation.consumers) { + potentiallyAffectedOperations.add(consumer); + } + + const { name } = operation; + const expectedState: OperationEnabledState | undefined = operationEnabledStates.get(name); + switch (expectedState) { + case 'affected': + operation.enabled = true; + break; + case 'never': + operation.enabled = false; + break; + case 'changed': + operation.enabled = context.projectsInUnknownState.has(operation.associatedProject); + break; + case 'default': + case undefined: + // Use the original value. + break; + } + } + + invalidateOperation = context.invalidateOperation; + + return operations; + } + ); + + hooks.beforeExecuteOperations.tap( + PLUGIN_NAME, + ( + operationsToExecute: Map, + context: IExecuteOperationsContext + ): void => { + for (const [operation, result] of operationsToExecute) { + operationStates.set(operation.name, result); + } + + executionAbortController = context.abortController; + + const beforeExecuteMessage: IWebSocketBeforeExecuteEventMessage = { + event: 'before-execute', + operations: convertToOperationInfoArray(operationsToExecute.values()) + }; + buildStatus = 'Executing'; + sendWebSocketMessage(beforeExecuteMessage); + } + ); + + hooks.afterExecuteOperations.tap(PLUGIN_NAME, (result: IExecutionResult): void => { + buildStatus = readableStatusFromStatus[result.status]; + const infos: IOperationInfo[] = convertToOperationInfoArray(result.operationResults.values() ?? []); + const afterExecuteMessage: IWebSocketAfterExecuteEventMessage = { + event: 'after-execute', + operations: infos, + status: buildStatus + }; + sendWebSocketMessage(afterExecuteMessage); + }); + + const pendingStatusChanges: Map = new Map(); + let statusChangeTimeout: NodeJS.Immediate | undefined; + function sendBatchedStatusChange(): void { + statusChangeTimeout = undefined; + const infos: IOperationInfo[] = convertToOperationInfoArray(pendingStatusChanges.values()); + pendingStatusChanges.clear(); + const message: IWebSocketBatchStatusChangeEventMessage = { + event: 'status-change', + operations: infos + }; + sendWebSocketMessage(message); + } + + hooks.onOperationStatusChanged.tap(PLUGIN_NAME, (record: IOperationExecutionResult): void => { + pendingStatusChanges.set(record.operation, record); + if (!statusChangeTimeout) { + statusChangeTimeout = setImmediate(sendBatchedStatusChange); + } + }); + + const connector: WebSocketServerUpgrader = (server: Http2SecureServer) => { + const wss: WebSocketServer = new WebSocketServer({ + server: server as unknown as HTTPSecureServer, + path: buildStatusWebSocketPath + }); + wss.addListener('connection', (webSocket: WebSocket): void => { + webSockets.add(webSocket); + + sendSyncMessage(webSocket); + + webSocket.addEventListener('message', (ev: MessageEvent) => { + const parsedMessage: IWebSocketCommandMessage = JSON.parse(ev.data.toString()); + switch (parsedMessage.command) { + case 'sync': { + sendSyncMessage(webSocket); + break; + } + + case 'set-enabled-states': { + const { enabledStateByOperationName } = parsedMessage; + for (const [name, state] of Object.entries(enabledStateByOperationName)) { + operationEnabledStates.set(name, state); + } + break; + } + + case 'invalidate': { + const { operationNames } = parsedMessage; + const operationNameSet: Set = new Set(operationNames); + if (invalidateOperation) { + for (const operationName of operationNameSet) { + const operationState: IOperationExecutionResult | undefined = + operationStates.get(operationName); + if (operationState) { + invalidateOperation(operationState.operation, 'Invalidated via WebSocket'); + operationStates.delete(operationName); + } + } + } + break; + } + + case 'abort-execution': { + executionAbortController?.abort(); + break; + } + + default: { + // Unknown message. Ignore. + } + } + }); + + webSocket.addEventListener( + 'close', + () => { + webSockets.delete(webSocket); + }, + { once: true } + ); + }); + }; + + return connector; +} diff --git a/rush-plugins/rush-serve-plugin/src/types.ts b/rush-plugins/rush-serve-plugin/src/types.ts new file mode 100644 index 00000000000..8e5473c8d94 --- /dev/null +++ b/rush-plugins/rush-serve-plugin/src/types.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { RushConfiguration, RushSession, IPhasedCommand } from '@rushstack/rush-sdk'; + +export interface IPhasedCommandHandlerOptions { + rushSession: RushSession; + rushConfiguration: RushConfiguration; + command: IPhasedCommand; + portParameterLongName: string | undefined; + logServePath: string | undefined; + globalRoutingRules: IRoutingRule[]; + buildStatusWebSocketPath: string | undefined; +} +export interface IRushProjectServeJson { + routing: IRoutingRuleJson[]; +} + +export interface IBaseRoutingRuleJson { + servePath: string; + immutable?: boolean; +} + +export interface IRoutingFolderRuleJson extends IBaseRoutingRuleJson { + projectRelativeFile: undefined; + projectRelativeFolder: string; +} + +export interface IRoutingFileRuleJson extends IBaseRoutingRuleJson { + projectRelativeFile: string; + projectRelativeFolder: undefined; +} + +export type IRoutingRuleJson = IRoutingFileRuleJson | IRoutingFolderRuleJson; + +export interface IRoutingRule { + type: 'file' | 'folder'; + diskPath: string; + servePath: string; + immutable: boolean; +}