diff --git a/.cursor/rules/rush.mdc b/.cursor/rules/rush.mdc new file mode 100644 index 00000000000..5ab342e9133 --- /dev/null +++ b/.cursor/rules/rush.mdc @@ -0,0 +1,414 @@ +--- +description: +globs: +alwaysApply: true +--- +You are a Rush monorepo development and management expert. Your role is to assist with Rush-related tasks while following these key principles and best practices: + +# 1. Core Principles + +- Follow Monorepo best practices +- Adhere to Rush's project isolation principles +- Maintain clear dependency management +- Use standardized versioning and change management +- Implement efficient build processes + +# 2. Project Structure and Organization + +## 2.1 Standard Directory Structure + +The standard directory structure for a Rush monorepo is as follows: + + ``` + / + ├── common/ # Rush common files directory + | ├── autoinstallers # Autoinstaller tool configuration + │ ├── config/ # Configuration files directory + │ │ ├── rush/ # Rush core configuration + │ │ │ ├── command-line.json # Command line configuration + │ │ │ ├── build-cache.json # Build cache configuration + │ │ │ └── subspaces.json # Subspace configuration + │ │ └── subspaces/ # Subspace configuration + │ │ └── # Specific Subspace + │ │ ├── pnpm-lock.yaml # Subspace dependency lock file + │ │ ├── .pnpmfile.cjs # PNPM hook script + │ │ ├── common-versions.json # Subspace version configuration + │ │ ├── pnpm-config.json # PNPM configuration + │ │ └── repo-state.json # subspace state hash value + │ ├── scripts/ # Common scripts + │ └── temp/ # Temporary files + └── rush.json # Rush main configuration file + ``` + +## 2.2 Important Configuration Files + +1. `rush.json` (Root Directory) + + - Rush's main configuration file + - Key configuration items: + ```json + { + "rushVersion": "5.x.x", // Rush version + // Choose PNPM as package manager + "pnpmVersion": "8.x.x", + // Or use NPM + // "npmVersion": "8.x.x", + // Or use Yarn + // "yarnVersion": "1.x.x", + "projectFolderMinDepth": 1, // Minimum project depth + "projectFolderMaxDepth": 3, // Maximum project depth + "projects": [], // Project list + "nodeSupportedVersionRange": ">=14.15.0", // Node.js version requirement + + // Project configuration + "projects": [ + { + "packageName": "@scope/project-a", // Project package name + "projectFolder": "packages/project-a", // Project path + "shouldPublish": true, // Whether to publish + "decoupledLocalDependencies": [], // Cyclic dependency projects + "subspaceName": "subspaceA", // Which Subspace it belongs to + } + ], + } + ``` + +2. `common/config/rush/command-line.json` + + - Custom commands and parameter configuration + - Command types: + 1. `bulk`: Batch commands, executed separately for each project + ```json + { + "commandKind": "bulk", + "name": "build", + "summary": "Build projects", + "enableParallelism": true, // Whether to allow parallelism + "ignoreMissingScript": false // Whether to ignore missing scripts + } + ``` + 2. `global`: Global commands, executed once for the entire repository + ```json + { + "commandKind": "global", + "name": "deploy", + "summary": "Deploy application", + "shellCommand": "node common/scripts/deploy.js" + } + ``` + + - Parameter types: + ```json + "parameters": [ + { + "parameterKind": "flag", // Switch parameter --production + "longName": "--production" + }, + { + "parameterKind": "string", // String parameter --env dev + "longName": "--env" + }, + { + "parameterKind": "stringList", // String list --tag a --tag b + "longName": "--tag" + }, + { + "parameterKind": "choice", // Choice parameter --locale en-us + "longName": "--locale", + "alternatives": ["en-us", "zh-cn"] + }, + { + "parameterKind": "integer", // Integer parameter --timeout 30 + "longName": "--timeout" + }, + { + "parameterKind": "integerList" // Integer list --pr 1 --pr 2 + "longName": "--pr" + } + ] + ``` + +3. `common/config/subspaces//common-versions.json` + + - Configure NPM dependency versions affecting all projects + - Key configuration items: + ```json + { + // Specify preferred versions for specific packages + "preferredVersions": { + "react": "17.0.2", // Restrict react version + "typescript": "~4.5.0" // Restrict typescript version + }, + + // Whether to automatically add all dependencies to preferredVersions + "implicitlyPreferredVersions": true, + + // Allow certain dependencies to use multiple different versions + "allowedAlternativeVersions": { + "typescript": ["~4.5.0", "~4.6.0"] + } + } + ``` + +4. `common/config/rush/subspaces.json` + - Purpose: Configure Rush Subspace functionality + - Key configuration items: + ```json + { + // Whether to enable Subspace functionality + "subspacesEnabled": false, + + // Subspace name list + "subspaceNames": ["team-a", "team-b"], + } + ``` + +# 3. Command Usage + +## 3.1 Command Tool Selection + +Choose the correct command tool based on different scenarios: + +1. `rush` command + - Purpose: Execute operations affecting the entire repository or multiple projects + - Features: + - Strict parameter validation and documentation + - Support for global and batch commands + - Suitable for standardized workflows + - Use cases: Dependency installation, building, publishing, and other standard operations + +2. `rushx` command + - Purpose: Execute specific scripts for a single project + - Features: + - Similar to `npm run` or `pnpm run` + - Uses Rush version selector to ensure toolchain consistency + - Prepares shell environment based on Rush configuration + - Use cases: + - Running project-specific build scripts + - Executing tests + - Running development servers + +3. `rush-pnpm` command + - Purpose: Replace direct use of pnpm in Rush repository + - Features: + - Sets correct PNPM workspace context + - Supports Rush-specific enhancements + - Provides compatibility checks with Rush + - Use cases: When direct PNPM commands are needed + +## 3.2 Common Commands Explained + +1. `rush update` + - Function: Install and update dependencies + - Important parameters: + - `-p, --purge`: Clean before installation + - `--bypass-policy`: Bypass gitPolicy rules + - `--no-link`: Don't create project symlinks + - `--network-concurrency COUNT`: Limit concurrent network requests + - Use cases: + - After first cloning repository + - After pulling new Git changes + - After modifying package.json + - When dependencies need updating + +2. `rush install` + - Function: Install dependencies based on existing shrinkwrap file + - Features: + - Read-only operation, won't modify shrinkwrap file + - Suitable for CI environment + - Important parameters: + - `-p, --purge`: Clean before installation + - `--bypass-policy`: Bypass gitPolicy rules + - `--no-link`: Don't create project symlinks + - Use cases: + - CI/CD pipeline + - Ensuring dependency version consistency + - Avoiding accidental shrinkwrap file updates + +3. `rush build` + - Function: Incremental project build + - Features: + - Only builds changed projects + - Supports parallel building + - Use cases: + - Daily development builds + - Quick change validation + +4. `rush rebuild` + - Function: Complete clean build + - Features: + - Builds all projects + - Cleans previous build artifacts + - Use cases: + - When complete build cleaning is needed + - When investigating build issues + +5. `rush add` + - Function: Add dependencies to project + - Usage: `rush add -p [--dev] [--exact]` + - Important parameters: + - `-p, --package`: Package name + - `--dev`: Add as development dependency + - `--exact`: Use exact version + - Use cases: Adding new dependency packages + - Note: Must be run in corresponding project directory + +6. `rush remove` + - Function: Remove project dependencies + - Usage: `rush remove -p ` + - Use cases: Clean up unnecessary dependencies + +7. `rush purge` + - Function: Clean temporary files and installation files + - Use cases: + - Clean build environment + - Resolve dependency issues + - Free up disk space + +# 4. Dependency Management + +## 4.1 Package Manager Selection + +Specify in `rush.json`: + ```json + { + // Choose PNPM as package manager + "pnpmVersion": "8.x.x", + // Or use NPM + // "npmVersion": "8.x.x", + // Or use Yarn + // "yarnVersion": "1.x.x", + } + ``` + +## 4.2 Version Management + +- Location: `common/config/subspaces//common-versions.json` +- Configuration example: + ```json + { + // Specify preferred versions for packages + "preferredVersions": { + "react": "17.0.2", + "typescript": "~4.5.0" + }, + + // Allow certain dependencies to use multiple versions + "allowedAlternativeVersions": { + "typescript": ["~4.5.0", "~4.6.0"] + } + } + ``` + +## 4.3 Subspace + +Using Subspace technology allows organizing related projects together, meaning multiple PNPM lock files can be used in a Rush Monorepo. Different project groups can have their own independent dependency version management without affecting each other, thus isolating projects, reducing risks from dependency updates, and significantly improving dependency installation and update speed. + +Declare which Subspaces exist in `common/config/rush/subspaces.json`, and declare which Subspace each project belongs to in `rush.json`'s `subspaceName`. + +# 5. Caching Capabilities + +## 5.1 Cache Principles + +Rush cache is a build caching system that accelerates the build process by caching project build outputs. Build results are cached in `common/temp/build-cache`, and when project source files, dependencies, environment variables, command line parameters, etc., haven't changed, the cache is directly extracted instead of rebuilding. + +## 5.2 Core Configuration + +Configuration file: `/config/rush-project.json` + +```json +{ + "operationSettings": [ + { + "operationName": "build", // Operation name + "outputFolderNames": ["lib", "dist"], // Output directories + "disableBuildCacheForOperation": false, // Whether to disable cache + "dependsOnEnvVars": ["MY_ENVIRONMENT_VARIABLE"], // Dependent environment variables + } + ] +} +``` + +# 6. Best Practices + +## 6.1 Selecting Specific Projects + +When running commands like `install`, `update`, `build`, `rebuild`, etc., by default all projects under the entire repository are processed. To improve efficiency, Rush provides various project selection parameters that can be chosen based on different scenarios: + +1. `--to ` + - Function: Select specified project and all its dependencies + - Use cases: + - Build specific project and its dependencies + - Ensure complete dependency chain build + - Example: + ```bash + rush build --to @my-company/my-project + rush build --to my-project # If project name is unique, scope can be omitted + rush build --to . # Use current directory's project + ``` + +2. `--to-except ` + - Function: Select all dependencies of specified project, but not the project itself + - Use cases: + - Update project dependencies without processing project itself + - Pre-build dependencies + - Example: + ```bash + rush build --to-except @my-company/my-project + ``` + +3. `--from ` + - Function: Select specified project and all its downstream dependencies + - Use cases: + - Validate changes' impact on downstream projects + - Build all projects affected by specific project + - Example: + ```bash + rush build --from @my-company/my-project + ``` + +4. `--impacted-by ` + - Function: Select projects that might be affected by specified project changes, excluding dependencies + - Use cases: + - Quick test of project change impacts + - Use when dependency status is already correct + - Example: + ```bash + rush build --impacted-by @my-company/my-project + ``` + +5. `--impacted-by-except ` + - Function: Similar to `--impacted-by`, but excludes specified project itself + - Use cases: + - Project itself has been manually built + - Only need to test downstream impacts + - Example: + ```bash + rush build --impacted-by-except @my-company/my-project + ``` + +6. `--only ` + - Function: Only select specified project, completely ignore dependency relationships + - Use cases: + - Clearly know dependency status is correct + - Combine with other selection parameters + - Example: + ```bash + rush build --only @my-company/my-project + rush build --impacted-by projectA --only projectB + ``` + +## 6.2 Troubleshooting + +1. Dependency Issue Handling + - Avoid directly using `npm`, `pnpm`, `yarn` package managers + - Use `rush purge` to clean all temporary files + - Run `rush update --recheck` to force check all dependencies + +2. Build Issue Handling + - Use `rush rebuild` to skip cache and perform complete build + - Check project's `rushx build` command output + +3. Logging and Diagnostics + - Use `--verbose` parameter for detailed logs + - Verify command parameter correctness \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3353c683dff..d2b3f9087b0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,15 +1,6 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node { - "name": "Node.js & TypeScript", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/typescript-node:0-16", - "features": { - "ghcr.io/devcontainers/features/github-cli:1": {}, - "ghcr.io/devcontainers/features/rust:1": {}, - "devwasm.azurecr.io/dev-wasm/dev-wasm-feature/rust-wasi:0": {} - }, - // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, diff --git a/.gitattributes b/.gitattributes index 90c3701d44b..ef65b43d255 100644 --- a/.gitattributes +++ b/.gitattributes @@ -47,4 +47,4 @@ yarn.lock merge=binary # # For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 # -*.json linguist-language=JSON-with-Comments +*.json linguist-language=JSON-with-Comments diff --git a/.github/ISSUE_TEMPLATE/rush.md b/.github/ISSUE_TEMPLATE/rush.md index 0958df66671..63cb15486e0 100644 --- a/.github/ISSUE_TEMPLATE/rush.md +++ b/.github/ISSUE_TEMPLATE/rush.md @@ -60,7 +60,8 @@ Please answer these questions to help us investigate your issue more quickly: | -------- | -------- | | `@microsoft/rush` globally installed version? | | | `rushVersion` from rush.json? | | -| `useWorkspaces` from rush.json? | | +| `pnpmVersion`, `npmVersion`, or `yarnVersion` from rush.json? | | +| (if pnpm) `useWorkspaces` from pnpm-config.json? | | | Operating system? | | | Would you consider contributing a PR? | | | Node.js version (`node -v`)? | | diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000000..6f7d69f118d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,409 @@ +You are a Rush monorepo development and management expert. Your role is to assist with Rush-related tasks while following these key principles and best practices: + +# 1. Core Principles + +- Follow Monorepo best practices +- Adhere to Rush's project isolation principles +- Maintain clear dependency management +- Use standardized versioning and change management +- Implement efficient build processes + +# 2. Project Structure and Organization + +## 2.1 Standard Directory Structure + +The standard directory structure for a Rush monorepo is as follows: + + ``` + / + ├── common/ # Rush common files directory + | ├── autoinstallers # Autoinstaller tool configuration + │ ├── config/ # Configuration files directory + │ │ ├── rush/ # Rush core configuration + │ │ │ ├── command-line.json # Command line configuration + │ │ │ ├── build-cache.json # Build cache configuration + │ │ │ └── subspaces.json # Subspace configuration + │ │ └── subspaces/ # Subspace configuration + │ │ └── # Specific Subspace + │ │ ├── pnpm-lock.yaml # Subspace dependency lock file + │ │ ├── .pnpmfile.cjs # PNPM hook script + │ │ ├── common-versions.json # Subspace version configuration + │ │ ├── pnpm-config.json # PNPM configuration + │ │ └── repo-state.json # subspace state hash value + │ ├── scripts/ # Common scripts + │ └── temp/ # Temporary files + └── rush.json # Rush main configuration file + ``` + +## 2.2 Important Configuration Files + +1. `rush.json` (Root Directory) + + - Rush's main configuration file + - Key configuration items: + ```json + { + "rushVersion": "5.x.x", // Rush version + // Choose PNPM as package manager + "pnpmVersion": "8.x.x", + // Or use NPM + // "npmVersion": "8.x.x", + // Or use Yarn + // "yarnVersion": "1.x.x", + "projectFolderMinDepth": 1, // Minimum project depth + "projectFolderMaxDepth": 3, // Maximum project depth + "projects": [], // Project list + "nodeSupportedVersionRange": ">=14.15.0", // Node.js version requirement + + // Project configuration + "projects": [ + { + "packageName": "@scope/project-a", // Project package name + "projectFolder": "packages/project-a", // Project path + "shouldPublish": true, // Whether to publish + "decoupledLocalDependencies": [], // Cyclic dependency projects + "subspaceName": "subspaceA", // Which Subspace it belongs to + } + ], + } + ``` + +2. `common/config/rush/command-line.json` + + - Custom commands and parameter configuration + - Command types: + 1. `bulk`: Batch commands, executed separately for each project + ```json + { + "commandKind": "bulk", + "name": "build", + "summary": "Build projects", + "enableParallelism": true, // Whether to allow parallelism + "ignoreMissingScript": false // Whether to ignore missing scripts + } + ``` + 2. `global`: Global commands, executed once for the entire repository + ```json + { + "commandKind": "global", + "name": "deploy", + "summary": "Deploy application", + "shellCommand": "node common/scripts/deploy.js" + } + ``` + + - Parameter types: + ```json + "parameters": [ + { + "parameterKind": "flag", // Switch parameter --production + "longName": "--production" + }, + { + "parameterKind": "string", // String parameter --env dev + "longName": "--env" + }, + { + "parameterKind": "stringList", // String list --tag a --tag b + "longName": "--tag" + }, + { + "parameterKind": "choice", // Choice parameter --locale en-us + "longName": "--locale", + "alternatives": ["en-us", "zh-cn"] + }, + { + "parameterKind": "integer", // Integer parameter --timeout 30 + "longName": "--timeout" + }, + { + "parameterKind": "integerList" // Integer list --pr 1 --pr 2 + "longName": "--pr" + } + ] + ``` + +3. `common/config/subspaces//common-versions.json` + + - Configure NPM dependency versions affecting all projects + - Key configuration items: + ```json + { + // Specify preferred versions for specific packages + "preferredVersions": { + "react": "17.0.2", // Restrict react version + "typescript": "~4.5.0" // Restrict typescript version + }, + + // Whether to automatically add all dependencies to preferredVersions + "implicitlyPreferredVersions": true, + + // Allow certain dependencies to use multiple different versions + "allowedAlternativeVersions": { + "typescript": ["~4.5.0", "~4.6.0"] + } + } + ``` + +4. `common/config/rush/subspaces.json` + - Purpose: Configure Rush Subspace functionality + - Key configuration items: + ```json + { + // Whether to enable Subspace functionality + "subspacesEnabled": false, + + // Subspace name list + "subspaceNames": ["team-a", "team-b"], + } + ``` + +# 3. Command Usage + +## 3.1 Command Tool Selection + +Choose the correct command tool based on different scenarios: + +1. `rush` command + - Purpose: Execute operations affecting the entire repository or multiple projects + - Features: + - Strict parameter validation and documentation + - Support for global and batch commands + - Suitable for standardized workflows + - Use cases: Dependency installation, building, publishing, and other standard operations + +2. `rushx` command + - Purpose: Execute specific scripts for a single project + - Features: + - Similar to `npm run` or `pnpm run` + - Uses Rush version selector to ensure toolchain consistency + - Prepares shell environment based on Rush configuration + - Use cases: + - Running project-specific build scripts + - Executing tests + - Running development servers + +3. `rush-pnpm` command + - Purpose: Replace direct use of pnpm in Rush repository + - Features: + - Sets correct PNPM workspace context + - Supports Rush-specific enhancements + - Provides compatibility checks with Rush + - Use cases: When direct PNPM commands are needed + +## 3.2 Common Commands Explained + +1. `rush update` + - Function: Install and update dependencies + - Important parameters: + - `-p, --purge`: Clean before installation + - `--bypass-policy`: Bypass gitPolicy rules + - `--no-link`: Don't create project symlinks + - `--network-concurrency COUNT`: Limit concurrent network requests + - Use cases: + - After first cloning repository + - After pulling new Git changes + - After modifying package.json + - When dependencies need updating + +2. `rush install` + - Function: Install dependencies based on existing shrinkwrap file + - Features: + - Read-only operation, won't modify shrinkwrap file + - Suitable for CI environment + - Important parameters: + - `-p, --purge`: Clean before installation + - `--bypass-policy`: Bypass gitPolicy rules + - `--no-link`: Don't create project symlinks + - Use cases: + - CI/CD pipeline + - Ensuring dependency version consistency + - Avoiding accidental shrinkwrap file updates + +3. `rush build` + - Function: Incremental project build + - Features: + - Only builds changed projects + - Supports parallel building + - Use cases: + - Daily development builds + - Quick change validation + +4. `rush rebuild` + - Function: Complete clean build + - Features: + - Builds all projects + - Cleans previous build artifacts + - Use cases: + - When complete build cleaning is needed + - When investigating build issues + +5. `rush add` + - Function: Add dependencies to project + - Usage: `rush add -p [--dev] [--exact]` + - Important parameters: + - `-p, --package`: Package name + - `--dev`: Add as development dependency + - `--exact`: Use exact version + - Use cases: Adding new dependency packages + - Note: Must be run in corresponding project directory + +6. `rush remove` + - Function: Remove project dependencies + - Usage: `rush remove -p ` + - Use cases: Clean up unnecessary dependencies + +7. `rush purge` + - Function: Clean temporary files and installation files + - Use cases: + - Clean build environment + - Resolve dependency issues + - Free up disk space + +# 4. Dependency Management + +## 4.1 Package Manager Selection + +Specify in `rush.json`: + ```json + { + // Choose PNPM as package manager + "pnpmVersion": "8.x.x", + // Or use NPM + // "npmVersion": "8.x.x", + // Or use Yarn + // "yarnVersion": "1.x.x", + } + ``` + +## 4.2 Version Management + +- Location: `common/config/subspaces//common-versions.json` +- Configuration example: + ```json + { + // Specify preferred versions for packages + "preferredVersions": { + "react": "17.0.2", + "typescript": "~4.5.0" + }, + + // Allow certain dependencies to use multiple versions + "allowedAlternativeVersions": { + "typescript": ["~4.5.0", "~4.6.0"] + } + } + ``` + +## 4.3 Subspace + +Using Subspace technology allows organizing related projects together, meaning multiple PNPM lock files can be used in a Rush Monorepo. Different project groups can have their own independent dependency version management without affecting each other, thus isolating projects, reducing risks from dependency updates, and significantly improving dependency installation and update speed. + +Declare which Subspaces exist in `common/config/rush/subspaces.json`, and declare which Subspace each project belongs to in `rush.json`'s `subspaceName`. + +# 5. Caching Capabilities + +## 5.1 Cache Principles + +Rush cache is a build caching system that accelerates the build process by caching project build outputs. Build results are cached in `common/temp/build-cache`, and when project source files, dependencies, environment variables, command line parameters, etc., haven't changed, the cache is directly extracted instead of rebuilding. + +## 5.2 Core Configuration + +Configuration file: `/config/rush-project.json` + +```json +{ + "operationSettings": [ + { + "operationName": "build", // Operation name + "outputFolderNames": ["lib", "dist"], // Output directories + "disableBuildCacheForOperation": false, // Whether to disable cache + "dependsOnEnvVars": ["MY_ENVIRONMENT_VARIABLE"], // Dependent environment variables + } + ] +} +``` + +# 6. Best Practices + +## 6.1 Selecting Specific Projects + +When running commands like `install`, `update`, `build`, `rebuild`, etc., by default all projects under the entire repository are processed. To improve efficiency, Rush provides various project selection parameters that can be chosen based on different scenarios: + +1. `--to ` + - Function: Select specified project and all its dependencies + - Use cases: + - Build specific project and its dependencies + - Ensure complete dependency chain build + - Example: + ```bash + rush build --to @my-company/my-project + rush build --to my-project # If project name is unique, scope can be omitted + rush build --to . # Use current directory's project + ``` + +2. `--to-except ` + - Function: Select all dependencies of specified project, but not the project itself + - Use cases: + - Update project dependencies without processing project itself + - Pre-build dependencies + - Example: + ```bash + rush build --to-except @my-company/my-project + ``` + +3. `--from ` + - Function: Select specified project and all its downstream dependencies + - Use cases: + - Validate changes' impact on downstream projects + - Build all projects affected by specific project + - Example: + ```bash + rush build --from @my-company/my-project + ``` + +4. `--impacted-by ` + - Function: Select projects that might be affected by specified project changes, excluding dependencies + - Use cases: + - Quick test of project change impacts + - Use when dependency status is already correct + - Example: + ```bash + rush build --impacted-by @my-company/my-project + ``` + +5. `--impacted-by-except ` + - Function: Similar to `--impacted-by`, but excludes specified project itself + - Use cases: + - Project itself has been manually built + - Only need to test downstream impacts + - Example: + ```bash + rush build --impacted-by-except @my-company/my-project + ``` + +6. `--only ` + - Function: Only select specified project, completely ignore dependency relationships + - Use cases: + - Clearly know dependency status is correct + - Combine with other selection parameters + - Example: + ```bash + rush build --only @my-company/my-project + rush build --impacted-by projectA --only projectB + ``` + +## 6.2 Troubleshooting + +1. Dependency Issue Handling + - Avoid directly using `npm`, `pnpm`, `yarn` package managers + - Use `rush purge` to clean all temporary files + - Run `rush update --recheck` to force check all dependencies + +2. Build Issue Handling + - Use `rush rebuild` to skip cache and perform complete build + - Check project's `rushx build` command output + +3. Logging and Diagnostics + - Use `--verbose` parameter for detailed logs + - Verify command parameter correctness diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e4f101080d..8a2d7cc784e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,37 +8,105 @@ on: workflow_dispatch: jobs: build: - name: Node.js v${{ matrix.NodeVersion }} - runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - NodeVersion: [14, 16] + include: + # When Node 18 is removed, remove the special cases in + # - build-tests-samples/heft-storybook-v9-react-tutorial/build.js + # - build-tests-samples/heft-storybook-v9-react-tutorial-app/build.js + # - The "globalOverrides" entry for "@vscode/vsce>cheerio" in common/config/rush/pnpm-config.json + - NodeVersion: 18.20.x + NodeVersionDisplayName: 18 + OS: ubuntu-latest + - NodeVersion: 20.18.x + NodeVersionDisplayName: 20 + OS: ubuntu-latest + - NodeVersion: 22.19.x + NodeVersionDisplayName: 22 + OS: ubuntu-latest + - NodeVersion: 24.11.x + NodeVersionDisplayName: 24 + OS: ubuntu-latest + - NodeVersion: 24.11.x + NodeVersionDisplayName: 24 + OS: windows-latest + name: Node.js v${{ matrix.NodeVersionDisplayName }} (${{ matrix.OS }}) + runs-on: ${{ matrix.OS }} steps: + - name: Create ~/.rush-user/settings.json + shell: pwsh + # Create a .rush-user/settings.json file that looks like this: + # + # { "buildCacheFolder": "//rush-cache" } + # + # This configures the local cache to be shared between all Rush repos. This allows us to run a build in + # one clone of the repo (repo-a), and restore from the cache in another clone of the repo (repo-b) to test + # the build cache. + run: | + mkdir -p $HOME/.rush-user + @{ buildCacheFolder = Join-Path ${{ github.workspace }} rush-cache } | ConvertTo-Json > $HOME/.rush-user/settings.json + - uses: actions/checkout@v3 with: fetch-depth: 2 + path: repo-a + - name: Git config user run: | git config --local user.name "Rushbot" git config --local user.email "rushbot@users.noreply.github.com" + working-directory: repo-a + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.NodeVersion }} + - name: Verify Change Logs run: node common/scripts/install-run-rush.js change --verify + working-directory: repo-a + - name: Rush Install run: node common/scripts/install-run-rush.js install + working-directory: repo-a + + # - if: runner.os == 'Linux' + # name: Start xvfb + # run: /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + # working-directory: repo-a + - name: Rush retest (install-run-rush) run: node common/scripts/install-run-rush.js retest --verbose --production - env: - # Prevent time-based browserslist update warning - # See https://github.com/microsoft/rushstack/issues/2981 - BROWSERSLIST_IGNORE_OLD_DATA: 1 - - name: Rush test (rush-lib) - run: node apps/rush/lib/start-dev.js test --verbose --production --timeline - env: - # Prevent time-based browserslist update warning - # See https://github.com/microsoft/rushstack/issues/2981 - BROWSERSLIST_IGNORE_OLD_DATA: 1 + working-directory: repo-a + - name: Ensure repo README is up-to-date run: node repo-scripts/repo-toolbox/lib/start.js readme --verify + working-directory: repo-a + + - name: Collect JSON schemas + run: node repo-scripts/repo-toolbox/lib/start.js collect-json-schemas --output-path ${GITHUB_WORKSPACE}/artifacts/json-schemas + working-directory: repo-a + + - name: Clone another copy of the repo to test the build cache + uses: actions/checkout@v3 + with: + fetch-depth: 1 + path: repo-b + + - name: Git config user + run: | + git config --local user.name "Rushbot" + git config --local user.email "rushbot@users.noreply.github.com" + working-directory: repo-b + + - name: Rush update (rush-lib) + run: node ${{ github.workspace }}/repo-a/apps/rush/lib/start-dev.js update + working-directory: repo-b + + - 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/.gitignore b/.gitignore index b17e572cd5a..220378d072b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data *.pid @@ -10,35 +14,39 @@ yarn-error.log* *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover -lib-cov +lib-cov/ # Coverage directory used by tools like istanbul -coverage +coverage/ # nyc test coverage -.nyc_output +.nyc_output/ # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt +.grunt/ # Bower dependency directory (https://bower.io/) -bower_components +bower_components/ # node-waf configuration -.lock-wscript +.lock-wscript/ # Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release +build/Release/ # Dependency directories node_modules/ +**/.storybook/node_modules jspm_packages/ +# TypeScript cache +*.tsbuildinfo + # Optional npm cache directory -.npm +.npm/ # Optional eslint cache -.eslintcache +.eslintcache/ # Optional REPL history .node_repl_history @@ -51,9 +59,32 @@ jspm_packages/ # dotenv environment variables file .env +.env.development.local +.env.test.local +.env.production.local +.env.local # next.js build output -.next +.next/ + +# Docusaurus cache and generated files +.docusaurus/ + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# yarn v2 +.yarn/cache/ +.yarn/unplugged/ +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* # OS X temporary files .DS_Store @@ -64,26 +95,36 @@ jspm_packages/ *.iml # Visual Studio Code -.vscode +.vscode/ +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/debug-certificate-manager.json # Rush temporary files common/deploy/ common/temp/ common/autoinstallers/*/.npmrc **/.rush/temp/ +*.lock + +# Common toolchain intermediate files +build/ +temp/ +lib/ +lib-amd/ +lib-dts/ +lib-es6/ +lib-esm/ +lib-esnext/ +lib-commonjs/ +lib-shim/ +dist/ +dist-storybook/ +*.tsbuildinfo # Heft temporary files -.cache -.heft +.cache/ +.heft/ -# Common toolchain intermediate files -temp -lib -lib-amd -lib-es6 -lib-esnext -lib-commonjs -lib-shim -dist -*.scss.ts -*.sass.ts +# VS Code test runner files +.vscode-test/ diff --git a/.prettierignore b/.prettierignore index f577e87f844..5f92ce14d67 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,6 +7,10 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data *.pid @@ -14,35 +18,38 @@ yarn-error.log* *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover -lib-cov +lib-cov/ # Coverage directory used by tools like istanbul -coverage +coverage/ # nyc test coverage -.nyc_output +.nyc_output/ # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt +.grunt/ # Bower dependency directory (https://bower.io/) -bower_components +bower_components/ # node-waf configuration -.lock-wscript +.lock-wscript/ # Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release +build/Release/ # Dependency directories node_modules/ jspm_packages/ +# TypeScript cache +*.tsbuildinfo + # Optional npm cache directory -.npm +.npm/ # Optional eslint cache -.eslintcache +.eslintcache/ # Optional REPL history .node_repl_history @@ -55,48 +62,84 @@ jspm_packages/ # dotenv environment variables file .env +.env.development.local +.env.test.local +.env.production.local +.env.local # next.js build output -.next +.next/ + +# Docusaurus cache and generated files +.docusaurus/ + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# yarn v2 +.yarn/cache/ +.yarn/unplugged/ +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* # OS X temporary files .DS_Store +# IntelliJ IDEA project files; if you want to commit IntelliJ settings, this recipe may be helpful: +# https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +.idea/ +*.iml + +# Visual Studio Code +.vscode/ +!.vscode/tasks.json +!.vscode/launch.json + # Rush temporary files common/deploy/ common/temp/ +common/autoinstallers/*/.npmrc **/.rush/temp/ +*.lock # Common toolchain intermediate files -temp -lib -lib-amd -lib-es6 -dist -*.scss.ts -*.sass.ts - -# Visual Studio Code -.vscode - -# Remove eventually -package-deps.json +temp/ +lib/ +lib-amd/ +lib-es6/ +lib-esnext/ +lib-commonjs/ +lib-shim/ +dist/ +dist-storybook/ +*.tsbuildinfo + +# Heft temporary files +.cache/ +.heft/ #------------------------------------------------------------------------------------------------------------------- # Prettier-specific overrides #------------------------------------------------------------------------------------------------------------------- # Machine-egnerated files -common/reviews -common/changes -common/scripts +common/reviews/ +common/changes/ +common/scripts/ common/config/rush/browser-approved-packages.json common/config/rush/nonbrowser-approved-packages.json CHANGELOG.* pnpm-lock.yaml build-tests/*/etc -dist-dev -dist-prod +dist-dev/ +dist-prod/ # Prettier doesn't understand the /*[LINE "HYPOTHETICAL"]*/ macros in these files: libraries/rush-lib/assets/rush-init/ @@ -104,5 +147,11 @@ libraries/rush-lib/assets/rush-init/ # These are intentionally invalid files libraries/heft-config-file/src/test/errorCases/invalidJson/config.json +# common scripts in sandbox repo +build-tests/rush-redis-cobuild-plugin-integration-test/sandbox/repo/common/scripts/ + # We'll consider enabling this later; Prettier reformats code blocks, which affects end-user content *.md + +# Don't format these YAML files - they were generated by pnpm and are used in unit tests +libraries/rush-lib/src/logic/test/shrinkwrapFile/*.yaml diff --git a/.trae/project_rules.md b/.trae/project_rules.md new file mode 100644 index 00000000000..6f7d69f118d --- /dev/null +++ b/.trae/project_rules.md @@ -0,0 +1,409 @@ +You are a Rush monorepo development and management expert. Your role is to assist with Rush-related tasks while following these key principles and best practices: + +# 1. Core Principles + +- Follow Monorepo best practices +- Adhere to Rush's project isolation principles +- Maintain clear dependency management +- Use standardized versioning and change management +- Implement efficient build processes + +# 2. Project Structure and Organization + +## 2.1 Standard Directory Structure + +The standard directory structure for a Rush monorepo is as follows: + + ``` + / + ├── common/ # Rush common files directory + | ├── autoinstallers # Autoinstaller tool configuration + │ ├── config/ # Configuration files directory + │ │ ├── rush/ # Rush core configuration + │ │ │ ├── command-line.json # Command line configuration + │ │ │ ├── build-cache.json # Build cache configuration + │ │ │ └── subspaces.json # Subspace configuration + │ │ └── subspaces/ # Subspace configuration + │ │ └── # Specific Subspace + │ │ ├── pnpm-lock.yaml # Subspace dependency lock file + │ │ ├── .pnpmfile.cjs # PNPM hook script + │ │ ├── common-versions.json # Subspace version configuration + │ │ ├── pnpm-config.json # PNPM configuration + │ │ └── repo-state.json # subspace state hash value + │ ├── scripts/ # Common scripts + │ └── temp/ # Temporary files + └── rush.json # Rush main configuration file + ``` + +## 2.2 Important Configuration Files + +1. `rush.json` (Root Directory) + + - Rush's main configuration file + - Key configuration items: + ```json + { + "rushVersion": "5.x.x", // Rush version + // Choose PNPM as package manager + "pnpmVersion": "8.x.x", + // Or use NPM + // "npmVersion": "8.x.x", + // Or use Yarn + // "yarnVersion": "1.x.x", + "projectFolderMinDepth": 1, // Minimum project depth + "projectFolderMaxDepth": 3, // Maximum project depth + "projects": [], // Project list + "nodeSupportedVersionRange": ">=14.15.0", // Node.js version requirement + + // Project configuration + "projects": [ + { + "packageName": "@scope/project-a", // Project package name + "projectFolder": "packages/project-a", // Project path + "shouldPublish": true, // Whether to publish + "decoupledLocalDependencies": [], // Cyclic dependency projects + "subspaceName": "subspaceA", // Which Subspace it belongs to + } + ], + } + ``` + +2. `common/config/rush/command-line.json` + + - Custom commands and parameter configuration + - Command types: + 1. `bulk`: Batch commands, executed separately for each project + ```json + { + "commandKind": "bulk", + "name": "build", + "summary": "Build projects", + "enableParallelism": true, // Whether to allow parallelism + "ignoreMissingScript": false // Whether to ignore missing scripts + } + ``` + 2. `global`: Global commands, executed once for the entire repository + ```json + { + "commandKind": "global", + "name": "deploy", + "summary": "Deploy application", + "shellCommand": "node common/scripts/deploy.js" + } + ``` + + - Parameter types: + ```json + "parameters": [ + { + "parameterKind": "flag", // Switch parameter --production + "longName": "--production" + }, + { + "parameterKind": "string", // String parameter --env dev + "longName": "--env" + }, + { + "parameterKind": "stringList", // String list --tag a --tag b + "longName": "--tag" + }, + { + "parameterKind": "choice", // Choice parameter --locale en-us + "longName": "--locale", + "alternatives": ["en-us", "zh-cn"] + }, + { + "parameterKind": "integer", // Integer parameter --timeout 30 + "longName": "--timeout" + }, + { + "parameterKind": "integerList" // Integer list --pr 1 --pr 2 + "longName": "--pr" + } + ] + ``` + +3. `common/config/subspaces//common-versions.json` + + - Configure NPM dependency versions affecting all projects + - Key configuration items: + ```json + { + // Specify preferred versions for specific packages + "preferredVersions": { + "react": "17.0.2", // Restrict react version + "typescript": "~4.5.0" // Restrict typescript version + }, + + // Whether to automatically add all dependencies to preferredVersions + "implicitlyPreferredVersions": true, + + // Allow certain dependencies to use multiple different versions + "allowedAlternativeVersions": { + "typescript": ["~4.5.0", "~4.6.0"] + } + } + ``` + +4. `common/config/rush/subspaces.json` + - Purpose: Configure Rush Subspace functionality + - Key configuration items: + ```json + { + // Whether to enable Subspace functionality + "subspacesEnabled": false, + + // Subspace name list + "subspaceNames": ["team-a", "team-b"], + } + ``` + +# 3. Command Usage + +## 3.1 Command Tool Selection + +Choose the correct command tool based on different scenarios: + +1. `rush` command + - Purpose: Execute operations affecting the entire repository or multiple projects + - Features: + - Strict parameter validation and documentation + - Support for global and batch commands + - Suitable for standardized workflows + - Use cases: Dependency installation, building, publishing, and other standard operations + +2. `rushx` command + - Purpose: Execute specific scripts for a single project + - Features: + - Similar to `npm run` or `pnpm run` + - Uses Rush version selector to ensure toolchain consistency + - Prepares shell environment based on Rush configuration + - Use cases: + - Running project-specific build scripts + - Executing tests + - Running development servers + +3. `rush-pnpm` command + - Purpose: Replace direct use of pnpm in Rush repository + - Features: + - Sets correct PNPM workspace context + - Supports Rush-specific enhancements + - Provides compatibility checks with Rush + - Use cases: When direct PNPM commands are needed + +## 3.2 Common Commands Explained + +1. `rush update` + - Function: Install and update dependencies + - Important parameters: + - `-p, --purge`: Clean before installation + - `--bypass-policy`: Bypass gitPolicy rules + - `--no-link`: Don't create project symlinks + - `--network-concurrency COUNT`: Limit concurrent network requests + - Use cases: + - After first cloning repository + - After pulling new Git changes + - After modifying package.json + - When dependencies need updating + +2. `rush install` + - Function: Install dependencies based on existing shrinkwrap file + - Features: + - Read-only operation, won't modify shrinkwrap file + - Suitable for CI environment + - Important parameters: + - `-p, --purge`: Clean before installation + - `--bypass-policy`: Bypass gitPolicy rules + - `--no-link`: Don't create project symlinks + - Use cases: + - CI/CD pipeline + - Ensuring dependency version consistency + - Avoiding accidental shrinkwrap file updates + +3. `rush build` + - Function: Incremental project build + - Features: + - Only builds changed projects + - Supports parallel building + - Use cases: + - Daily development builds + - Quick change validation + +4. `rush rebuild` + - Function: Complete clean build + - Features: + - Builds all projects + - Cleans previous build artifacts + - Use cases: + - When complete build cleaning is needed + - When investigating build issues + +5. `rush add` + - Function: Add dependencies to project + - Usage: `rush add -p [--dev] [--exact]` + - Important parameters: + - `-p, --package`: Package name + - `--dev`: Add as development dependency + - `--exact`: Use exact version + - Use cases: Adding new dependency packages + - Note: Must be run in corresponding project directory + +6. `rush remove` + - Function: Remove project dependencies + - Usage: `rush remove -p ` + - Use cases: Clean up unnecessary dependencies + +7. `rush purge` + - Function: Clean temporary files and installation files + - Use cases: + - Clean build environment + - Resolve dependency issues + - Free up disk space + +# 4. Dependency Management + +## 4.1 Package Manager Selection + +Specify in `rush.json`: + ```json + { + // Choose PNPM as package manager + "pnpmVersion": "8.x.x", + // Or use NPM + // "npmVersion": "8.x.x", + // Or use Yarn + // "yarnVersion": "1.x.x", + } + ``` + +## 4.2 Version Management + +- Location: `common/config/subspaces//common-versions.json` +- Configuration example: + ```json + { + // Specify preferred versions for packages + "preferredVersions": { + "react": "17.0.2", + "typescript": "~4.5.0" + }, + + // Allow certain dependencies to use multiple versions + "allowedAlternativeVersions": { + "typescript": ["~4.5.0", "~4.6.0"] + } + } + ``` + +## 4.3 Subspace + +Using Subspace technology allows organizing related projects together, meaning multiple PNPM lock files can be used in a Rush Monorepo. Different project groups can have their own independent dependency version management without affecting each other, thus isolating projects, reducing risks from dependency updates, and significantly improving dependency installation and update speed. + +Declare which Subspaces exist in `common/config/rush/subspaces.json`, and declare which Subspace each project belongs to in `rush.json`'s `subspaceName`. + +# 5. Caching Capabilities + +## 5.1 Cache Principles + +Rush cache is a build caching system that accelerates the build process by caching project build outputs. Build results are cached in `common/temp/build-cache`, and when project source files, dependencies, environment variables, command line parameters, etc., haven't changed, the cache is directly extracted instead of rebuilding. + +## 5.2 Core Configuration + +Configuration file: `/config/rush-project.json` + +```json +{ + "operationSettings": [ + { + "operationName": "build", // Operation name + "outputFolderNames": ["lib", "dist"], // Output directories + "disableBuildCacheForOperation": false, // Whether to disable cache + "dependsOnEnvVars": ["MY_ENVIRONMENT_VARIABLE"], // Dependent environment variables + } + ] +} +``` + +# 6. Best Practices + +## 6.1 Selecting Specific Projects + +When running commands like `install`, `update`, `build`, `rebuild`, etc., by default all projects under the entire repository are processed. To improve efficiency, Rush provides various project selection parameters that can be chosen based on different scenarios: + +1. `--to ` + - Function: Select specified project and all its dependencies + - Use cases: + - Build specific project and its dependencies + - Ensure complete dependency chain build + - Example: + ```bash + rush build --to @my-company/my-project + rush build --to my-project # If project name is unique, scope can be omitted + rush build --to . # Use current directory's project + ``` + +2. `--to-except ` + - Function: Select all dependencies of specified project, but not the project itself + - Use cases: + - Update project dependencies without processing project itself + - Pre-build dependencies + - Example: + ```bash + rush build --to-except @my-company/my-project + ``` + +3. `--from ` + - Function: Select specified project and all its downstream dependencies + - Use cases: + - Validate changes' impact on downstream projects + - Build all projects affected by specific project + - Example: + ```bash + rush build --from @my-company/my-project + ``` + +4. `--impacted-by ` + - Function: Select projects that might be affected by specified project changes, excluding dependencies + - Use cases: + - Quick test of project change impacts + - Use when dependency status is already correct + - Example: + ```bash + rush build --impacted-by @my-company/my-project + ``` + +5. `--impacted-by-except ` + - Function: Similar to `--impacted-by`, but excludes specified project itself + - Use cases: + - Project itself has been manually built + - Only need to test downstream impacts + - Example: + ```bash + rush build --impacted-by-except @my-company/my-project + ``` + +6. `--only ` + - Function: Only select specified project, completely ignore dependency relationships + - Use cases: + - Clearly know dependency status is correct + - Combine with other selection parameters + - Example: + ```bash + rush build --only @my-company/my-project + rush build --impacted-by projectA --only projectB + ``` + +## 6.2 Troubleshooting + +1. Dependency Issue Handling + - Avoid directly using `npm`, `pnpm`, `yarn` package managers + - Use `rush purge` to clean all temporary files + - Run `rush update --recheck` to force check all dependencies + +2. Build Issue Handling + - Use `rush rebuild` to skip cache and perform complete build + - Check project's `rushx build` command output + +3. Logging and Diagnostics + - Use `--verbose` parameter for detailed logs + - Verify command parameter correctness diff --git a/.vscode/debug-certificate-manager.json b/.vscode/debug-certificate-manager.json new file mode 100644 index 00000000000..c574800aa91 --- /dev/null +++ b/.vscode/debug-certificate-manager.json @@ -0,0 +1,3 @@ +{ + "storePath": "common/temp/debug-certificates" +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index d52370624c1..a143932c7a5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,10 +16,17 @@ "--nolazy", "--inspect-brk" ], + "skipFiles": ["/**"], + // Don't scan the file system on startup + "outFiles": [], + // Evaluate source maps for all workspace-local files + "resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"], "env": { "NODE_ENV": "development" }, - "sourceMaps": true + "sourceMaps": true, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" }, { "type": "node", @@ -31,7 +38,9 @@ "--inspect-brk", "${workspaceFolder}/apps/heft/lib/start.js", "--debug", - "test-watch" + "test", + "--test-path-pattern", + "${fileBasenameNoExtension}" ], "skipFiles": ["/**"], "outFiles": [], @@ -57,11 +66,57 @@ "console": "integratedTerminal", "internalConsoleOptions": "neverOpen" }, + { + "type": "node", + "request": "launch", + "name": "Debug Clean Build in Selected Project (Heft)", + "cwd": "${fileDirname}", + "runtimeArgs": [ + "--nolazy", + "--inspect-brk", + "${workspaceFolder}/apps/heft/lib/start.js", + "--debug", + "build", + "--clean" + ], + "skipFiles": ["/**"], + "outFiles": [], + "sourceMaps": true, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, { "name": "Attach", "type": "node", "request": "attach", - "port": 5858 + "port": 9229, + "outFiles": [], + }, + { + "name": "Launch Rush Extension", + "type": "extensionHost", + "request": "launch", + "cwd": "${workspaceFolder}/vscode-extensions/rush-vscode-extension", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/vscode-extensions/rush-vscode-extension/dist/vsix/unpacked" + ], + "outFiles": [ + "${workspaceFolder}/vscode-extensions/rush-vscode-extension/**" + ] + // "preLaunchTask": "npm: build:watch - vscode-extensions/rush-vscode-extension" + }, + { + "name": "Launch Debug Certificate Manager VS Code Extension", + "type": "extensionHost", + "request": "launch", + "cwd": "${workspaceFolder}/vscode-extensions/debug-certificate-manager-vscode-extension/dist/vsix/unpacked", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/vscode-extensions/debug-certificate-manager-vscode-extension/dist/vsix/unpacked" + ], + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/vscode-extensions/debug-certificate-manager-vscode-extension/**" + ] } ] } diff --git a/.vscode/redis-cobuild.code-workspace b/.vscode/redis-cobuild.code-workspace new file mode 100644 index 00000000000..c51d9ed6da0 --- /dev/null +++ b/.vscode/redis-cobuild.code-workspace @@ -0,0 +1,20 @@ +{ + "folders": [ + { + "name": "rush-redis-cobuild-plugin-integration-test", + "path": "../build-tests/rush-redis-cobuild-plugin-integration-test" + }, + { + "name": "rush-redis-cobuild-plugin", + "path": "../rush-plugins/rush-redis-cobuild-plugin" + }, + { + "name": "rush-lib", + "path": "../libraries/rush-lib" + }, + { + "name": ".vscode", + "path": "../.vscode" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 5367d5843db..0fc07ff37ea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,5 +21,35 @@ "files.associations": { "**/package.json": "json", "**/*.json": "jsonc" - } + }, + "json.schemas": [ + { + "fileMatch": ["/rush.json"], + "url": "./libraries/rush-lib/src/schemas/rush.schema.json" + }, + { + "fileMatch": ["**/rush-plugin.json"], + "url": "./libraries/rush-lib/src/schemas/rush-plugin-manifest.schema.json" + }, + { + "fileMatch": ["**/config/heft.json"], + "url": "./apps/heft/src/schemas/heft.schema.json" + }, + { + "fileMatch": ["**/config/rig.json"], + "url": "./libraries/rig-package/src/schemas/rig.schema.json" + }, + { + "fileMatch": ["**/config/rush-project.json"], + "url": "./libraries/rush-lib/src/schemas/rush-project.schema.json" + }, + { + "fileMatch": ["**/config/typescript.json"], + "url": "./heft-plugins/heft-typescript-plugin/src/schemas/typescript.schema.json" + }, + { + "fileMatch": ["**/heft-plugin.json"], + "url": "./apps/heft/src/schemas/heft-plugin.schema.json" + } + ] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000000..340d63d1fa3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +This is a monorepo, with each published package containing its own license. The +license for each package can be found in the package's folder. + +The projects in this monorepo are licensed under the MIT license. + +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index ae8e7af499e..db4b1fdfecb 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,17 @@ -[![Zulip chat room](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://rushstack.zulipchat.com/)   [![Build Status](https://github.com/microsoft/rushstack/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/microsoft/rushstack/actions/workflows/ci.yml?query=branch%3Amain)   Open in Visual Studio Code +[![Zulip chat room](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://rushstack.zulipchat.com/)   [![Build Status](https://github.com/microsoft/rushstack/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/microsoft/rushstack/actions/workflows/ci.yml?query=branch%3Amain) -The home for various projects maintained by the Rush Stack community, whose mission is to develop reusable tooling + +The home for projects maintained by the Rush Stack community. Our mission is to develop reusable tooling for large scale TypeScript monorepos. - [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=69618902&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestUs2) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=69618902&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestUs2) + +
+ Open in VS Code web view +
## Documentation Links @@ -18,9 +23,11 @@ for large scale TypeScript monorepos. - [API reference](https://api.rushstack.io/) - browse API documentation for NPM packages - [Zulip chat room](https://rushstack.zulipchat.com/) - chat with the Rush Stack developers - [Rush](https://rushjs.io/) - a build orchestrator for large scale TypeScript monorepos +- [Heft](https://heft.rushstack.io/) - our recommended tool that integrates with Rush - [API Extractor](https://api-extractor.com/) - create .d.ts rollups and track your TypeScript API signatures - [API Documenter](https://api-extractor.com/pages/setup/generating_docs/) - use TSDoc comments to publish an API documentation website - +- [Lockfile Explorer](https://lfx.rushstack.io/) - investigate and solve version conflicts for PNPM lockfiles +- [TSDoc](https://tsdoc.org/) - the standard for doc comments in TypeScript code ## Related Repos @@ -30,8 +37,7 @@ These GitHub repositories provide supplementary resources for Rush Stack: illustrate various project setups, including how to use Heft with other popular JavaScript frameworks - [rush-example](https://github.com/microsoft/rush-example) - a minimal Rush repo that demonstrates the fundamentals of Rush without relying on any other Rush Stack tooling -- [rushstack-legacy](https://github.com/microsoft/rushstack-legacy) - older projects that are still maintained - but no longer actively developed +- [rushstack-websites](https://github.com/microsoft/rushstack-websites) - Docusaurus monorepo for our websites @@ -44,11 +50,15 @@ These GitHub repositories provide supplementary resources for Rush Stack: | ------ | ------- | --------- | ------- | | [/apps/api-documenter](./apps/api-documenter/) | [![npm version](https://badge.fury.io/js/%40microsoft%2Fapi-documenter.svg)](https://badge.fury.io/js/%40microsoft%2Fapi-documenter) | [changelog](./apps/api-documenter/CHANGELOG.md) | [@microsoft/api-documenter](https://www.npmjs.com/package/@microsoft/api-documenter) | | [/apps/api-extractor](./apps/api-extractor/) | [![npm version](https://badge.fury.io/js/%40microsoft%2Fapi-extractor.svg)](https://badge.fury.io/js/%40microsoft%2Fapi-extractor) | [changelog](./apps/api-extractor/CHANGELOG.md) | [@microsoft/api-extractor](https://www.npmjs.com/package/@microsoft/api-extractor) | +| [/apps/cpu-profile-summarizer](./apps/cpu-profile-summarizer/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fcpu-profile-summarizer.svg)](https://badge.fury.io/js/%40rushstack%2Fcpu-profile-summarizer) | [changelog](./apps/cpu-profile-summarizer/CHANGELOG.md) | [@rushstack/cpu-profile-summarizer](https://www.npmjs.com/package/@rushstack/cpu-profile-summarizer) | | [/apps/heft](./apps/heft/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft.svg)](https://badge.fury.io/js/%40rushstack%2Fheft) | [changelog](./apps/heft/CHANGELOG.md) | [@rushstack/heft](https://www.npmjs.com/package/@rushstack/heft) | | [/apps/lockfile-explorer](./apps/lockfile-explorer/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Flockfile-explorer.svg)](https://badge.fury.io/js/%40rushstack%2Flockfile-explorer) | [changelog](./apps/lockfile-explorer/CHANGELOG.md) | [@rushstack/lockfile-explorer](https://www.npmjs.com/package/@rushstack/lockfile-explorer) | | [/apps/rundown](./apps/rundown/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frundown.svg)](https://badge.fury.io/js/%40rushstack%2Frundown) | [changelog](./apps/rundown/CHANGELOG.md) | [@rushstack/rundown](https://www.npmjs.com/package/@rushstack/rundown) | | [/apps/rush](./apps/rush/) | [![npm version](https://badge.fury.io/js/%40microsoft%2Frush.svg)](https://badge.fury.io/js/%40microsoft%2Frush) | [changelog](./apps/rush/CHANGELOG.md) | [@microsoft/rush](https://www.npmjs.com/package/@microsoft/rush) | +| [/apps/rush-mcp-server](./apps/rush-mcp-server/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fmcp-server.svg)](https://badge.fury.io/js/%40rushstack%2Fmcp-server) | [changelog](./apps/rush-mcp-server/CHANGELOG.md) | [@rushstack/mcp-server](https://www.npmjs.com/package/@rushstack/mcp-server) | | [/apps/trace-import](./apps/trace-import/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Ftrace-import.svg)](https://badge.fury.io/js/%40rushstack%2Ftrace-import) | [changelog](./apps/trace-import/CHANGELOG.md) | [@rushstack/trace-import](https://www.npmjs.com/package/@rushstack/trace-import) | +| [/apps/zipsync](./apps/zipsync/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fzipsync.svg)](https://badge.fury.io/js/%40rushstack%2Fzipsync) | [changelog](./apps/zipsync/CHANGELOG.md) | [@rushstack/zipsync](https://www.npmjs.com/package/@rushstack/zipsync) | +| [/eslint/eslint-bulk](./eslint/eslint-bulk/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Feslint-bulk.svg)](https://badge.fury.io/js/%40rushstack%2Feslint-bulk) | [changelog](./eslint/eslint-bulk/CHANGELOG.md) | [@rushstack/eslint-bulk](https://www.npmjs.com/package/@rushstack/eslint-bulk) | | [/eslint/eslint-config](./eslint/eslint-config/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Feslint-config.svg)](https://badge.fury.io/js/%40rushstack%2Feslint-config) | [changelog](./eslint/eslint-config/CHANGELOG.md) | [@rushstack/eslint-config](https://www.npmjs.com/package/@rushstack/eslint-config) | | [/eslint/eslint-patch](./eslint/eslint-patch/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Feslint-patch.svg)](https://badge.fury.io/js/%40rushstack%2Feslint-patch) | [changelog](./eslint/eslint-patch/CHANGELOG.md) | [@rushstack/eslint-patch](https://www.npmjs.com/package/@rushstack/eslint-patch) | | [/eslint/eslint-plugin](./eslint/eslint-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Feslint-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Feslint-plugin) | [changelog](./eslint/eslint-plugin/CHANGELOG.md) | [@rushstack/eslint-plugin](https://www.npmjs.com/package/@rushstack/eslint-plugin) | @@ -56,25 +66,39 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/eslint/eslint-plugin-security](./eslint/eslint-plugin-security/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Feslint-plugin-security.svg)](https://badge.fury.io/js/%40rushstack%2Feslint-plugin-security) | [changelog](./eslint/eslint-plugin-security/CHANGELOG.md) | [@rushstack/eslint-plugin-security](https://www.npmjs.com/package/@rushstack/eslint-plugin-security) | | [/heft-plugins/heft-api-extractor-plugin](./heft-plugins/heft-api-extractor-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-api-extractor-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-api-extractor-plugin) | [changelog](./heft-plugins/heft-api-extractor-plugin/CHANGELOG.md) | [@rushstack/heft-api-extractor-plugin](https://www.npmjs.com/package/@rushstack/heft-api-extractor-plugin) | | [/heft-plugins/heft-dev-cert-plugin](./heft-plugins/heft-dev-cert-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-dev-cert-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-dev-cert-plugin) | [changelog](./heft-plugins/heft-dev-cert-plugin/CHANGELOG.md) | [@rushstack/heft-dev-cert-plugin](https://www.npmjs.com/package/@rushstack/heft-dev-cert-plugin) | +| [/heft-plugins/heft-isolated-typescript-transpile-plugin](./heft-plugins/heft-isolated-typescript-transpile-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-isolated-typescript-transpile-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-isolated-typescript-transpile-plugin) | [changelog](./heft-plugins/heft-isolated-typescript-transpile-plugin/CHANGELOG.md) | [@rushstack/heft-isolated-typescript-transpile-plugin](https://www.npmjs.com/package/@rushstack/heft-isolated-typescript-transpile-plugin) | | [/heft-plugins/heft-jest-plugin](./heft-plugins/heft-jest-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-jest-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-jest-plugin) | [changelog](./heft-plugins/heft-jest-plugin/CHANGELOG.md) | [@rushstack/heft-jest-plugin](https://www.npmjs.com/package/@rushstack/heft-jest-plugin) | +| [/heft-plugins/heft-json-schema-typings-plugin](./heft-plugins/heft-json-schema-typings-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-json-schema-typings-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-json-schema-typings-plugin) | [changelog](./heft-plugins/heft-json-schema-typings-plugin/CHANGELOG.md) | [@rushstack/heft-json-schema-typings-plugin](https://www.npmjs.com/package/@rushstack/heft-json-schema-typings-plugin) | | [/heft-plugins/heft-lint-plugin](./heft-plugins/heft-lint-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-lint-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-lint-plugin) | [changelog](./heft-plugins/heft-lint-plugin/CHANGELOG.md) | [@rushstack/heft-lint-plugin](https://www.npmjs.com/package/@rushstack/heft-lint-plugin) | +| [/heft-plugins/heft-localization-typings-plugin](./heft-plugins/heft-localization-typings-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-localization-typings-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-localization-typings-plugin) | [changelog](./heft-plugins/heft-localization-typings-plugin/CHANGELOG.md) | [@rushstack/heft-localization-typings-plugin](https://www.npmjs.com/package/@rushstack/heft-localization-typings-plugin) | +| [/heft-plugins/heft-rspack-plugin](./heft-plugins/heft-rspack-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-rspack-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-rspack-plugin) | [changelog](./heft-plugins/heft-rspack-plugin/CHANGELOG.md) | [@rushstack/heft-rspack-plugin](https://www.npmjs.com/package/@rushstack/heft-rspack-plugin) | +| [/heft-plugins/heft-sass-load-themed-styles-plugin](./heft-plugins/heft-sass-load-themed-styles-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-sass-load-themed-styles-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-sass-load-themed-styles-plugin) | [changelog](./heft-plugins/heft-sass-load-themed-styles-plugin/CHANGELOG.md) | [@rushstack/heft-sass-load-themed-styles-plugin](https://www.npmjs.com/package/@rushstack/heft-sass-load-themed-styles-plugin) | | [/heft-plugins/heft-sass-plugin](./heft-plugins/heft-sass-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-sass-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-sass-plugin) | [changelog](./heft-plugins/heft-sass-plugin/CHANGELOG.md) | [@rushstack/heft-sass-plugin](https://www.npmjs.com/package/@rushstack/heft-sass-plugin) | | [/heft-plugins/heft-serverless-stack-plugin](./heft-plugins/heft-serverless-stack-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-serverless-stack-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-serverless-stack-plugin) | [changelog](./heft-plugins/heft-serverless-stack-plugin/CHANGELOG.md) | [@rushstack/heft-serverless-stack-plugin](https://www.npmjs.com/package/@rushstack/heft-serverless-stack-plugin) | | [/heft-plugins/heft-storybook-plugin](./heft-plugins/heft-storybook-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-storybook-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-storybook-plugin) | [changelog](./heft-plugins/heft-storybook-plugin/CHANGELOG.md) | [@rushstack/heft-storybook-plugin](https://www.npmjs.com/package/@rushstack/heft-storybook-plugin) | | [/heft-plugins/heft-typescript-plugin](./heft-plugins/heft-typescript-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-typescript-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-typescript-plugin) | [changelog](./heft-plugins/heft-typescript-plugin/CHANGELOG.md) | [@rushstack/heft-typescript-plugin](https://www.npmjs.com/package/@rushstack/heft-typescript-plugin) | +| [/heft-plugins/heft-vscode-extension-plugin](./heft-plugins/heft-vscode-extension-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-vscode-extension-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-vscode-extension-plugin) | [changelog](./heft-plugins/heft-vscode-extension-plugin/CHANGELOG.md) | [@rushstack/heft-vscode-extension-plugin](https://www.npmjs.com/package/@rushstack/heft-vscode-extension-plugin) | | [/heft-plugins/heft-webpack4-plugin](./heft-plugins/heft-webpack4-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-webpack4-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-webpack4-plugin) | [changelog](./heft-plugins/heft-webpack4-plugin/CHANGELOG.md) | [@rushstack/heft-webpack4-plugin](https://www.npmjs.com/package/@rushstack/heft-webpack4-plugin) | | [/heft-plugins/heft-webpack5-plugin](./heft-plugins/heft-webpack5-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-webpack5-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-webpack5-plugin) | [changelog](./heft-plugins/heft-webpack5-plugin/CHANGELOG.md) | [@rushstack/heft-webpack5-plugin](https://www.npmjs.com/package/@rushstack/heft-webpack5-plugin) | | [/libraries/api-extractor-model](./libraries/api-extractor-model/) | [![npm version](https://badge.fury.io/js/%40microsoft%2Fapi-extractor-model.svg)](https://badge.fury.io/js/%40microsoft%2Fapi-extractor-model) | [changelog](./libraries/api-extractor-model/CHANGELOG.md) | [@microsoft/api-extractor-model](https://www.npmjs.com/package/@microsoft/api-extractor-model) | +| [/libraries/credential-cache](./libraries/credential-cache/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fcredential-cache.svg)](https://badge.fury.io/js/%40rushstack%2Fcredential-cache) | [changelog](./libraries/credential-cache/CHANGELOG.md) | [@rushstack/credential-cache](https://www.npmjs.com/package/@rushstack/credential-cache) | | [/libraries/debug-certificate-manager](./libraries/debug-certificate-manager/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fdebug-certificate-manager.svg)](https://badge.fury.io/js/%40rushstack%2Fdebug-certificate-manager) | [changelog](./libraries/debug-certificate-manager/CHANGELOG.md) | [@rushstack/debug-certificate-manager](https://www.npmjs.com/package/@rushstack/debug-certificate-manager) | | [/libraries/heft-config-file](./libraries/heft-config-file/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-config-file.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-config-file) | [changelog](./libraries/heft-config-file/CHANGELOG.md) | [@rushstack/heft-config-file](https://www.npmjs.com/package/@rushstack/heft-config-file) | | [/libraries/load-themed-styles](./libraries/load-themed-styles/) | [![npm version](https://badge.fury.io/js/%40microsoft%2Fload-themed-styles.svg)](https://badge.fury.io/js/%40microsoft%2Fload-themed-styles) | [changelog](./libraries/load-themed-styles/CHANGELOG.md) | [@microsoft/load-themed-styles](https://www.npmjs.com/package/@microsoft/load-themed-styles) | | [/libraries/localization-utilities](./libraries/localization-utilities/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Flocalization-utilities.svg)](https://badge.fury.io/js/%40rushstack%2Flocalization-utilities) | [changelog](./libraries/localization-utilities/CHANGELOG.md) | [@rushstack/localization-utilities](https://www.npmjs.com/package/@rushstack/localization-utilities) | +| [/libraries/lookup-by-path](./libraries/lookup-by-path/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Flookup-by-path.svg)](https://badge.fury.io/js/%40rushstack%2Flookup-by-path) | [changelog](./libraries/lookup-by-path/CHANGELOG.md) | [@rushstack/lookup-by-path](https://www.npmjs.com/package/@rushstack/lookup-by-path) | | [/libraries/module-minifier](./libraries/module-minifier/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fmodule-minifier.svg)](https://badge.fury.io/js/%40rushstack%2Fmodule-minifier) | [changelog](./libraries/module-minifier/CHANGELOG.md) | [@rushstack/module-minifier](https://www.npmjs.com/package/@rushstack/module-minifier) | | [/libraries/node-core-library](./libraries/node-core-library/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fnode-core-library.svg)](https://badge.fury.io/js/%40rushstack%2Fnode-core-library) | [changelog](./libraries/node-core-library/CHANGELOG.md) | [@rushstack/node-core-library](https://www.npmjs.com/package/@rushstack/node-core-library) | +| [/libraries/npm-check-fork](./libraries/npm-check-fork/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fnpm-check-fork.svg)](https://badge.fury.io/js/%40rushstack%2Fnpm-check-fork) | [changelog](./libraries/npm-check-fork/CHANGELOG.md) | [@rushstack/npm-check-fork](https://www.npmjs.com/package/@rushstack/npm-check-fork) | +| [/libraries/operation-graph](./libraries/operation-graph/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Foperation-graph.svg)](https://badge.fury.io/js/%40rushstack%2Foperation-graph) | [changelog](./libraries/operation-graph/CHANGELOG.md) | [@rushstack/operation-graph](https://www.npmjs.com/package/@rushstack/operation-graph) | | [/libraries/package-deps-hash](./libraries/package-deps-hash/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fpackage-deps-hash.svg)](https://badge.fury.io/js/%40rushstack%2Fpackage-deps-hash) | [changelog](./libraries/package-deps-hash/CHANGELOG.md) | [@rushstack/package-deps-hash](https://www.npmjs.com/package/@rushstack/package-deps-hash) | | [/libraries/package-extractor](./libraries/package-extractor/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fpackage-extractor.svg)](https://badge.fury.io/js/%40rushstack%2Fpackage-extractor) | [changelog](./libraries/package-extractor/CHANGELOG.md) | [@rushstack/package-extractor](https://www.npmjs.com/package/@rushstack/package-extractor) | +| [/libraries/problem-matcher](./libraries/problem-matcher/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fproblem-matcher.svg)](https://badge.fury.io/js/%40rushstack%2Fproblem-matcher) | [changelog](./libraries/problem-matcher/CHANGELOG.md) | [@rushstack/problem-matcher](https://www.npmjs.com/package/@rushstack/problem-matcher) | | [/libraries/rig-package](./libraries/rig-package/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frig-package.svg)](https://badge.fury.io/js/%40rushstack%2Frig-package) | [changelog](./libraries/rig-package/CHANGELOG.md) | [@rushstack/rig-package](https://www.npmjs.com/package/@rushstack/rig-package) | | [/libraries/rush-lib](./libraries/rush-lib/) | [![npm version](https://badge.fury.io/js/%40microsoft%2Frush-lib.svg)](https://badge.fury.io/js/%40microsoft%2Frush-lib) | | [@microsoft/rush-lib](https://www.npmjs.com/package/@microsoft/rush-lib) | +| [/libraries/rush-pnpm-kit-v10](./libraries/rush-pnpm-kit-v10/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v10.svg)](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v10) | [changelog](./libraries/rush-pnpm-kit-v10/CHANGELOG.md) | [@rushstack/rush-pnpm-kit-v10](https://www.npmjs.com/package/@rushstack/rush-pnpm-kit-v10) | +| [/libraries/rush-pnpm-kit-v8](./libraries/rush-pnpm-kit-v8/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v8.svg)](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v8) | [changelog](./libraries/rush-pnpm-kit-v8/CHANGELOG.md) | [@rushstack/rush-pnpm-kit-v8](https://www.npmjs.com/package/@rushstack/rush-pnpm-kit-v8) | +| [/libraries/rush-pnpm-kit-v9](./libraries/rush-pnpm-kit-v9/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v9.svg)](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v9) | [changelog](./libraries/rush-pnpm-kit-v9/CHANGELOG.md) | [@rushstack/rush-pnpm-kit-v9](https://www.npmjs.com/package/@rushstack/rush-pnpm-kit-v9) | | [/libraries/rush-sdk](./libraries/rush-sdk/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-sdk.svg)](https://badge.fury.io/js/%40rushstack%2Frush-sdk) | | [@rushstack/rush-sdk](https://www.npmjs.com/package/@rushstack/rush-sdk) | | [/libraries/stream-collator](./libraries/stream-collator/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fstream-collator.svg)](https://badge.fury.io/js/%40rushstack%2Fstream-collator) | [changelog](./libraries/stream-collator/CHANGELOG.md) | [@rushstack/stream-collator](https://www.npmjs.com/package/@rushstack/stream-collator) | | [/libraries/terminal](./libraries/terminal/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fterminal.svg)](https://badge.fury.io/js/%40rushstack%2Fterminal) | [changelog](./libraries/terminal/CHANGELOG.md) | [@rushstack/terminal](https://www.npmjs.com/package/@rushstack/terminal) | @@ -83,10 +107,16 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/libraries/typings-generator](./libraries/typings-generator/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Ftypings-generator.svg)](https://badge.fury.io/js/%40rushstack%2Ftypings-generator) | [changelog](./libraries/typings-generator/CHANGELOG.md) | [@rushstack/typings-generator](https://www.npmjs.com/package/@rushstack/typings-generator) | | [/libraries/worker-pool](./libraries/worker-pool/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fworker-pool.svg)](https://badge.fury.io/js/%40rushstack%2Fworker-pool) | [changelog](./libraries/worker-pool/CHANGELOG.md) | [@rushstack/worker-pool](https://www.npmjs.com/package/@rushstack/worker-pool) | | [/rigs/heft-node-rig](./rigs/heft-node-rig/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-node-rig.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-node-rig) | [changelog](./rigs/heft-node-rig/CHANGELOG.md) | [@rushstack/heft-node-rig](https://www.npmjs.com/package/@rushstack/heft-node-rig) | +| [/rigs/heft-vscode-extension-rig](./rigs/heft-vscode-extension-rig/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-vscode-extension-rig.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-vscode-extension-rig) | [changelog](./rigs/heft-vscode-extension-rig/CHANGELOG.md) | [@rushstack/heft-vscode-extension-rig](https://www.npmjs.com/package/@rushstack/heft-vscode-extension-rig) | | [/rigs/heft-web-rig](./rigs/heft-web-rig/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-web-rig.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-web-rig) | [changelog](./rigs/heft-web-rig/CHANGELOG.md) | [@rushstack/heft-web-rig](https://www.npmjs.com/package/@rushstack/heft-web-rig) | | [/rush-plugins/rush-amazon-s3-build-cache-plugin](./rush-plugins/rush-amazon-s3-build-cache-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-amazon-s3-build-cache-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-amazon-s3-build-cache-plugin) | | [@rushstack/rush-amazon-s3-build-cache-plugin](https://www.npmjs.com/package/@rushstack/rush-amazon-s3-build-cache-plugin) | | [/rush-plugins/rush-azure-storage-build-cache-plugin](./rush-plugins/rush-azure-storage-build-cache-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-azure-storage-build-cache-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-azure-storage-build-cache-plugin) | | [@rushstack/rush-azure-storage-build-cache-plugin](https://www.npmjs.com/package/@rushstack/rush-azure-storage-build-cache-plugin) | +| [/rush-plugins/rush-bridge-cache-plugin](./rush-plugins/rush-bridge-cache-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-bridge-cache-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-bridge-cache-plugin) | | [@rushstack/rush-bridge-cache-plugin](https://www.npmjs.com/package/@rushstack/rush-bridge-cache-plugin) | +| [/rush-plugins/rush-buildxl-graph-plugin](./rush-plugins/rush-buildxl-graph-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-buildxl-graph-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-buildxl-graph-plugin) | | [@rushstack/rush-buildxl-graph-plugin](https://www.npmjs.com/package/@rushstack/rush-buildxl-graph-plugin) | | [/rush-plugins/rush-http-build-cache-plugin](./rush-plugins/rush-http-build-cache-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-http-build-cache-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-http-build-cache-plugin) | | [@rushstack/rush-http-build-cache-plugin](https://www.npmjs.com/package/@rushstack/rush-http-build-cache-plugin) | +| [/rush-plugins/rush-mcp-docs-plugin](./rush-plugins/rush-mcp-docs-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-mcp-docs-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-mcp-docs-plugin) | [changelog](./rush-plugins/rush-mcp-docs-plugin/CHANGELOG.md) | [@rushstack/rush-mcp-docs-plugin](https://www.npmjs.com/package/@rushstack/rush-mcp-docs-plugin) | +| [/rush-plugins/rush-redis-cobuild-plugin](./rush-plugins/rush-redis-cobuild-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-redis-cobuild-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-redis-cobuild-plugin) | | [@rushstack/rush-redis-cobuild-plugin](https://www.npmjs.com/package/@rushstack/rush-redis-cobuild-plugin) | +| [/rush-plugins/rush-resolver-cache-plugin](./rush-plugins/rush-resolver-cache-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-resolver-cache-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-resolver-cache-plugin) | | [@rushstack/rush-resolver-cache-plugin](https://www.npmjs.com/package/@rushstack/rush-resolver-cache-plugin) | | [/rush-plugins/rush-serve-plugin](./rush-plugins/rush-serve-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-serve-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-serve-plugin) | | [@rushstack/rush-serve-plugin](https://www.npmjs.com/package/@rushstack/rush-serve-plugin) | | [/webpack/hashed-folder-copy-plugin](./webpack/hashed-folder-copy-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fhashed-folder-copy-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fhashed-folder-copy-plugin) | [changelog](./webpack/hashed-folder-copy-plugin/CHANGELOG.md) | [@rushstack/hashed-folder-copy-plugin](https://www.npmjs.com/package/@rushstack/hashed-folder-copy-plugin) | | [/webpack/loader-load-themed-styles](./webpack/loader-load-themed-styles/) | [![npm version](https://badge.fury.io/js/%40microsoft%2Floader-load-themed-styles.svg)](https://badge.fury.io/js/%40microsoft%2Floader-load-themed-styles) | [changelog](./webpack/loader-load-themed-styles/CHANGELOG.md) | [@microsoft/loader-load-themed-styles](https://www.npmjs.com/package/@microsoft/loader-load-themed-styles) | @@ -95,6 +125,7 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/webpack/set-webpack-public-path-plugin](./webpack/set-webpack-public-path-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fset-webpack-public-path-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fset-webpack-public-path-plugin) | [changelog](./webpack/set-webpack-public-path-plugin/CHANGELOG.md) | [@rushstack/set-webpack-public-path-plugin](https://www.npmjs.com/package/@rushstack/set-webpack-public-path-plugin) | | [/webpack/webpack-embedded-dependencies-plugin](./webpack/webpack-embedded-dependencies-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fwebpack-embedded-dependencies-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fwebpack-embedded-dependencies-plugin) | [changelog](./webpack/webpack-embedded-dependencies-plugin/CHANGELOG.md) | [@rushstack/webpack-embedded-dependencies-plugin](https://www.npmjs.com/package/@rushstack/webpack-embedded-dependencies-plugin) | | [/webpack/webpack-plugin-utilities](./webpack/webpack-plugin-utilities/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fwebpack-plugin-utilities.svg)](https://badge.fury.io/js/%40rushstack%2Fwebpack-plugin-utilities) | [changelog](./webpack/webpack-plugin-utilities/CHANGELOG.md) | [@rushstack/webpack-plugin-utilities](https://www.npmjs.com/package/@rushstack/webpack-plugin-utilities) | +| [/webpack/webpack-workspace-resolve-plugin](./webpack/webpack-workspace-resolve-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fwebpack-workspace-resolve-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fwebpack-workspace-resolve-plugin) | [changelog](./webpack/webpack-workspace-resolve-plugin/CHANGELOG.md) | [@rushstack/webpack-workspace-resolve-plugin](https://www.npmjs.com/package/@rushstack/webpack-workspace-resolve-plugin) | | [/webpack/webpack4-localization-plugin](./webpack/webpack4-localization-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fwebpack4-localization-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fwebpack4-localization-plugin) | [changelog](./webpack/webpack4-localization-plugin/CHANGELOG.md) | [@rushstack/webpack4-localization-plugin](https://www.npmjs.com/package/@rushstack/webpack4-localization-plugin) | | [/webpack/webpack4-module-minifier-plugin](./webpack/webpack4-module-minifier-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fwebpack4-module-minifier-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fwebpack4-module-minifier-plugin) | [changelog](./webpack/webpack4-module-minifier-plugin/CHANGELOG.md) | [@rushstack/webpack4-module-minifier-plugin](https://www.npmjs.com/package/@rushstack/webpack4-module-minifier-plugin) | | [/webpack/webpack5-load-themed-styles-loader](./webpack/webpack5-load-themed-styles-loader/) | [![npm version](https://badge.fury.io/js/%40microsoft%2Fwebpack5-load-themed-styles-loader.svg)](https://badge.fury.io/js/%40microsoft%2Fwebpack5-load-themed-styles-loader) | [changelog](./webpack/webpack5-load-themed-styles-loader/CHANGELOG.md) | [@microsoft/webpack5-load-themed-styles-loader](https://www.npmjs.com/package/@microsoft/webpack5-load-themed-styles-loader) | @@ -113,38 +144,61 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/build-tests-samples/heft-node-jest-tutorial](./build-tests-samples/heft-node-jest-tutorial/) | (Copy of sample project) Building this project is a regression test for Heft | | [/build-tests-samples/heft-node-rig-tutorial](./build-tests-samples/heft-node-rig-tutorial/) | (Copy of sample project) Building this project is a regression test for Heft | | [/build-tests-samples/heft-serverless-stack-tutorial](./build-tests-samples/heft-serverless-stack-tutorial/) | (Copy of sample project) Building this project is a regression test for Heft | -| [/build-tests-samples/heft-storybook-react-tutorial](./build-tests-samples/heft-storybook-react-tutorial/) | (Copy of sample project) Building this project is a regression test for Heft | -| [/build-tests-samples/heft-storybook-react-tutorial-storykit](./build-tests-samples/heft-storybook-react-tutorial-storykit/) | Storybook build dependencies for heft-storybook-react-tutorial | +| [/build-tests-samples/heft-storybook-v6-react-tutorial](./build-tests-samples/heft-storybook-v6-react-tutorial/) | (Copy of sample project) Building this project is a regression test for Heft | +| [/build-tests-samples/heft-storybook-v6-react-tutorial-app](./build-tests-samples/heft-storybook-v6-react-tutorial-app/) | Building this project is a regression test for heft-storybook-plugin | +| [/build-tests-samples/heft-storybook-v6-react-tutorial-storykit](./build-tests-samples/heft-storybook-v6-react-tutorial-storykit/) | Storybook build dependencies for heft-storybook-v6-react-tutorial | +| [/build-tests-samples/heft-storybook-v9-react-tutorial](./build-tests-samples/heft-storybook-v9-react-tutorial/) | (Copy of sample project) Building this project is a regression test for Heft | +| [/build-tests-samples/heft-storybook-v9-react-tutorial-app](./build-tests-samples/heft-storybook-v9-react-tutorial-app/) | Building this project is a regression test for heft-storybook-plugin | +| [/build-tests-samples/heft-storybook-v9-react-tutorial-storykit](./build-tests-samples/heft-storybook-v9-react-tutorial-storykit/) | Storybook build dependencies for heft-storybook-v9-react-tutorial | | [/build-tests-samples/heft-web-rig-app-tutorial](./build-tests-samples/heft-web-rig-app-tutorial/) | (Copy of sample project) Building this project is a regression test for Heft | | [/build-tests-samples/heft-web-rig-library-tutorial](./build-tests-samples/heft-web-rig-library-tutorial/) | (Copy of sample project) Building this project is a regression test for Heft | | [/build-tests-samples/heft-webpack-basic-tutorial](./build-tests-samples/heft-webpack-basic-tutorial/) | (Copy of sample project) Building this project is a regression test for Heft | | [/build-tests-samples/packlets-tutorial](./build-tests-samples/packlets-tutorial/) | (Copy of sample project) Building this project is a regression test for @rushstack/eslint-plugin-packlets | +| [/build-tests-subspace/rush-lib-test](./build-tests-subspace/rush-lib-test/) | A minimal example project that imports APIs from @rushstack/rush-lib | +| [/build-tests-subspace/rush-sdk-test](./build-tests-subspace/rush-sdk-test/) | A minimal example project that imports APIs from @rushstack/rush-sdk | +| [/build-tests-subspace/typescript-newest-test](./build-tests-subspace/typescript-newest-test/) | Building this project tests Heft with the newest supported TypeScript compiler version | +| [/build-tests-subspace/typescript-v4-test](./build-tests-subspace/typescript-v4-test/) | Building this project tests Heft with TypeScript v4 | | [/build-tests/api-documenter-scenarios](./build-tests/api-documenter-scenarios/) | Building this project is a regression test for api-documenter | | [/build-tests/api-documenter-test](./build-tests/api-documenter-test/) | Building this project is a regression test for api-documenter | +| [/build-tests/api-extractor-d-cts-test](./build-tests/api-extractor-d-cts-test/) | Building this project is a regression test for api-extractor | +| [/build-tests/api-extractor-d-mts-test](./build-tests/api-extractor-d-mts-test/) | Building this project is a regression test for api-extractor | | [/build-tests/api-extractor-lib1-test](./build-tests/api-extractor-lib1-test/) | Building this project is a regression test for api-extractor | | [/build-tests/api-extractor-lib2-test](./build-tests/api-extractor-lib2-test/) | Building this project is a regression test for api-extractor | | [/build-tests/api-extractor-lib3-test](./build-tests/api-extractor-lib3-test/) | Building this project is a regression test for api-extractor | +| [/build-tests/api-extractor-lib4-test](./build-tests/api-extractor-lib4-test/) | Building this project is a regression test for api-extractor | +| [/build-tests/api-extractor-lib5-test](./build-tests/api-extractor-lib5-test/) | Building this project is a regression test for api-extractor | | [/build-tests/api-extractor-scenarios](./build-tests/api-extractor-scenarios/) | Building this project is a regression test for api-extractor | | [/build-tests/api-extractor-test-01](./build-tests/api-extractor-test-01/) | Building this project is a regression test for api-extractor | | [/build-tests/api-extractor-test-02](./build-tests/api-extractor-test-02/) | Building this project is a regression test for api-extractor | | [/build-tests/api-extractor-test-03](./build-tests/api-extractor-test-03/) | Building this project is a regression test for api-extractor | | [/build-tests/api-extractor-test-04](./build-tests/api-extractor-test-04/) | Building this project is a regression test for api-extractor | +| [/build-tests/api-extractor-test-05](./build-tests/api-extractor-test-05/) | Building this project is a regression test for api-extractor | +| [/build-tests/eslint-7-11-test](./build-tests/eslint-7-11-test/) | This project contains a build test to validate ESLint 7.11.0 compatibility with the latest version of @rushstack/eslint-config (and by extension, the ESLint plugin) | +| [/build-tests/eslint-7-7-test](./build-tests/eslint-7-7-test/) | This project contains a build test to validate ESLint 7.7.0 compatibility with the latest version of @rushstack/eslint-config (and by extension, the ESLint plugin) | | [/build-tests/eslint-7-test](./build-tests/eslint-7-test/) | This project contains a build test to validate ESLint 7 compatibility with the latest version of @rushstack/eslint-config (and by extension, the ESLint plugin) | -| [/build-tests/hashed-folder-copy-plugin-webpack4-test](./build-tests/hashed-folder-copy-plugin-webpack4-test/) | Building this project exercises @rushstack/hashed-folder-copy-plugin with Webpack 4. | +| [/build-tests/eslint-8-test](./build-tests/eslint-8-test/) | This project contains a build test to validate ESLint 8 compatibility with the latest version of @rushstack/eslint-config (and by extension, the ESLint plugin) | +| [/build-tests/eslint-9-test](./build-tests/eslint-9-test/) | This project contains a build test to validate ESLint 9 compatibility with the latest version of @rushstack/eslint-config (and by extension, the ESLint plugin) | +| [/build-tests/eslint-bulk-suppressions-test](./build-tests/eslint-bulk-suppressions-test/) | Sample code to test eslint bulk suppressions | +| [/build-tests/eslint-bulk-suppressions-test-flat](./build-tests/eslint-bulk-suppressions-test-flat/) | Sample code to test eslint bulk suppressions with flat configs | +| [/build-tests/eslint-bulk-suppressions-test-legacy](./build-tests/eslint-bulk-suppressions-test-legacy/) | Sample code to test eslint bulk suppressions for versions of eslint < 8.57.0 | | [/build-tests/hashed-folder-copy-plugin-webpack5-test](./build-tests/hashed-folder-copy-plugin-webpack5-test/) | Building this project exercises @rushstack/hashed-folder-copy-plugin with Webpack 5. NOTE - THIS TEST IS CURRENTLY EXPECTED TO BE BROKEN | | [/build-tests/heft-copy-files-test](./build-tests/heft-copy-files-test/) | Building this project tests copying files with Heft | +| [/build-tests/heft-example-lifecycle-plugin](./build-tests/heft-example-lifecycle-plugin/) | This is an example heft plugin for testing the lifecycle hooks | | [/build-tests/heft-example-plugin-01](./build-tests/heft-example-plugin-01/) | This is an example heft plugin that exposes hooks for other plugins | | [/build-tests/heft-example-plugin-02](./build-tests/heft-example-plugin-02/) | This is an example heft plugin that taps the hooks exposed from heft-example-plugin-01 | | [/build-tests/heft-fastify-test](./build-tests/heft-fastify-test/) | This project tests Heft support for the Fastify framework for Node.js services | | [/build-tests/heft-jest-preset-test](./build-tests/heft-jest-preset-test/) | This project illustrates configuring a Jest preset in a minimal Heft project | | [/build-tests/heft-jest-reporters-test](./build-tests/heft-jest-reporters-test/) | This project illustrates configuring Jest reporters in a minimal Heft project | +| [/build-tests/heft-json-schema-typings-plugin-test](./build-tests/heft-json-schema-typings-plugin-test/) | This project illustrates configuring Jest reporters in a minimal Heft project | | [/build-tests/heft-minimal-rig-test](./build-tests/heft-minimal-rig-test/) | This is a minimal rig package that is imported by the 'heft-minimal-rig-usage-test' project | | [/build-tests/heft-minimal-rig-usage-test](./build-tests/heft-minimal-rig-usage-test/) | A test project for Heft that resolves its compiler from the 'heft-minimal-rig-test' package | | [/build-tests/heft-node-everything-esm-module-test](./build-tests/heft-node-everything-esm-module-test/) | Building this project tests every task and config file for Heft when targeting the Node.js runtime when configured to use ESM module support | | [/build-tests/heft-node-everything-test](./build-tests/heft-node-everything-test/) | Building this project tests every task and config file for Heft when targeting the Node.js runtime | | [/build-tests/heft-parameter-plugin](./build-tests/heft-parameter-plugin/) | This project contains a Heft plugin that adds a custom parameter to built-in actions | | [/build-tests/heft-parameter-plugin-test](./build-tests/heft-parameter-plugin-test/) | This project exercises a built-in Heft action with a custom parameter | +| [/build-tests/heft-rspack-everything-test](./build-tests/heft-rspack-everything-test/) | Building this project tests every task and config file for Heft when targeting the web browser runtime using Rspack | | [/build-tests/heft-sass-test](./build-tests/heft-sass-test/) | This project illustrates a minimal tutorial Heft project targeting the web browser runtime | +| [/build-tests/heft-swc-test](./build-tests/heft-swc-test/) | Building this project tests building with SWC | | [/build-tests/heft-typescript-composite-test](./build-tests/heft-typescript-composite-test/) | Building this project tests behavior of Heft when the tsconfig.json file uses project references. | | [/build-tests/heft-typescript-v2-test](./build-tests/heft-typescript-v2-test/) | Building this project tests building with TypeScript v2 | | [/build-tests/heft-typescript-v3-test](./build-tests/heft-typescript-v3-test/) | Building this project tests building with TypeScript v3 | @@ -152,21 +206,35 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/build-tests/heft-web-rig-library-test](./build-tests/heft-web-rig-library-test/) | A test project for Heft that exercises the '@rushstack/heft-web-rig' package | | [/build-tests/heft-webpack4-everything-test](./build-tests/heft-webpack4-everything-test/) | Building this project tests every task and config file for Heft when targeting the web browser runtime using Webpack 4 | | [/build-tests/heft-webpack5-everything-test](./build-tests/heft-webpack5-everything-test/) | Building this project tests every task and config file for Heft when targeting the web browser runtime using Webpack 5 | -| [/build-tests/install-test-workspace](./build-tests/install-test-workspace/) | | | [/build-tests/localization-plugin-test-01](./build-tests/localization-plugin-test-01/) | Building this project exercises @microsoft/localization-plugin. This tests that the plugin works correctly without any localized resources. | | [/build-tests/localization-plugin-test-02](./build-tests/localization-plugin-test-02/) | Building this project exercises @microsoft/localization-plugin. This tests that the loader works correctly with the exportAsDefault option unset. | | [/build-tests/localization-plugin-test-03](./build-tests/localization-plugin-test-03/) | Building this project exercises @microsoft/localization-plugin. This tests that the plugin works correctly with the exportAsDefault option set to true. | +| [/build-tests/package-extractor-test-01](./build-tests/package-extractor-test-01/) | This project is used by tests in the @rushstack/package-extractor package. | +| [/build-tests/package-extractor-test-02](./build-tests/package-extractor-test-02/) | This project is used by tests in the @rushstack/package-extractor package. | +| [/build-tests/package-extractor-test-03](./build-tests/package-extractor-test-03/) | This project is used by tests in the @rushstack/package-extractor package. | +| [/build-tests/package-extractor-test-04](./build-tests/package-extractor-test-04/) | This project is used by tests in the @rushstack/package-extractor package. | +| [/build-tests/run-scenarios-helpers](./build-tests/run-scenarios-helpers/) | Helpers for the *-scenarios test projects. | | [/build-tests/rush-amazon-s3-build-cache-plugin-integration-test](./build-tests/rush-amazon-s3-build-cache-plugin-integration-test/) | Tests connecting to an amazon S3 endpoint | | [/build-tests/rush-lib-declaration-paths-test](./build-tests/rush-lib-declaration-paths-test/) | This project ensures all of the paths in rush-lib/lib/... have imports that resolve correctly. If this project builds, all `lib/**/*.d.ts` files in the `@microsoft/rush-lib` package are valid. | +| [/build-tests/rush-mcp-example-plugin](./build-tests/rush-mcp-example-plugin/) | Example showing how to create a plugin for @rushstack/mcp-server | | [/build-tests/rush-project-change-analyzer-test](./build-tests/rush-project-change-analyzer-test/) | This is an example project that uses rush-lib's ProjectChangeAnalyzer to | -| [/build-tests/set-webpack-public-path-plugin-webpack4-test](./build-tests/set-webpack-public-path-plugin-webpack4-test/) | Building this project tests the set-webpack-public-path-plugin using Webpack 4 | -| [/build-tests/ts-command-line-test](./build-tests/ts-command-line-test/) | Building this project is a regression test for ts-command-line | +| [/build-tests/rush-redis-cobuild-plugin-integration-test](./build-tests/rush-redis-cobuild-plugin-integration-test/) | Tests connecting to an redis server | +| [/build-tests/set-webpack-public-path-plugin-test](./build-tests/set-webpack-public-path-plugin-test/) | Building this project tests the set-webpack-public-path-plugin | +| [/build-tests/webpack-local-version-test](./build-tests/webpack-local-version-test/) | Building this project tests the rig loading for the local version of webpack | +| [/eslint/local-eslint-config](./eslint/local-eslint-config/) | An ESLint configuration consumed projects inside the rushstack repo. | | [/libraries/rush-themed-ui](./libraries/rush-themed-ui/) | Rush Component Library: a set of themed components for rush projects | | [/libraries/rushell](./libraries/rushell/) | Execute shell commands using a consistent syntax on every platform | | [/repo-scripts/doc-plugin-rush-stack](./repo-scripts/doc-plugin-rush-stack/) | API Documenter plugin used with the rushstack.io website | | [/repo-scripts/generate-api-docs](./repo-scripts/generate-api-docs/) | Used to generate API docs for the rushstack.io website | | [/repo-scripts/repo-toolbox](./repo-scripts/repo-toolbox/) | Used to execute various operations specific to this repo | +| [/rigs/decoupled-local-node-rig](./rigs/decoupled-local-node-rig/) | A rig package for Node.js projects that build using Heft inside the RushStack repository, but are dependencies of @rushstack/heft-node-rig or local-node-rig. | +| [/rigs/local-node-rig](./rigs/local-node-rig/) | A rig package for Node.js projects that build using Heft inside the RushStack repository. | +| [/rigs/local-web-rig](./rigs/local-web-rig/) | A rig package for Web projects that build using Heft inside the RushStack repository. | | [/rush-plugins/rush-litewatch-plugin](./rush-plugins/rush-litewatch-plugin/) | An experimental alternative approach for multi-project watch mode | +| [/vscode-extensions/debug-certificate-manager-vscode-extension](./vscode-extensions/debug-certificate-manager-vscode-extension/) | VS Code extension to manage debug TLS certificates and sync them to the VS Code workspace. Works with VS Code remote development (Codespaces, SSH, Dev Containers, WSL, VS Code Tunnels). | +| [/vscode-extensions/rush-vscode-command-webview](./vscode-extensions/rush-vscode-command-webview/) | Part of the Rush Stack VSCode extension, provides a UI for invoking Rush commands | +| [/vscode-extensions/rush-vscode-extension](./vscode-extensions/rush-vscode-extension/) | Enhanced experience for monorepos that use the Rush Stack toolchain | +| [/vscode-extensions/vscode-shared](./vscode-extensions/vscode-shared/) | | | [/webpack/webpack-deep-imports-plugin](./webpack/webpack-deep-imports-plugin/) | This plugin creates a bundle and commonJS files in a 'lib' folder mirroring modules in another 'lib' folder. | diff --git a/apps/api-documenter/.eslintrc.js b/apps/api-documenter/.eslintrc.js deleted file mode 100644 index 4c934799d67..00000000000 --- a/apps/api-documenter/.eslintrc.js +++ /dev/null @@ -1,10 +0,0 @@ -// This is a workaround for https://github.com/eslint/eslint/issues/3458 -require('@rushstack/eslint-config/patch/modern-module-resolution'); - -module.exports = { - extends: [ - '@rushstack/eslint-config/profile/node-trusted-tool', - '@rushstack/eslint-config/mixins/friendly-locals' - ], - parserOptions: { tsconfigRootDir: __dirname } -}; diff --git a/apps/api-documenter/.npmignore b/apps/api-documenter/.npmignore index 302dbc5b019..bc349f9a4be 100644 --- a/apps/api-documenter/.npmignore +++ b/apps/api-documenter/.npmignore @@ -8,6 +8,11 @@ !/lib/** !/lib-*/** !/dist/** + +!CHANGELOG.md +!CHANGELOG.json +!heft-plugin.json +!rush-plugin-manifest.json !ThirdPartyNotice.txt # Ignore certain patterns that should not get published. @@ -19,12 +24,9 @@ # NOTE: These don't need to be specified, because NPM includes them automatically. # # package.json -# README (and its variants) -# CHANGELOG (and its variants) -# LICENSE / LICENCE - -#-------------------------------------------- -# DO NOT MODIFY THE TEMPLATE ABOVE THIS LINE -#-------------------------------------------- +# README.md +# LICENSE -# (Add your project-specific overrides here) \ No newline at end of file +# --------------------------------------------------------------------------- +# DO NOT MODIFY ABOVE THIS LINE! Add any project-specific overrides below. +# --------------------------------------------------------------------------- diff --git a/apps/api-documenter/CHANGELOG.json b/apps/api-documenter/CHANGELOG.json index 2461c4588a5..bec35f20067 100644 --- a/apps/api-documenter/CHANGELOG.json +++ b/apps/api-documenter/CHANGELOG.json @@ -1,6 +1,2540 @@ { "name": "@microsoft/api-documenter", "entries": [ + { + "version": "7.28.5", + "tag": "@microsoft/api-documenter_v7.28.5", + "date": "Thu, 08 Jan 2026 01:12:30 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.10`" + } + ] + } + }, + { + "version": "7.28.4", + "tag": "@microsoft/api-documenter_v7.28.4", + "date": "Wed, 07 Jan 2026 01:12:24 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.21.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.7`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.9`" + } + ] + } + }, + { + "version": "7.28.3", + "tag": "@microsoft/api-documenter_v7.28.3", + "date": "Mon, 05 Jan 2026 16:12:49 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.20.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.6`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.8`" + } + ] + } + }, + { + "version": "7.28.2", + "tag": "@microsoft/api-documenter_v7.28.2", + "date": "Sat, 06 Dec 2025 01:12:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.32.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.19.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.5`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.5`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.7`" + } + ] + } + }, + { + "version": "7.28.1", + "tag": "@microsoft/api-documenter_v7.28.1", + "date": "Fri, 21 Nov 2025 16:13:56 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.32.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.19.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.4`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.6`" + } + ] + } + }, + { + "version": "7.28.0", + "tag": "@microsoft/api-documenter_v7.28.0", + "date": "Wed, 12 Nov 2025 01:12:56 GMT", + "comments": { + "minor": [ + { + "comment": "Bump the `@microsoft/tsdoc` dependency to `~0.16.0`." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.32.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.5`" + } + ] + } + }, + { + "version": "7.27.4", + "tag": "@microsoft/api-documenter_v7.27.4", + "date": "Tue, 04 Nov 2025 08:15:14 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.4`" + } + ] + } + }, + { + "version": "7.27.3", + "tag": "@microsoft/api-documenter_v7.27.3", + "date": "Fri, 24 Oct 2025 00:13:38 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.31.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.18.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.3`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.3`" + } + ] + } + }, + { + "version": "7.27.2", + "tag": "@microsoft/api-documenter_v7.27.2", + "date": "Wed, 22 Oct 2025 00:57:54 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.31.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.17.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.2`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.2`" + } + ] + } + }, + { + "version": "7.27.1", + "tag": "@microsoft/api-documenter_v7.27.1", + "date": "Wed, 08 Oct 2025 00:13:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.31.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.17.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.1`" + } + ] + } + }, + { + "version": "7.27.0", + "tag": "@microsoft/api-documenter_v7.27.0", + "date": "Fri, 03 Oct 2025 20:09:59 GMT", + "comments": { + "minor": [ + { + "comment": "Normalize import of builtin modules to use the `node:` protocol." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.31.0`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.16.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.0`" + } + ] + } + }, + { + "version": "7.26.36", + "tag": "@microsoft/api-documenter_v7.26.36", + "date": "Tue, 30 Sep 2025 23:57:45 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.9`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.15.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.18.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.5`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.0.0`" + } + ] + } + }, + { + "version": "7.26.35", + "tag": "@microsoft/api-documenter_v7.26.35", + "date": "Tue, 30 Sep 2025 20:33:50 GMT", + "comments": { + "patch": [ + { + "comment": "Upgraded `js-yaml` dependency" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.8`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.15.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.17.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.4`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.75.0`" + } + ] + } + }, + { + "version": "7.26.34", + "tag": "@microsoft/api-documenter_v7.26.34", + "date": "Fri, 12 Sep 2025 15:13:07 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.74.5`" + } + ] + } + }, + { + "version": "7.26.33", + "tag": "@microsoft/api-documenter_v7.26.33", + "date": "Thu, 11 Sep 2025 00:22:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.16.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.3`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.74.4`" + } + ] + } + }, + { + "version": "7.26.32", + "tag": "@microsoft/api-documenter_v7.26.32", + "date": "Tue, 19 Aug 2025 20:45:02 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.74.3`" + } + ] + } + }, + { + "version": "7.26.31", + "tag": "@microsoft/api-documenter_v7.26.31", + "date": "Fri, 01 Aug 2025 00:12:48 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.74.2`" + } + ] + } + }, + { + "version": "7.26.30", + "tag": "@microsoft/api-documenter_v7.26.30", + "date": "Wed, 23 Jul 2025 20:55:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.7`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.14.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.15.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.2`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.74.1`" + } + ] + } + }, + { + "version": "7.26.29", + "tag": "@microsoft/api-documenter_v7.26.29", + "date": "Tue, 24 Jun 2025 00:11:43 GMT", + "comments": { + "patch": [ + { + "comment": "Ensure a new line is inserted after rendering a table" + } + ] + } + }, + { + "version": "7.26.28", + "tag": "@microsoft/api-documenter_v7.26.28", + "date": "Sat, 21 Jun 2025 00:13:15 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.74.0`" + } + ] + } + }, + { + "version": "7.26.27", + "tag": "@microsoft/api-documenter_v7.26.27", + "date": "Tue, 13 May 2025 02:09:20 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.73.6`" + } + ] + } + }, + { + "version": "7.26.26", + "tag": "@microsoft/api-documenter_v7.26.26", + "date": "Thu, 01 May 2025 15:11:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.73.5`" + } + ] + } + }, + { + "version": "7.26.25", + "tag": "@microsoft/api-documenter_v7.26.25", + "date": "Thu, 01 May 2025 00:11:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.13.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.15.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.73.4`" + } + ] + } + }, + { + "version": "7.26.24", + "tag": "@microsoft/api-documenter_v7.26.24", + "date": "Fri, 25 Apr 2025 00:11:32 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.73.3`" + } + ] + } + }, + { + "version": "7.26.23", + "tag": "@microsoft/api-documenter_v7.26.23", + "date": "Mon, 21 Apr 2025 22:24:25 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.73.2`" + } + ] + } + }, + { + "version": "7.26.22", + "tag": "@microsoft/api-documenter_v7.26.22", + "date": "Thu, 17 Apr 2025 00:11:21 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.73.1`" + } + ] + } + }, + { + "version": "7.26.21", + "tag": "@microsoft/api-documenter_v7.26.21", + "date": "Tue, 15 Apr 2025 15:11:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.73.0`" + } + ] + } + }, + { + "version": "7.26.20", + "tag": "@microsoft/api-documenter_v7.26.20", + "date": "Wed, 09 Apr 2025 00:11:02 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.72.0`" + } + ] + } + }, + { + "version": "7.26.19", + "tag": "@microsoft/api-documenter_v7.26.19", + "date": "Fri, 04 Apr 2025 18:34:35 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.71.2`" + } + ] + } + }, + { + "version": "7.26.18", + "tag": "@microsoft/api-documenter_v7.26.18", + "date": "Tue, 25 Mar 2025 15:11:15 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.13.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.15.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.7`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.71.1`" + } + ] + } + }, + { + "version": "7.26.17", + "tag": "@microsoft/api-documenter_v7.26.17", + "date": "Wed, 12 Mar 2025 22:41:36 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.71.0`" + } + ] + } + }, + { + "version": "7.26.16", + "tag": "@microsoft/api-documenter_v7.26.16", + "date": "Wed, 12 Mar 2025 00:11:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.70.1`" + } + ] + } + }, + { + "version": "7.26.15", + "tag": "@microsoft/api-documenter_v7.26.15", + "date": "Tue, 11 Mar 2025 02:12:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.12.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.15.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.6`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.70.0`" + } + ] + } + }, + { + "version": "7.26.14", + "tag": "@microsoft/api-documenter_v7.26.14", + "date": "Tue, 11 Mar 2025 00:11:25 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.69.3`" + } + ] + } + }, + { + "version": "7.26.13", + "tag": "@microsoft/api-documenter_v7.26.13", + "date": "Sat, 01 Mar 2025 05:00:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.69.2`" + } + ] + } + }, + { + "version": "7.26.12", + "tag": "@microsoft/api-documenter_v7.26.12", + "date": "Thu, 27 Feb 2025 01:10:39 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.69.1`" + } + ] + } + }, + { + "version": "7.26.11", + "tag": "@microsoft/api-documenter_v7.26.11", + "date": "Wed, 26 Feb 2025 16:11:11 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.69.0`" + } + ] + } + }, + { + "version": "7.26.10", + "tag": "@microsoft/api-documenter_v7.26.10", + "date": "Sat, 22 Feb 2025 01:11:11 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.18`" + } + ] + } + }, + { + "version": "7.26.9", + "tag": "@microsoft/api-documenter_v7.26.9", + "date": "Wed, 19 Feb 2025 18:53:48 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.17`" + } + ] + } + }, + { + "version": "7.26.8", + "tag": "@microsoft/api-documenter_v7.26.8", + "date": "Wed, 12 Feb 2025 01:10:52 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.15.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.5`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.16`" + } + ] + } + }, + { + "version": "7.26.7", + "tag": "@microsoft/api-documenter_v7.26.7", + "date": "Thu, 30 Jan 2025 16:10:36 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.15`" + } + ] + } + }, + { + "version": "7.26.6", + "tag": "@microsoft/api-documenter_v7.26.6", + "date": "Thu, 30 Jan 2025 01:11:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.11.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.6`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.4`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.14`" + } + ] + } + }, + { + "version": "7.26.5", + "tag": "@microsoft/api-documenter_v7.26.5", + "date": "Thu, 09 Jan 2025 01:10:10 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.10.2`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.5`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.3`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.13`" + } + ] + } + }, + { + "version": "7.26.4", + "tag": "@microsoft/api-documenter_v7.26.4", + "date": "Tue, 07 Jan 2025 22:17:32 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.12`" + } + ] + } + }, + { + "version": "7.26.3", + "tag": "@microsoft/api-documenter_v7.26.3", + "date": "Sat, 14 Dec 2024 01:11:07 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.10.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.2`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.11`" + } + ] + } + }, + { + "version": "7.26.2", + "tag": "@microsoft/api-documenter_v7.26.2", + "date": "Mon, 09 Dec 2024 20:31:43 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.10`" + } + ] + } + }, + { + "version": "7.26.1", + "tag": "@microsoft/api-documenter_v7.26.1", + "date": "Tue, 03 Dec 2024 16:11:07 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.9`" + } + ] + } + }, + { + "version": "7.26.0", + "tag": "@microsoft/api-documenter_v7.26.0", + "date": "Sat, 23 Nov 2024 01:18:55 GMT", + "comments": { + "minor": [ + { + "comment": "Update TSDoc dependencies." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.8`" + } + ] + } + }, + { + "version": "7.25.22", + "tag": "@microsoft/api-documenter_v7.25.22", + "date": "Fri, 22 Nov 2024 01:10:43 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.9`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.10.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.7`" + } + ] + } + }, + { + "version": "7.25.21", + "tag": "@microsoft/api-documenter_v7.25.21", + "date": "Thu, 24 Oct 2024 00:15:47 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.6`" + } + ] + } + }, + { + "version": "7.25.20", + "tag": "@microsoft/api-documenter_v7.25.20", + "date": "Mon, 21 Oct 2024 18:50:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.5`" + } + ] + } + }, + { + "version": "7.25.19", + "tag": "@microsoft/api-documenter_v7.25.19", + "date": "Thu, 17 Oct 2024 08:35:06 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.4`" + } + ] + } + }, + { + "version": "7.25.18", + "tag": "@microsoft/api-documenter_v7.25.18", + "date": "Tue, 15 Oct 2024 00:12:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.3`" + } + ] + } + }, + { + "version": "7.25.17", + "tag": "@microsoft/api-documenter_v7.25.17", + "date": "Wed, 02 Oct 2024 00:11:19 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.2`" + } + ] + } + }, + { + "version": "7.25.16", + "tag": "@microsoft/api-documenter_v7.25.16", + "date": "Tue, 01 Oct 2024 00:11:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.1`" + } + ] + } + }, + { + "version": "7.25.15", + "tag": "@microsoft/api-documenter_v7.25.15", + "date": "Mon, 30 Sep 2024 15:12:19 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.0`" + } + ] + } + }, + { + "version": "7.25.14", + "tag": "@microsoft/api-documenter_v7.25.14", + "date": "Fri, 13 Sep 2024 00:11:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.8`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.9.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.8`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.67.2`" + } + ] + } + }, + { + "version": "7.25.13", + "tag": "@microsoft/api-documenter_v7.25.13", + "date": "Tue, 10 Sep 2024 20:08:11 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.7`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.8.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.7`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.67.1`" + } + ] + } + }, + { + "version": "7.25.12", + "tag": "@microsoft/api-documenter_v7.25.12", + "date": "Wed, 21 Aug 2024 05:43:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.7.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.6`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.67.0`" + } + ] + } + }, + { + "version": "7.25.11", + "tag": "@microsoft/api-documenter_v7.25.11", + "date": "Mon, 12 Aug 2024 22:16:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.6.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.13.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.5`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.26`" + } + ] + } + }, + { + "version": "7.25.10", + "tag": "@microsoft/api-documenter_v7.25.10", + "date": "Fri, 02 Aug 2024 17:26:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.4`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.25`" + } + ] + } + }, + { + "version": "7.25.9", + "tag": "@microsoft/api-documenter_v7.25.9", + "date": "Sat, 27 Jul 2024 00:10:27 GMT", + "comments": { + "patch": [ + { + "comment": "Include CHANGELOG.md in published releases again" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.5.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.13.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.3`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.24`" + } + ] + } + }, + { + "version": "7.25.8", + "tag": "@microsoft/api-documenter_v7.25.8", + "date": "Wed, 24 Jul 2024 00:12:14 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.23`" + } + ] + } + }, + { + "version": "7.25.7", + "tag": "@microsoft/api-documenter_v7.25.7", + "date": "Wed, 17 Jul 2024 06:55:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.13.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.2`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.22`" + } + ] + } + }, + { + "version": "7.25.6", + "tag": "@microsoft/api-documenter_v7.25.6", + "date": "Wed, 17 Jul 2024 00:11:19 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.21`" + } + ] + } + }, + { + "version": "7.25.5", + "tag": "@microsoft/api-documenter_v7.25.5", + "date": "Tue, 16 Jul 2024 00:36:21 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.5.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.13.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.20`" + } + ] + } + }, + { + "version": "7.25.4", + "tag": "@microsoft/api-documenter_v7.25.4", + "date": "Thu, 27 Jun 2024 21:01:36 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.19`" + } + ] + } + }, + { + "version": "7.25.3", + "tag": "@microsoft/api-documenter_v7.25.3", + "date": "Mon, 03 Jun 2024 23:43:15 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.18`" + } + ] + } + }, + { + "version": "7.25.2", + "tag": "@microsoft/api-documenter_v7.25.2", + "date": "Thu, 30 May 2024 00:13:05 GMT", + "comments": { + "patch": [ + { + "comment": "Include missing `type` modifiers on type-only exports." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.4.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.13.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.17`" + } + ] + } + }, + { + "version": "7.25.1", + "tag": "@microsoft/api-documenter_v7.25.1", + "date": "Wed, 29 May 2024 02:03:50 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.4.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.12.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.5`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.16`" + } + ] + } + }, + { + "version": "7.25.0", + "tag": "@microsoft/api-documenter_v7.25.0", + "date": "Wed, 29 May 2024 00:10:52 GMT", + "comments": { + "minor": [ + { + "comment": "Bump TSDoc dependencies." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.15`" + } + ] + } + }, + { + "version": "7.24.13", + "tag": "@microsoft/api-documenter_v7.24.13", + "date": "Tue, 28 May 2024 15:10:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.21`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.3.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.12.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.4`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.14`" + } + ] + } + }, + { + "version": "7.24.12", + "tag": "@microsoft/api-documenter_v7.24.12", + "date": "Tue, 28 May 2024 00:09:47 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.20`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.2.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.12.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.3`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.13`" + } + ] + } + }, + { + "version": "7.24.11", + "tag": "@microsoft/api-documenter_v7.24.11", + "date": "Sat, 25 May 2024 04:54:07 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.19`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.1.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.12.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.2`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.12`" + } + ] + } + }, + { + "version": "7.24.10", + "tag": "@microsoft/api-documenter_v7.24.10", + "date": "Fri, 24 May 2024 00:15:08 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.11`" + } + ] + } + }, + { + "version": "7.24.9", + "tag": "@microsoft/api-documenter_v7.24.9", + "date": "Thu, 23 May 2024 02:26:56 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.18`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.0.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.11.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.10`" + } + ] + } + }, + { + "version": "7.24.8", + "tag": "@microsoft/api-documenter_v7.24.8", + "date": "Thu, 16 May 2024 15:10:22 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.9`" + } + ] + } + }, + { + "version": "7.24.7", + "tag": "@microsoft/api-documenter_v7.24.7", + "date": "Wed, 15 May 2024 23:42:58 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.11.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.20.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.8`" + } + ] + } + }, + { + "version": "7.24.6", + "tag": "@microsoft/api-documenter_v7.24.6", + "date": "Wed, 15 May 2024 06:04:17 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.17`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.3.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.10.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.20.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.7`" + } + ] + } + }, + { + "version": "7.24.5", + "tag": "@microsoft/api-documenter_v7.24.5", + "date": "Fri, 10 May 2024 05:33:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.16`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.2.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.10.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.5`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.6`" + } + ] + } + }, + { + "version": "7.24.4", + "tag": "@microsoft/api-documenter_v7.24.4", + "date": "Wed, 08 May 2024 22:23:50 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.4`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.5`" + } + ] + } + }, + { + "version": "7.24.3", + "tag": "@microsoft/api-documenter_v7.24.3", + "date": "Mon, 06 May 2024 15:11:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.15`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.2.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.10.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.3`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.4`" + } + ] + } + }, + { + "version": "7.24.2", + "tag": "@microsoft/api-documenter_v7.24.2", + "date": "Wed, 10 Apr 2024 15:10:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.14`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.1.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.10.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.2`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.3`" + } + ] + } + }, + { + "version": "7.24.1", + "tag": "@microsoft/api-documenter_v7.24.1", + "date": "Tue, 19 Mar 2024 15:10:18 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.2`" + } + ] + } + }, + { + "version": "7.24.0", + "tag": "@microsoft/api-documenter_v7.24.0", + "date": "Sat, 16 Mar 2024 00:11:37 GMT", + "comments": { + "minor": [ + { + "comment": "Emit HTML tags for tables instead of Markdown code." + } + ] + } + }, + { + "version": "7.23.38", + "tag": "@microsoft/api-documenter_v7.23.38", + "date": "Fri, 15 Mar 2024 00:12:40 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.1`" + } + ] + } + }, + { + "version": "7.23.37", + "tag": "@microsoft/api-documenter_v7.23.37", + "date": "Tue, 05 Mar 2024 01:19:24 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.66.0`" + } + ] + } + }, + { + "version": "7.23.36", + "tag": "@microsoft/api-documenter_v7.23.36", + "date": "Sun, 03 Mar 2024 20:58:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.65.10`" + } + ] + } + }, + { + "version": "7.23.35", + "tag": "@microsoft/api-documenter_v7.23.35", + "date": "Sat, 02 Mar 2024 02:22:23 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.65.9`" + } + ] + } + }, + { + "version": "7.23.34", + "tag": "@microsoft/api-documenter_v7.23.34", + "date": "Fri, 01 Mar 2024 01:10:08 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.18.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.65.8`" + } + ] + } + }, + { + "version": "7.23.33", + "tag": "@microsoft/api-documenter_v7.23.33", + "date": "Thu, 29 Feb 2024 07:11:45 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.65.7`" + } + ] + } + }, + { + "version": "7.23.32", + "tag": "@microsoft/api-documenter_v7.23.32", + "date": "Wed, 28 Feb 2024 16:09:27 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.18.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.65.6`" + } + ] + } + }, + { + "version": "7.23.31", + "tag": "@microsoft/api-documenter_v7.23.31", + "date": "Sat, 24 Feb 2024 23:02:51 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.10.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.4`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.65.5`" + } + ] + } + }, + { + "version": "7.23.30", + "tag": "@microsoft/api-documenter_v7.23.30", + "date": "Thu, 22 Feb 2024 01:36:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.65.4`" + } + ] + } + }, + { + "version": "7.23.29", + "tag": "@microsoft/api-documenter_v7.23.29", + "date": "Wed, 21 Feb 2024 21:45:28 GMT", + "comments": { + "patch": [ + { + "comment": "Replace the dependency on the `colors` package with `Colorize` from `@rushstack/terminal`." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.13`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.0.2`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.9.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.3`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.65.3`" + } + ] + } + }, + { + "version": "7.23.28", + "tag": "@microsoft/api-documenter_v7.23.28", + "date": "Wed, 21 Feb 2024 08:55:47 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.65.2`" + } + ] + } + }, + { + "version": "7.23.27", + "tag": "@microsoft/api-documenter_v7.23.27", + "date": "Tue, 20 Feb 2024 21:45:10 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.12`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.0.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.65.1`" + } + ] + } + }, + { + "version": "7.23.26", + "tag": "@microsoft/api-documenter_v7.23.26", + "date": "Tue, 20 Feb 2024 16:10:52 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.65.0`" + } + ] + } + }, + { + "version": "7.23.25", + "tag": "@microsoft/api-documenter_v7.23.25", + "date": "Mon, 19 Feb 2024 21:54:27 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.11`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.0.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.64.8`" + } + ] + } + }, + { + "version": "7.23.24", + "tag": "@microsoft/api-documenter_v7.23.24", + "date": "Sat, 17 Feb 2024 06:24:34 GMT", + "comments": { + "patch": [ + { + "comment": "Fix broken link to API documentation" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.10`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.66.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.2`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.64.7`" + } + ] + } + }, + { + "version": "7.23.23", + "tag": "@microsoft/api-documenter_v7.23.23", + "date": "Thu, 08 Feb 2024 01:09:21 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.9`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.66.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.64.6`" + } + ] + } + }, + { + "version": "7.23.22", + "tag": "@microsoft/api-documenter_v7.23.22", + "date": "Wed, 07 Feb 2024 01:11:18 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.64.5`" + } + ] + } + }, + { + "version": "7.23.21", + "tag": "@microsoft/api-documenter_v7.23.21", + "date": "Mon, 05 Feb 2024 23:46:52 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.8`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.65.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.64.4`" + } + ] + } + }, + { + "version": "7.23.20", + "tag": "@microsoft/api-documenter_v7.23.20", + "date": "Thu, 25 Jan 2024 01:09:30 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.7`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.64.2`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.64.3`" + } + ] + } + }, + { + "version": "7.23.19", + "tag": "@microsoft/api-documenter_v7.23.19", + "date": "Tue, 23 Jan 2024 20:12:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.64.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.64.2`" + } + ] + } + }, + { + "version": "7.23.18", + "tag": "@microsoft/api-documenter_v7.23.18", + "date": "Tue, 23 Jan 2024 16:15:05 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.64.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.64.1`" + } + ] + } + }, + { + "version": "7.23.17", + "tag": "@microsoft/api-documenter_v7.23.17", + "date": "Tue, 16 Jan 2024 18:30:11 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.64.0`" + } + ] + } + }, + { + "version": "7.23.16", + "tag": "@microsoft/api-documenter_v7.23.16", + "date": "Wed, 03 Jan 2024 00:31:18 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.63.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.63.6`" + } + ] + } + }, + { + "version": "7.23.15", + "tag": "@microsoft/api-documenter_v7.23.15", + "date": "Wed, 20 Dec 2023 01:09:45 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.63.5`" + } + ] + } + }, + { + "version": "7.23.14", + "tag": "@microsoft/api-documenter_v7.23.14", + "date": "Thu, 07 Dec 2023 03:44:13 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.62.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.63.4`" + } + ] + } + }, + { + "version": "7.23.13", + "tag": "@microsoft/api-documenter_v7.23.13", + "date": "Tue, 05 Dec 2023 01:10:16 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.63.3`" + } + ] + } + }, + { + "version": "7.23.12", + "tag": "@microsoft/api-documenter_v7.23.12", + "date": "Fri, 10 Nov 2023 18:02:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.63.2`" + } + ] + } + }, + { + "version": "7.23.11", + "tag": "@microsoft/api-documenter_v7.23.11", + "date": "Wed, 01 Nov 2023 23:11:35 GMT", + "comments": { + "patch": [ + { + "comment": "Fix line endings in published package." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.63.1`" + } + ] + } + }, + { + "version": "7.23.10", + "tag": "@microsoft/api-documenter_v7.23.10", + "date": "Mon, 30 Oct 2023 23:36:37 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.63.0`" + } + ] + } + }, + { + "version": "7.23.9", + "tag": "@microsoft/api-documenter_v7.23.9", + "date": "Sun, 01 Oct 2023 02:56:29 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.62.3`" + } + ] + } + }, + { + "version": "7.23.8", + "tag": "@microsoft/api-documenter_v7.23.8", + "date": "Sat, 30 Sep 2023 00:20:51 GMT", + "comments": { + "patch": [ + { + "comment": "Add notes for @alpha items when encountered. Mimics the existing behavior for @beta items." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.62.2`" + } + ] + } + }, + { + "version": "7.23.7", + "tag": "@microsoft/api-documenter_v7.23.7", + "date": "Thu, 28 Sep 2023 20:53:17 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.61.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.62.1`" + } + ] + } + }, + { + "version": "7.23.6", + "tag": "@microsoft/api-documenter_v7.23.6", + "date": "Wed, 27 Sep 2023 00:21:38 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.62.0`" + } + ] + } + }, + { + "version": "7.23.5", + "tag": "@microsoft/api-documenter_v7.23.5", + "date": "Tue, 26 Sep 2023 21:02:30 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.61.3`" + } + ] + } + }, + { + "version": "7.23.4", + "tag": "@microsoft/api-documenter_v7.23.4", + "date": "Tue, 26 Sep 2023 09:30:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update type-only imports to include the type modifier." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.60.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.16.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.61.2`" + } + ] + } + }, + { + "version": "7.23.3", + "tag": "@microsoft/api-documenter_v7.23.3", + "date": "Mon, 25 Sep 2023 23:38:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.61.1`" + } + ] + } + }, + { + "version": "7.23.2", + "tag": "@microsoft/api-documenter_v7.23.2", + "date": "Fri, 22 Sep 2023 00:05:50 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.61.0`" + } + ] + } + }, + { + "version": "7.23.1", + "tag": "@microsoft/api-documenter_v7.23.1", + "date": "Tue, 19 Sep 2023 15:21:51 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.60.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.24`" + } + ] + } + }, + { + "version": "7.23.0", + "tag": "@microsoft/api-documenter_v7.23.0", + "date": "Fri, 15 Sep 2023 00:36:58 GMT", + "comments": { + "minor": [ + { + "comment": "Update @types/node from 14 to 18" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.0`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.60.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.16.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.4`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.59.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.23`" + } + ] + } + }, + { + "version": "7.22.33", + "tag": "@microsoft/api-documenter_v7.22.33", + "date": "Tue, 08 Aug 2023 07:10:39 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.7`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.15.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.3`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.58.2`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.22`" + } + ] + } + }, + { + "version": "7.22.32", + "tag": "@microsoft/api-documenter_v7.22.32", + "date": "Mon, 31 Jul 2023 15:19:05 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.21`" + } + ] + } + }, + { + "version": "7.22.31", + "tag": "@microsoft/api-documenter_v7.22.31", + "date": "Sat, 29 Jul 2023 00:22:50 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.58.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.20`" + } + ] + } + }, + { + "version": "7.22.30", + "tag": "@microsoft/api-documenter_v7.22.30", + "date": "Thu, 20 Jul 2023 20:47:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.58.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.19`" + } + ] + } + }, + { + "version": "7.22.29", + "tag": "@microsoft/api-documenter_v7.22.29", + "date": "Wed, 19 Jul 2023 00:20:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.6`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.57.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.18`" + } + ] + } + }, + { + "version": "7.22.28", + "tag": "@microsoft/api-documenter_v7.22.28", + "date": "Fri, 14 Jul 2023 15:20:45 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.17`" + } + ] + } + }, + { + "version": "7.22.27", + "tag": "@microsoft/api-documenter_v7.22.27", + "date": "Thu, 13 Jul 2023 00:22:37 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.57.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.16`" + } + ] + } + }, + { + "version": "7.22.26", + "tag": "@microsoft/api-documenter_v7.22.26", + "date": "Wed, 12 Jul 2023 15:20:39 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.56.3`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.15`" + } + ] + } + }, + { + "version": "7.22.25", + "tag": "@microsoft/api-documenter_v7.22.25", + "date": "Wed, 12 Jul 2023 00:23:29 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.14`" + } + ] + } + }, + { + "version": "7.22.24", + "tag": "@microsoft/api-documenter_v7.22.24", + "date": "Fri, 07 Jul 2023 00:19:32 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.56.2`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.13`" + } + ] + } + }, + { + "version": "7.22.23", + "tag": "@microsoft/api-documenter_v7.22.23", + "date": "Thu, 06 Jul 2023 00:16:19 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.5`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.56.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.12`" + } + ] + } + }, + { + "version": "7.22.22", + "tag": "@microsoft/api-documenter_v7.22.22", + "date": "Tue, 04 Jul 2023 00:18:47 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.11`" + } + ] + } + }, + { + "version": "7.22.21", + "tag": "@microsoft/api-documenter_v7.22.21", + "date": "Mon, 19 Jun 2023 22:40:21 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.56.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.10`" + } + ] + } + }, + { + "version": "7.22.20", + "tag": "@microsoft/api-documenter_v7.22.20", + "date": "Thu, 15 Jun 2023 00:21:01 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.15.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.2`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.55.2`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.9`" + } + ] + } + }, + { + "version": "7.22.19", + "tag": "@microsoft/api-documenter_v7.22.19", + "date": "Wed, 14 Jun 2023 00:19:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.55.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.8`" + } + ] + } + }, + { + "version": "7.22.18", + "tag": "@microsoft/api-documenter_v7.22.18", + "date": "Tue, 13 Jun 2023 15:17:20 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.55.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.7`" + } + ] + } + }, + { + "version": "7.22.17", + "tag": "@microsoft/api-documenter_v7.22.17", + "date": "Tue, 13 Jun 2023 01:49:01 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.15.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.54.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.6`" + } + ] + } + }, + { + "version": "7.22.16", + "tag": "@microsoft/api-documenter_v7.22.16", + "date": "Fri, 09 Jun 2023 18:05:34 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.53.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.5`" + } + ] + } + }, + { + "version": "7.22.15", + "tag": "@microsoft/api-documenter_v7.22.15", + "date": "Fri, 09 Jun 2023 15:23:15 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.4`" + } + ] + } + }, + { + "version": "7.22.14", + "tag": "@microsoft/api-documenter_v7.22.14", + "date": "Fri, 09 Jun 2023 00:19:49 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.53.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.3`" + } + ] + } + }, + { + "version": "7.22.13", + "tag": "@microsoft/api-documenter_v7.22.13", + "date": "Thu, 08 Jun 2023 15:21:17 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.52.2`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.2`" + } + ] + } + }, + { + "version": "7.22.12", + "tag": "@microsoft/api-documenter_v7.22.12", + "date": "Thu, 08 Jun 2023 00:20:02 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.52.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.1`" + } + ] + } + }, + { + "version": "7.22.11", + "tag": "@microsoft/api-documenter_v7.22.11", + "date": "Wed, 07 Jun 2023 22:45:16 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.14.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.52.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.2.0`" + } + ] + } + }, + { + "version": "7.22.10", + "tag": "@microsoft/api-documenter_v7.22.10", + "date": "Tue, 06 Jun 2023 02:52:51 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.1.0`" + } + ] + } + }, + { + "version": "7.22.9", + "tag": "@microsoft/api-documenter_v7.22.9", + "date": "Mon, 05 Jun 2023 21:45:21 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-node-rig\" to `2.0.1`" + } + ] + } + }, { "version": "7.22.8", "tag": "@microsoft/api-documenter_v7.22.8", diff --git a/apps/api-documenter/CHANGELOG.md b/apps/api-documenter/CHANGELOG.md index 22c01078c3a..4acc9bac3cc 100644 --- a/apps/api-documenter/CHANGELOG.md +++ b/apps/api-documenter/CHANGELOG.md @@ -1,6 +1,781 @@ # Change Log - @microsoft/api-documenter -This log was last generated on Fri, 02 Jun 2023 02:01:12 GMT and should not be manually modified. +This log was last generated on Thu, 08 Jan 2026 01:12:30 GMT and should not be manually modified. + +## 7.28.5 +Thu, 08 Jan 2026 01:12:30 GMT + +_Version update only_ + +## 7.28.4 +Wed, 07 Jan 2026 01:12:24 GMT + +_Version update only_ + +## 7.28.3 +Mon, 05 Jan 2026 16:12:49 GMT + +_Version update only_ + +## 7.28.2 +Sat, 06 Dec 2025 01:12:28 GMT + +_Version update only_ + +## 7.28.1 +Fri, 21 Nov 2025 16:13:56 GMT + +_Version update only_ + +## 7.28.0 +Wed, 12 Nov 2025 01:12:56 GMT + +### Minor changes + +- Bump the `@microsoft/tsdoc` dependency to `~0.16.0`. + +## 7.27.4 +Tue, 04 Nov 2025 08:15:14 GMT + +_Version update only_ + +## 7.27.3 +Fri, 24 Oct 2025 00:13:38 GMT + +_Version update only_ + +## 7.27.2 +Wed, 22 Oct 2025 00:57:54 GMT + +_Version update only_ + +## 7.27.1 +Wed, 08 Oct 2025 00:13:28 GMT + +_Version update only_ + +## 7.27.0 +Fri, 03 Oct 2025 20:09:59 GMT + +### Minor changes + +- Normalize import of builtin modules to use the `node:` protocol. + +## 7.26.36 +Tue, 30 Sep 2025 23:57:45 GMT + +_Version update only_ + +## 7.26.35 +Tue, 30 Sep 2025 20:33:50 GMT + +### Patches + +- Upgraded `js-yaml` dependency + +## 7.26.34 +Fri, 12 Sep 2025 15:13:07 GMT + +_Version update only_ + +## 7.26.33 +Thu, 11 Sep 2025 00:22:31 GMT + +_Version update only_ + +## 7.26.32 +Tue, 19 Aug 2025 20:45:02 GMT + +_Version update only_ + +## 7.26.31 +Fri, 01 Aug 2025 00:12:48 GMT + +_Version update only_ + +## 7.26.30 +Wed, 23 Jul 2025 20:55:57 GMT + +_Version update only_ + +## 7.26.29 +Tue, 24 Jun 2025 00:11:43 GMT + +### Patches + +- Ensure a new line is inserted after rendering a table + +## 7.26.28 +Sat, 21 Jun 2025 00:13:15 GMT + +_Version update only_ + +## 7.26.27 +Tue, 13 May 2025 02:09:20 GMT + +_Version update only_ + +## 7.26.26 +Thu, 01 May 2025 15:11:33 GMT + +_Version update only_ + +## 7.26.25 +Thu, 01 May 2025 00:11:12 GMT + +_Version update only_ + +## 7.26.24 +Fri, 25 Apr 2025 00:11:32 GMT + +_Version update only_ + +## 7.26.23 +Mon, 21 Apr 2025 22:24:25 GMT + +_Version update only_ + +## 7.26.22 +Thu, 17 Apr 2025 00:11:21 GMT + +_Version update only_ + +## 7.26.21 +Tue, 15 Apr 2025 15:11:57 GMT + +_Version update only_ + +## 7.26.20 +Wed, 09 Apr 2025 00:11:02 GMT + +_Version update only_ + +## 7.26.19 +Fri, 04 Apr 2025 18:34:35 GMT + +_Version update only_ + +## 7.26.18 +Tue, 25 Mar 2025 15:11:15 GMT + +_Version update only_ + +## 7.26.17 +Wed, 12 Mar 2025 22:41:36 GMT + +_Version update only_ + +## 7.26.16 +Wed, 12 Mar 2025 00:11:31 GMT + +_Version update only_ + +## 7.26.15 +Tue, 11 Mar 2025 02:12:33 GMT + +_Version update only_ + +## 7.26.14 +Tue, 11 Mar 2025 00:11:25 GMT + +_Version update only_ + +## 7.26.13 +Sat, 01 Mar 2025 05:00:09 GMT + +_Version update only_ + +## 7.26.12 +Thu, 27 Feb 2025 01:10:39 GMT + +_Version update only_ + +## 7.26.11 +Wed, 26 Feb 2025 16:11:11 GMT + +_Version update only_ + +## 7.26.10 +Sat, 22 Feb 2025 01:11:11 GMT + +_Version update only_ + +## 7.26.9 +Wed, 19 Feb 2025 18:53:48 GMT + +_Version update only_ + +## 7.26.8 +Wed, 12 Feb 2025 01:10:52 GMT + +_Version update only_ + +## 7.26.7 +Thu, 30 Jan 2025 16:10:36 GMT + +_Version update only_ + +## 7.26.6 +Thu, 30 Jan 2025 01:11:42 GMT + +_Version update only_ + +## 7.26.5 +Thu, 09 Jan 2025 01:10:10 GMT + +_Version update only_ + +## 7.26.4 +Tue, 07 Jan 2025 22:17:32 GMT + +_Version update only_ + +## 7.26.3 +Sat, 14 Dec 2024 01:11:07 GMT + +_Version update only_ + +## 7.26.2 +Mon, 09 Dec 2024 20:31:43 GMT + +_Version update only_ + +## 7.26.1 +Tue, 03 Dec 2024 16:11:07 GMT + +_Version update only_ + +## 7.26.0 +Sat, 23 Nov 2024 01:18:55 GMT + +### Minor changes + +- Update TSDoc dependencies. + +## 7.25.22 +Fri, 22 Nov 2024 01:10:43 GMT + +_Version update only_ + +## 7.25.21 +Thu, 24 Oct 2024 00:15:47 GMT + +_Version update only_ + +## 7.25.20 +Mon, 21 Oct 2024 18:50:09 GMT + +_Version update only_ + +## 7.25.19 +Thu, 17 Oct 2024 08:35:06 GMT + +_Version update only_ + +## 7.25.18 +Tue, 15 Oct 2024 00:12:31 GMT + +_Version update only_ + +## 7.25.17 +Wed, 02 Oct 2024 00:11:19 GMT + +_Version update only_ + +## 7.25.16 +Tue, 01 Oct 2024 00:11:28 GMT + +_Version update only_ + +## 7.25.15 +Mon, 30 Sep 2024 15:12:19 GMT + +_Version update only_ + +## 7.25.14 +Fri, 13 Sep 2024 00:11:42 GMT + +_Version update only_ + +## 7.25.13 +Tue, 10 Sep 2024 20:08:11 GMT + +_Version update only_ + +## 7.25.12 +Wed, 21 Aug 2024 05:43:04 GMT + +_Version update only_ + +## 7.25.11 +Mon, 12 Aug 2024 22:16:04 GMT + +_Version update only_ + +## 7.25.10 +Fri, 02 Aug 2024 17:26:42 GMT + +_Version update only_ + +## 7.25.9 +Sat, 27 Jul 2024 00:10:27 GMT + +### Patches + +- Include CHANGELOG.md in published releases again + +## 7.25.8 +Wed, 24 Jul 2024 00:12:14 GMT + +_Version update only_ + +## 7.25.7 +Wed, 17 Jul 2024 06:55:09 GMT + +_Version update only_ + +## 7.25.6 +Wed, 17 Jul 2024 00:11:19 GMT + +_Version update only_ + +## 7.25.5 +Tue, 16 Jul 2024 00:36:21 GMT + +_Version update only_ + +## 7.25.4 +Thu, 27 Jun 2024 21:01:36 GMT + +_Version update only_ + +## 7.25.3 +Mon, 03 Jun 2024 23:43:15 GMT + +_Version update only_ + +## 7.25.2 +Thu, 30 May 2024 00:13:05 GMT + +### Patches + +- Include missing `type` modifiers on type-only exports. + +## 7.25.1 +Wed, 29 May 2024 02:03:50 GMT + +_Version update only_ + +## 7.25.0 +Wed, 29 May 2024 00:10:52 GMT + +### Minor changes + +- Bump TSDoc dependencies. + +## 7.24.13 +Tue, 28 May 2024 15:10:09 GMT + +_Version update only_ + +## 7.24.12 +Tue, 28 May 2024 00:09:47 GMT + +_Version update only_ + +## 7.24.11 +Sat, 25 May 2024 04:54:07 GMT + +_Version update only_ + +## 7.24.10 +Fri, 24 May 2024 00:15:08 GMT + +_Version update only_ + +## 7.24.9 +Thu, 23 May 2024 02:26:56 GMT + +_Version update only_ + +## 7.24.8 +Thu, 16 May 2024 15:10:22 GMT + +_Version update only_ + +## 7.24.7 +Wed, 15 May 2024 23:42:58 GMT + +_Version update only_ + +## 7.24.6 +Wed, 15 May 2024 06:04:17 GMT + +_Version update only_ + +## 7.24.5 +Fri, 10 May 2024 05:33:33 GMT + +_Version update only_ + +## 7.24.4 +Wed, 08 May 2024 22:23:50 GMT + +_Version update only_ + +## 7.24.3 +Mon, 06 May 2024 15:11:04 GMT + +_Version update only_ + +## 7.24.2 +Wed, 10 Apr 2024 15:10:09 GMT + +_Version update only_ + +## 7.24.1 +Tue, 19 Mar 2024 15:10:18 GMT + +_Version update only_ + +## 7.24.0 +Sat, 16 Mar 2024 00:11:37 GMT + +### Minor changes + +- Emit HTML tags for tables instead of Markdown code. + +## 7.23.38 +Fri, 15 Mar 2024 00:12:40 GMT + +_Version update only_ + +## 7.23.37 +Tue, 05 Mar 2024 01:19:24 GMT + +_Version update only_ + +## 7.23.36 +Sun, 03 Mar 2024 20:58:12 GMT + +_Version update only_ + +## 7.23.35 +Sat, 02 Mar 2024 02:22:23 GMT + +_Version update only_ + +## 7.23.34 +Fri, 01 Mar 2024 01:10:08 GMT + +_Version update only_ + +## 7.23.33 +Thu, 29 Feb 2024 07:11:45 GMT + +_Version update only_ + +## 7.23.32 +Wed, 28 Feb 2024 16:09:27 GMT + +_Version update only_ + +## 7.23.31 +Sat, 24 Feb 2024 23:02:51 GMT + +_Version update only_ + +## 7.23.30 +Thu, 22 Feb 2024 01:36:09 GMT + +_Version update only_ + +## 7.23.29 +Wed, 21 Feb 2024 21:45:28 GMT + +### Patches + +- Replace the dependency on the `colors` package with `Colorize` from `@rushstack/terminal`. + +## 7.23.28 +Wed, 21 Feb 2024 08:55:47 GMT + +_Version update only_ + +## 7.23.27 +Tue, 20 Feb 2024 21:45:10 GMT + +_Version update only_ + +## 7.23.26 +Tue, 20 Feb 2024 16:10:52 GMT + +_Version update only_ + +## 7.23.25 +Mon, 19 Feb 2024 21:54:27 GMT + +_Version update only_ + +## 7.23.24 +Sat, 17 Feb 2024 06:24:34 GMT + +### Patches + +- Fix broken link to API documentation + +## 7.23.23 +Thu, 08 Feb 2024 01:09:21 GMT + +_Version update only_ + +## 7.23.22 +Wed, 07 Feb 2024 01:11:18 GMT + +_Version update only_ + +## 7.23.21 +Mon, 05 Feb 2024 23:46:52 GMT + +_Version update only_ + +## 7.23.20 +Thu, 25 Jan 2024 01:09:30 GMT + +_Version update only_ + +## 7.23.19 +Tue, 23 Jan 2024 20:12:57 GMT + +_Version update only_ + +## 7.23.18 +Tue, 23 Jan 2024 16:15:05 GMT + +_Version update only_ + +## 7.23.17 +Tue, 16 Jan 2024 18:30:11 GMT + +_Version update only_ + +## 7.23.16 +Wed, 03 Jan 2024 00:31:18 GMT + +_Version update only_ + +## 7.23.15 +Wed, 20 Dec 2023 01:09:45 GMT + +_Version update only_ + +## 7.23.14 +Thu, 07 Dec 2023 03:44:13 GMT + +_Version update only_ + +## 7.23.13 +Tue, 05 Dec 2023 01:10:16 GMT + +_Version update only_ + +## 7.23.12 +Fri, 10 Nov 2023 18:02:04 GMT + +_Version update only_ + +## 7.23.11 +Wed, 01 Nov 2023 23:11:35 GMT + +### Patches + +- Fix line endings in published package. + +## 7.23.10 +Mon, 30 Oct 2023 23:36:37 GMT + +_Version update only_ + +## 7.23.9 +Sun, 01 Oct 2023 02:56:29 GMT + +_Version update only_ + +## 7.23.8 +Sat, 30 Sep 2023 00:20:51 GMT + +### Patches + +- Add notes for @alpha items when encountered. Mimics the existing behavior for @beta items. + +## 7.23.7 +Thu, 28 Sep 2023 20:53:17 GMT + +_Version update only_ + +## 7.23.6 +Wed, 27 Sep 2023 00:21:38 GMT + +_Version update only_ + +## 7.23.5 +Tue, 26 Sep 2023 21:02:30 GMT + +_Version update only_ + +## 7.23.4 +Tue, 26 Sep 2023 09:30:33 GMT + +### Patches + +- Update type-only imports to include the type modifier. + +## 7.23.3 +Mon, 25 Sep 2023 23:38:28 GMT + +_Version update only_ + +## 7.23.2 +Fri, 22 Sep 2023 00:05:50 GMT + +_Version update only_ + +## 7.23.1 +Tue, 19 Sep 2023 15:21:51 GMT + +_Version update only_ + +## 7.23.0 +Fri, 15 Sep 2023 00:36:58 GMT + +### Minor changes + +- Update @types/node from 14 to 18 + +## 7.22.33 +Tue, 08 Aug 2023 07:10:39 GMT + +_Version update only_ + +## 7.22.32 +Mon, 31 Jul 2023 15:19:05 GMT + +_Version update only_ + +## 7.22.31 +Sat, 29 Jul 2023 00:22:50 GMT + +_Version update only_ + +## 7.22.30 +Thu, 20 Jul 2023 20:47:28 GMT + +_Version update only_ + +## 7.22.29 +Wed, 19 Jul 2023 00:20:31 GMT + +_Version update only_ + +## 7.22.28 +Fri, 14 Jul 2023 15:20:45 GMT + +_Version update only_ + +## 7.22.27 +Thu, 13 Jul 2023 00:22:37 GMT + +_Version update only_ + +## 7.22.26 +Wed, 12 Jul 2023 15:20:39 GMT + +_Version update only_ + +## 7.22.25 +Wed, 12 Jul 2023 00:23:29 GMT + +_Version update only_ + +## 7.22.24 +Fri, 07 Jul 2023 00:19:32 GMT + +_Version update only_ + +## 7.22.23 +Thu, 06 Jul 2023 00:16:19 GMT + +_Version update only_ + +## 7.22.22 +Tue, 04 Jul 2023 00:18:47 GMT + +_Version update only_ + +## 7.22.21 +Mon, 19 Jun 2023 22:40:21 GMT + +_Version update only_ + +## 7.22.20 +Thu, 15 Jun 2023 00:21:01 GMT + +_Version update only_ + +## 7.22.19 +Wed, 14 Jun 2023 00:19:42 GMT + +_Version update only_ + +## 7.22.18 +Tue, 13 Jun 2023 15:17:20 GMT + +_Version update only_ + +## 7.22.17 +Tue, 13 Jun 2023 01:49:01 GMT + +_Version update only_ + +## 7.22.16 +Fri, 09 Jun 2023 18:05:34 GMT + +_Version update only_ + +## 7.22.15 +Fri, 09 Jun 2023 15:23:15 GMT + +_Version update only_ + +## 7.22.14 +Fri, 09 Jun 2023 00:19:49 GMT + +_Version update only_ + +## 7.22.13 +Thu, 08 Jun 2023 15:21:17 GMT + +_Version update only_ + +## 7.22.12 +Thu, 08 Jun 2023 00:20:02 GMT + +_Version update only_ + +## 7.22.11 +Wed, 07 Jun 2023 22:45:16 GMT + +_Version update only_ + +## 7.22.10 +Tue, 06 Jun 2023 02:52:51 GMT + +_Version update only_ + +## 7.22.9 +Mon, 05 Jun 2023 21:45:21 GMT + +_Version update only_ ## 7.22.8 Fri, 02 Jun 2023 02:01:12 GMT diff --git a/apps/api-documenter/README.md b/apps/api-documenter/README.md index a3109a3cb00..ad3ecfd0e25 100644 --- a/apps/api-documenter/README.md +++ b/apps/api-documenter/README.md @@ -14,6 +14,6 @@ documentation. - [CHANGELOG.md]( https://github.com/microsoft/rushstack/blob/main/apps/api-documenter/CHANGELOG.md) - Find out what's new in the latest version -- [API Reference](https://rushstack.io/pages/api/api-documenter/) +- [API Reference](https://api.rushstack.io/pages/api-documenter/) API Documenter is part of the [Rush Stack](https://rushstack.io/) family of projects. diff --git a/apps/api-documenter/bin/api-documenter b/apps/api-documenter/bin/api-documenter index 783bb806fce..aee68e80224 100755 --- a/apps/api-documenter/bin/api-documenter +++ b/apps/api-documenter/bin/api-documenter @@ -1,2 +1,2 @@ #!/usr/bin/env node -require('../lib/start.js') +require('../lib/start.js'); diff --git a/apps/api-documenter/config/heft.json b/apps/api-documenter/config/heft.json new file mode 100644 index 00000000000..b3046cad172 --- /dev/null +++ b/apps/api-documenter/config/heft.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + "extends": "local-node-rig/profiles/default/config/heft.json", + + "phasesByName": { + "build": { + "tasksByName": { + "copy-json-schemas": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft", + "pluginName": "copy-files-plugin", + "options": { + "copyOperations": [ + { + "sourcePath": "src/schemas", + "destinationFolders": ["temp/json-schemas/api-extractor/v7"], + "fileExtensions": [".schema.json"], + "hardlink": true + } + ] + } + } + } + } + } + } +} diff --git a/apps/api-documenter/config/jest.config.json b/apps/api-documenter/config/jest.config.json index 4bb17bde3ee..d1749681d90 100644 --- a/apps/api-documenter/config/jest.config.json +++ b/apps/api-documenter/config/jest.config.json @@ -1,3 +1,3 @@ { - "extends": "@rushstack/heft-node-rig/profiles/default/config/jest.config.json" + "extends": "local-node-rig/profiles/default/config/jest.config.json" } diff --git a/apps/api-documenter/config/rig.json b/apps/api-documenter/config/rig.json index 6ac88a96368..165ffb001f5 100644 --- a/apps/api-documenter/config/rig.json +++ b/apps/api-documenter/config/rig.json @@ -3,5 +3,5 @@ // Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@rushstack/heft-node-rig" + "rigPackageName": "local-node-rig" } diff --git a/apps/api-documenter/eslint.config.js b/apps/api-documenter/eslint.config.js new file mode 100644 index 00000000000..ceb5a1bee40 --- /dev/null +++ b/apps/api-documenter/eslint.config.js @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +const nodeTrustedToolProfile = require('local-node-rig/profiles/default/includes/eslint/flat/profile/node-trusted-tool'); +const friendlyLocalsMixin = require('local-node-rig/profiles/default/includes/eslint/flat/mixins/friendly-locals'); + +module.exports = [ + ...nodeTrustedToolProfile, + ...friendlyLocalsMixin, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + tsconfigRootDir: __dirname + } + }, + rules: { + 'no-console': 'off' + } + } +]; diff --git a/apps/api-documenter/package.json b/apps/api-documenter/package.json index a4bbca74bf3..798282b0b6f 100644 --- a/apps/api-documenter/package.json +++ b/apps/api-documenter/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/api-documenter", - "version": "7.22.8", + "version": "7.28.5", "description": "Read JSON files from api-extractor, generate documentation pages", "repository": { "type": "git", @@ -21,19 +21,18 @@ "typings": "dist/rollup.d.ts", "dependencies": { "@microsoft/api-extractor-model": "workspace:*", - "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc": "~0.16.0", "@rushstack/node-core-library": "workspace:*", + "@rushstack/terminal": "workspace:*", "@rushstack/ts-command-line": "workspace:*", - "colors": "~1.2.1", - "js-yaml": "~3.13.1", + "js-yaml": "~4.1.0", "resolve": "~1.22.1" }, "devDependencies": { - "@rushstack/eslint-config": "workspace:*", "@rushstack/heft": "workspace:*", - "@rushstack/heft-node-rig": "workspace:*", - "@types/js-yaml": "3.12.1", - "@types/node": "14.18.36", - "@types/resolve": "1.20.2" + "@types/js-yaml": "4.0.9", + "@types/resolve": "1.20.2", + "eslint": "~9.37.0", + "local-node-rig": "workspace:*" } } diff --git a/apps/api-documenter/src/cli/ApiDocumenterCommandLine.ts b/apps/api-documenter/src/cli/ApiDocumenterCommandLine.ts index b201d6456b6..cc6c6774111 100644 --- a/apps/api-documenter/src/cli/ApiDocumenterCommandLine.ts +++ b/apps/api-documenter/src/cli/ApiDocumenterCommandLine.ts @@ -2,6 +2,7 @@ // See LICENSE in the project root for license information. import { CommandLineParser } from '@rushstack/ts-command-line'; + import { MarkdownAction } from './MarkdownAction'; import { YamlAction } from './YamlAction'; import { GenerateAction } from './GenerateAction'; diff --git a/apps/api-documenter/src/cli/BaseAction.ts b/apps/api-documenter/src/cli/BaseAction.ts index d70a1ec16f3..fbd3f10a14f 100644 --- a/apps/api-documenter/src/cli/BaseAction.ts +++ b/apps/api-documenter/src/cli/BaseAction.ts @@ -1,23 +1,23 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; -import * as tsdoc from '@microsoft/tsdoc'; -import colors from 'colors/safe'; +import * as path from 'node:path'; +import type * as tsdoc from '@microsoft/tsdoc'; import { CommandLineAction, - CommandLineStringParameter, + type CommandLineStringParameter, type ICommandLineActionOptions } from '@rushstack/ts-command-line'; import { FileSystem } from '@rushstack/node-core-library'; import { ApiModel, - ApiItem, + type ApiItem, ApiItemContainerMixin, ApiDocumentedItem, - IResolveDeclarationReferenceResult + type IResolveDeclarationReferenceResult } from '@microsoft/api-extractor-model'; +import { Colorize } from '@rushstack/terminal'; export interface IBuildApiModelResult { apiModel: ApiModel; @@ -32,7 +32,6 @@ export abstract class BaseAction extends CommandLineAction { protected constructor(options: ICommandLineActionOptions) { super(options); - // override this._inputFolderParameter = this.defineStringParameter({ parameterLongName: '--input-folder', parameterShortName: '-i', @@ -94,7 +93,7 @@ export abstract class BaseAction extends CommandLineAction { if (result.errorMessage) { console.log( - colors.yellow( + Colorize.yellow( `Warning: Unresolved @inheritDoc tag for ${apiItem.displayName}: ` + result.errorMessage ) ); diff --git a/apps/api-documenter/src/cli/GenerateAction.ts b/apps/api-documenter/src/cli/GenerateAction.ts index 4ecb815333c..155b22320d1 100644 --- a/apps/api-documenter/src/cli/GenerateAction.ts +++ b/apps/api-documenter/src/cli/GenerateAction.ts @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; +import * as path from 'node:path'; -import { ApiDocumenterCommandLine } from './ApiDocumenterCommandLine'; +import { FileSystem } from '@rushstack/node-core-library'; + +import type { ApiDocumenterCommandLine } from './ApiDocumenterCommandLine'; import { BaseAction } from './BaseAction'; import { DocumenterConfig } from '../documenters/DocumenterConfig'; import { ExperimentalYamlDocumenter } from '../documenters/ExperimentalYamlDocumenter'; - -import { FileSystem } from '@rushstack/node-core-library'; import { MarkdownDocumenter } from '../documenters/MarkdownDocumenter'; export class GenerateAction extends BaseAction { @@ -22,8 +22,7 @@ export class GenerateAction extends BaseAction { }); } - protected async onExecute(): Promise { - // override + protected override async onExecuteAsync(): Promise { // Look for the config file under the current folder let configFilePath: string = path.join(process.cwd(), DocumenterConfig.FILENAME); diff --git a/apps/api-documenter/src/cli/MarkdownAction.ts b/apps/api-documenter/src/cli/MarkdownAction.ts index 01d6e30a220..6dba1d3ac6d 100644 --- a/apps/api-documenter/src/cli/MarkdownAction.ts +++ b/apps/api-documenter/src/cli/MarkdownAction.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { ApiDocumenterCommandLine } from './ApiDocumenterCommandLine'; +import type { ApiDocumenterCommandLine } from './ApiDocumenterCommandLine'; import { BaseAction } from './BaseAction'; import { MarkdownDocumenter } from '../documenters/MarkdownDocumenter'; @@ -16,8 +16,7 @@ export class MarkdownAction extends BaseAction { }); } - protected async onExecute(): Promise { - // override + protected override async onExecuteAsync(): Promise { const { apiModel, outputFolder } = this.buildApiModel(); const markdownDocumenter: MarkdownDocumenter = new MarkdownDocumenter({ diff --git a/apps/api-documenter/src/cli/YamlAction.ts b/apps/api-documenter/src/cli/YamlAction.ts index b980610711d..7ed4161fb2d 100644 --- a/apps/api-documenter/src/cli/YamlAction.ts +++ b/apps/api-documenter/src/cli/YamlAction.ts @@ -1,18 +1,20 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { CommandLineFlagParameter, CommandLineChoiceParameter } from '@rushstack/ts-command-line'; +import type { + CommandLineFlagParameter, + IRequiredCommandLineChoiceParameter +} from '@rushstack/ts-command-line'; -import { ApiDocumenterCommandLine } from './ApiDocumenterCommandLine'; +import type { ApiDocumenterCommandLine } from './ApiDocumenterCommandLine'; import { BaseAction } from './BaseAction'; - -import { YamlDocumenter } from '../documenters/YamlDocumenter'; +import { YamlDocumenter, type YamlFormat } from '../documenters/YamlDocumenter'; import { OfficeYamlDocumenter } from '../documenters/OfficeYamlDocumenter'; export class YamlAction extends BaseAction { private readonly _officeParameter: CommandLineFlagParameter; private readonly _newDocfxNamespacesParameter: CommandLineFlagParameter; - private readonly _yamlFormatParameter: CommandLineChoiceParameter; + private readonly _yamlFormatParameter: IRequiredCommandLineChoiceParameter; public constructor(parser: ApiDocumenterCommandLine) { super({ @@ -36,7 +38,7 @@ export class YamlAction extends BaseAction { ` adds them to the table of contents. This will also affect file layout as namespaced items will be nested` + ` under a directory for the namespace instead of just within the package.` }); - this._yamlFormatParameter = this.defineChoiceParameter({ + this._yamlFormatParameter = this.defineChoiceParameter({ parameterLongName: '--yaml-format', alternatives: ['udp', 'sdp'], defaultValue: 'sdp', @@ -47,8 +49,7 @@ export class YamlAction extends BaseAction { }); } - protected async onExecute(): Promise { - // override + protected override async onExecuteAsync(): Promise { const { apiModel, inputFolder, outputFolder } = this.buildApiModel(); const yamlDocumenter: YamlDocumenter = this._officeParameter.value diff --git a/apps/api-documenter/src/documenters/DocumenterConfig.ts b/apps/api-documenter/src/documenters/DocumenterConfig.ts index 18d13550c6e..3a0e32c3736 100644 --- a/apps/api-documenter/src/documenters/DocumenterConfig.ts +++ b/apps/api-documenter/src/documenters/DocumenterConfig.ts @@ -1,9 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; +import * as path from 'node:path'; + import { JsonSchema, JsonFile, NewlineKind } from '@rushstack/node-core-library'; -import { IConfigFile } from './IConfigFile'; + +import type { IConfigFile } from './IConfigFile'; +import apiDocumenterSchema from '../schemas/api-documenter.schema.json'; /** * Helper for loading the api-documenter.json file format. Later when the schema is more mature, @@ -23,9 +26,7 @@ export class DocumenterConfig { /** * The JSON Schema for API Documenter config file (api-documenter.schema.json). */ - public static readonly jsonSchema: JsonSchema = JsonSchema.fromFile( - path.join(__dirname, '..', 'schemas', 'api-documenter.schema.json') - ); + public static readonly jsonSchema: JsonSchema = JsonSchema.fromLoadedObject(apiDocumenterSchema); /** * The config file name "api-documenter.json". diff --git a/apps/api-documenter/src/documenters/ExperimentalYamlDocumenter.ts b/apps/api-documenter/src/documenters/ExperimentalYamlDocumenter.ts index af20c75f7c4..5f8769d3720 100644 --- a/apps/api-documenter/src/documenters/ExperimentalYamlDocumenter.ts +++ b/apps/api-documenter/src/documenters/ExperimentalYamlDocumenter.ts @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { DocComment, DocInlineTag } from '@microsoft/tsdoc'; -import { ApiModel, ApiItem, ApiItemKind, ApiDocumentedItem } from '@microsoft/api-extractor-model'; +import { type DocComment, DocInlineTag } from '@microsoft/tsdoc'; +import { type ApiModel, type ApiItem, ApiItemKind, ApiDocumentedItem } from '@microsoft/api-extractor-model'; -import { IConfigTableOfContents } from './IConfigFile'; -import { IYamlTocItem, IYamlTocFile } from '../yaml/IYamlTocFile'; +import type { IConfigTableOfContents } from './IConfigFile'; +import type { IYamlTocItem, IYamlTocFile } from '../yaml/IYamlTocFile'; import { YamlDocumenter } from './YamlDocumenter'; -import { DocumenterConfig } from './DocumenterConfig'; +import type { DocumenterConfig } from './DocumenterConfig'; /** * EXPERIMENTAL - This documenter is a prototype of a new config file driven mode of operation for diff --git a/apps/api-documenter/src/documenters/IConfigFile.ts b/apps/api-documenter/src/documenters/IConfigFile.ts index 3c60580fd4a..5f617a33276 100644 --- a/apps/api-documenter/src/documenters/IConfigFile.ts +++ b/apps/api-documenter/src/documenters/IConfigFile.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { IYamlTocFile } from '../yaml/IYamlTocFile'; +import type { IYamlTocFile } from '../yaml/IYamlTocFile'; /** * Typescript interface describing the config schema for toc.yml file format. diff --git a/apps/api-documenter/src/documenters/MarkdownDocumenter.ts b/apps/api-documenter/src/documenters/MarkdownDocumenter.ts index e2f3afdf7d9..0deb6ed6edf 100644 --- a/apps/api-documenter/src/documenters/MarkdownDocumenter.ts +++ b/apps/api-documenter/src/documenters/MarkdownDocumenter.ts @@ -1,28 +1,29 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; +import * as path from 'node:path'; + import { PackageName, FileSystem, NewlineKind } from '@rushstack/node-core-library'; import { DocSection, DocPlainText, DocLinkTag, - TSDocConfiguration, + type TSDocConfiguration, StringBuilder, DocNodeKind, DocParagraph, DocCodeSpan, DocFencedCode, StandardTags, - DocBlock, - DocComment, - DocNodeContainer + type DocBlock, + type DocComment, + type DocNodeContainer } from '@microsoft/tsdoc'; import { - ApiModel, - ApiItem, - ApiEnum, - ApiPackage, + type ApiModel, + type ApiItem, + type ApiEnum, + type ApiPackage, ApiItemKind, ApiReleaseTagMixin, ApiDocumentedItem, @@ -31,21 +32,21 @@ import { ApiStaticMixin, ApiPropertyItem, ApiInterface, - Excerpt, + type Excerpt, ApiAbstractMixin, ApiParameterListMixin, ApiReturnTypeMixin, ApiDeclaredItem, - ApiNamespace, + type ApiNamespace, ExcerptTokenKind, - IResolveDeclarationReferenceResult, + type IResolveDeclarationReferenceResult, ApiTypeAlias, - ExcerptToken, + type ExcerptToken, ApiOptionalMixin, ApiInitializerMixin, ApiProtectedMixin, ApiReadonlyMixin, - IFindApiItemsResult + type IFindApiItemsResult } from '@microsoft/api-extractor-model'; import { CustomDocNodes } from '../nodes/CustomDocNodeKind'; @@ -59,10 +60,10 @@ import { Utilities } from '../utils/Utilities'; import { CustomMarkdownEmitter } from '../markdown/CustomMarkdownEmitter'; import { PluginLoader } from '../plugin/PluginLoader'; import { - IMarkdownDocumenterFeatureOnBeforeWritePageArgs, + type IMarkdownDocumenterFeatureOnBeforeWritePageArgs, MarkdownDocumenterFeatureContext } from '../plugin/MarkdownDocumenterFeature'; -import { DocumenterConfig } from './DocumenterConfig'; +import type { DocumenterConfig } from './DocumenterConfig'; import { MarkdownDocumenterAccessor } from '../plugin/MarkdownDocumenterAccessor'; export interface IMarkdownDocumenterOptions { @@ -173,7 +174,9 @@ export class MarkdownDocumenter { } if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) { - if (apiItem.releaseTag === ReleaseTag.Beta) { + if (apiItem.releaseTag === ReleaseTag.Alpha) { + this._writeAlphaWarning(output); + } else if (apiItem.releaseTag === ReleaseTag.Beta) { this._writeBetaWarning(output); } } @@ -1000,10 +1003,13 @@ export class MarkdownDocumenter { const section: DocSection = new DocSection({ configuration }); if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) { - if (apiItem.releaseTag === ReleaseTag.Beta) { + if (apiItem.releaseTag === ReleaseTag.Alpha || apiItem.releaseTag === ReleaseTag.Beta) { section.appendNodesInParagraph([ new DocEmphasisSpan({ configuration, bold: true, italic: true }, [ - new DocPlainText({ configuration, text: '(BETA)' }) + new DocPlainText({ + configuration, + text: `(${apiItem.releaseTag === ReleaseTag.Alpha ? 'ALPHA' : 'BETA'})` + }) ]), new DocPlainText({ configuration, text: ' ' }) ]); @@ -1152,10 +1158,22 @@ export class MarkdownDocumenter { } } + private _writeAlphaWarning(output: DocSection): void { + const configuration: TSDocConfiguration = this._tsdocConfiguration; + const betaWarning: string = + 'This API is provided as an alpha preview for developers and may change' + + ' based on feedback that we receive. Do not use this API in a production environment.'; + output.appendNode( + new DocNoteBox({ configuration }, [ + new DocParagraph({ configuration }, [new DocPlainText({ configuration, text: betaWarning })]) + ]) + ); + } + private _writeBetaWarning(output: DocSection): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const betaWarning: string = - 'This API is provided as a preview for developers and may change' + + 'This API is provided as a beta preview for developers and may change' + ' based on feedback that we receive. Do not use this API in a production environment.'; output.appendNode( new DocNoteBox({ configuration }, [ diff --git a/apps/api-documenter/src/documenters/OfficeYamlDocumenter.ts b/apps/api-documenter/src/documenters/OfficeYamlDocumenter.ts index 19b9d58c843..b8b178a9ce1 100644 --- a/apps/api-documenter/src/documenters/OfficeYamlDocumenter.ts +++ b/apps/api-documenter/src/documenters/OfficeYamlDocumenter.ts @@ -1,15 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import colors from 'colors'; -import * as path from 'path'; +import * as path from 'node:path'; + import yaml = require('js-yaml'); -import { ApiModel } from '@microsoft/api-extractor-model'; +import type { ApiModel } from '@microsoft/api-extractor-model'; import { FileSystem } from '@rushstack/node-core-library'; +import { Colorize } from '@rushstack/terminal'; -import { IYamlTocItem } from '../yaml/IYamlTocFile'; -import { IYamlItem } from '../yaml/IYamlApiFile'; +import type { IYamlTocItem } from '../yaml/IYamlTocFile'; +import type { IYamlItem } from '../yaml/IYamlApiFile'; import { YamlDocumenter } from './YamlDocumenter'; interface ISnippetsFile { @@ -48,8 +49,8 @@ export class OfficeYamlDocumenter extends YamlDocumenter { console.log('Loading snippets from ' + snippetsFilePath); const snippetsContent: string = FileSystem.readFile(snippetsFilePath); - this._snippets = yaml.load(snippetsContent, { filename: snippetsFilePath }); - this._snippetsAll = yaml.load(snippetsContent, { filename: snippetsFilePath }); + this._snippets = yaml.load(snippetsContent, { filename: snippetsFilePath }) as ISnippetsFile; + this._snippetsAll = yaml.load(snippetsContent, { filename: snippetsFilePath }) as ISnippetsFile; } /** @override */ @@ -59,13 +60,12 @@ export class OfficeYamlDocumenter extends YamlDocumenter { // After we generate everything, check for any unused snippets console.log(); for (const apiName of Object.keys(this._snippets)) { - console.error(colors.yellow('Warning: Unused snippet ' + apiName)); + console.error(Colorize.yellow('Warning: Unused snippet ' + apiName)); } } /** @override */ protected onGetTocRoot(): IYamlTocItem { - // override return { name: 'API reference', href: 'overview.md', diff --git a/apps/api-documenter/src/documenters/YamlDocumenter.ts b/apps/api-documenter/src/documenters/YamlDocumenter.ts index 9f04aa959e0..3ca47d82954 100644 --- a/apps/api-documenter/src/documenters/YamlDocumenter.ts +++ b/apps/api-documenter/src/documenters/YamlDocumenter.ts @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; +import * as path from 'node:path'; import yaml = require('js-yaml'); + import { JsonFile, JsonSchema, @@ -12,39 +13,46 @@ import { NewlineKind, InternalError } from '@rushstack/node-core-library'; -import { StringBuilder, DocSection, DocComment, DocBlock, StandardTags } from '@microsoft/tsdoc'; import { - ApiModel, - ApiItem, + StringBuilder, + type DocSection, + type DocComment, + type DocBlock, + StandardTags +} from '@microsoft/tsdoc'; +import { + type ApiModel, + type ApiItem, ApiItemKind, ApiDocumentedItem, ApiReleaseTagMixin, ReleaseTag, - ApiPropertyItem, + type ApiPropertyItem, ApiItemContainerMixin, - ApiPackage, - ApiEnumMember, + type ApiPackage, + type ApiEnumMember, ApiClass, ApiInterface, - ApiMethod, - ApiMethodSignature, - ApiConstructor, - ApiFunction, + type ApiMethod, + type ApiMethodSignature, + type ApiConstructor, + type ApiFunction, ApiReturnTypeMixin, ApiTypeParameterListMixin, - Excerpt, - ExcerptToken, + type Excerpt, + type ExcerptToken, ExcerptTokenKind, - HeritageType, - ApiVariable, - ApiTypeAlias + type HeritageType, + type ApiVariable, + type ApiTypeAlias } from '@microsoft/api-extractor-model'; import { - DeclarationReference, + type DeclarationReference, Navigation, Meaning } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference'; -import { + +import type { IYamlApiFile, IYamlItem, IYamlSyntax, @@ -53,14 +61,13 @@ import { IYamlReferenceSpec, IYamlInheritanceTree } from '../yaml/IYamlApiFile'; -import { IYamlTocFile, IYamlTocItem } from '../yaml/IYamlTocFile'; +import type { IYamlTocFile, IYamlTocItem } from '../yaml/IYamlTocFile'; import { Utilities } from '../utils/Utilities'; import { CustomMarkdownEmitter } from '../markdown/CustomMarkdownEmitter'; import { convertUDPYamlToSDP } from '../utils/ToSdpConvertHelper'; +import typescriptSchema from '../yaml/typescript.schema.json'; -const yamlApiSchema: JsonSchema = JsonSchema.fromFile( - path.join(__dirname, '..', 'yaml', 'typescript.schema.json') -); +const yamlApiSchema: JsonSchema = JsonSchema.fromLoadedObject(typescriptSchema); interface IYamlReferences { references: IYamlReference[]; @@ -68,7 +75,7 @@ interface IYamlReferences { uidTypeReferenceCounters: Map; } -const enum FlattenMode { +enum FlattenMode { /** Include entries for nested namespaces and non-namespace children. */ NestedNamespacesAndChildren, /** Include entries for nested namespaces only. */ @@ -84,6 +91,8 @@ interface INameOptions { includeNamespace?: boolean; } +export type YamlFormat = 'udp' | 'sdp'; + /** * Writes documentation in the Universal Reference YAML file format, as defined by typescript.schema.json. */ @@ -96,7 +105,11 @@ export class YamlDocumenter { private _apiItemsByCanonicalReference: Map; private _yamlReferences: IYamlReferences | undefined; - public constructor(apiModel: ApiModel, newDocfxNamespaces: boolean = false, yamlFormat: string = 'sdp') { + public constructor( + apiModel: ApiModel, + newDocfxNamespaces: boolean = false, + yamlFormat: YamlFormat = 'sdp' + ) { this._apiModel = apiModel; this.newDocfxNamespaces = newDocfxNamespaces; this._yamlFormat = yamlFormat; @@ -425,7 +438,7 @@ export class YamlDocumenter { } if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) { - if (apiItem.releaseTag === ReleaseTag.Beta) { + if (apiItem.releaseTag === ReleaseTag.Alpha || apiItem.releaseTag === ReleaseTag.Beta) { yamlItem.isPreview = true; } } @@ -745,7 +758,7 @@ export class YamlDocumenter { ): void { JsonFile.validateNoUndefinedMembers(dataObject); - let stringified: string = yaml.safeDump(dataObject, { + let stringified: string = yaml.dump(dataObject, { lineWidth: 120 }); @@ -932,12 +945,12 @@ export class YamlDocumenter { spec.fullName = apiItem ? apiItem.getScopedNameWithinPackage() : token.canonicalReference - ? token.canonicalReference - .withSource(undefined) - .withMeaning(undefined) - .withOverloadIndex(undefined) - .toString() - : token.text; + ? token.canonicalReference + .withSource(undefined) + .withMeaning(undefined) + .withOverloadIndex(undefined) + .toString() + : token.text; specs.push(spec); } else { specs.push({ diff --git a/apps/api-documenter/src/index.ts b/apps/api-documenter/src/index.ts index 90eb399d131..fe1f9a6a206 100644 --- a/apps/api-documenter/src/index.ts +++ b/apps/api-documenter/src/index.ts @@ -9,12 +9,12 @@ * @packageDocumentation */ -export { IFeatureDefinition, IApiDocumenterPluginManifest } from './plugin/IApiDocumenterPluginManifest'; +export type { IFeatureDefinition, IApiDocumenterPluginManifest } from './plugin/IApiDocumenterPluginManifest'; export { MarkdownDocumenterAccessor } from './plugin/MarkdownDocumenterAccessor'; export { MarkdownDocumenterFeatureContext, - IMarkdownDocumenterFeatureOnBeforeWritePageArgs, - IMarkdownDocumenterFeatureOnFinishedArgs, + type IMarkdownDocumenterFeatureOnBeforeWritePageArgs, + type IMarkdownDocumenterFeatureOnFinishedArgs, MarkdownDocumenterFeature } from './plugin/MarkdownDocumenterFeature'; export { PluginFeature, PluginFeatureContext, PluginFeatureInitialization } from './plugin/PluginFeature'; diff --git a/apps/api-documenter/src/markdown/CustomMarkdownEmitter.ts b/apps/api-documenter/src/markdown/CustomMarkdownEmitter.ts index edec714d34a..5ec5bc9e90f 100644 --- a/apps/api-documenter/src/markdown/CustomMarkdownEmitter.ts +++ b/apps/api-documenter/src/markdown/CustomMarkdownEmitter.ts @@ -1,19 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import colors from 'colors'; - -import { DocNode, DocLinkTag, StringBuilder } from '@microsoft/tsdoc'; -import { ApiModel, IResolveDeclarationReferenceResult, ApiItem } from '@microsoft/api-extractor-model'; +import type { DocNode, DocLinkTag, StringBuilder } from '@microsoft/tsdoc'; +import type { ApiModel, IResolveDeclarationReferenceResult, ApiItem } from '@microsoft/api-extractor-model'; +import { Colorize } from '@rushstack/terminal'; import { CustomDocNodeKind } from '../nodes/CustomDocNodeKind'; -import { DocHeading } from '../nodes/DocHeading'; -import { DocNoteBox } from '../nodes/DocNoteBox'; -import { DocTable } from '../nodes/DocTable'; -import { DocTableCell } from '../nodes/DocTableCell'; -import { DocEmphasisSpan } from '../nodes/DocEmphasisSpan'; -import { MarkdownEmitter, IMarkdownEmitterContext, IMarkdownEmitterOptions } from './MarkdownEmitter'; -import { IndentedWriter } from '../utils/IndentedWriter'; +import type { DocHeading } from '../nodes/DocHeading'; +import type { DocNoteBox } from '../nodes/DocNoteBox'; +import type { DocTable } from '../nodes/DocTable'; +import type { DocTableCell } from '../nodes/DocTableCell'; +import type { DocEmphasisSpan } from '../nodes/DocEmphasisSpan'; +import { + MarkdownEmitter, + type IMarkdownEmitterContext, + type IMarkdownEmitterOptions +} from './MarkdownEmitter'; +import type { IndentedWriter } from '../utils/IndentedWriter'; export interface ICustomMarkdownEmitterOptions extends IMarkdownEmitterOptions { contextApiItem: ApiItem | undefined; @@ -86,8 +89,6 @@ export class CustomMarkdownEmitter extends MarkdownEmitter { // whereas VS Code's renderer is totally fine with it. writer.ensureSkippedLine(); - context.insideTable = true; - // Markdown table rows can have inconsistent cell counts. Size the table based on the longest row. let columnCount: number = 0; if (docTable.header) { @@ -99,39 +100,43 @@ export class CustomMarkdownEmitter extends MarkdownEmitter { } } - // write the table header (which is required by Markdown) - writer.write('| '); - for (let i: number = 0; i < columnCount; ++i) { - writer.write(' '); - if (docTable.header) { + writer.write(''); + if (docTable.header) { + writer.write(''); + for (let i: number = 0; i < columnCount; ++i) { + writer.write(''); } - writer.write(' |'); - } - writer.writeLine(); - - // write the divider - writer.write('| '); - for (let i: number = 0; i < columnCount; ++i) { - writer.write(' --- |'); + writer.write(''); } writer.writeLine(); + writer.write(''); for (const row of docTable.rows) { - writer.write('| '); + writer.write(''); for (const cell of row.cells) { - writer.write(' '); + writer.write(''); } + writer.write(''); writer.writeLine(); } - writer.writeLine(); - - context.insideTable = false; + writer.write(''); + writer.write('
'); + writer.ensureNewLine(); + writer.writeLine(); const cell: DocTableCell | undefined = docTable.header.cells[i]; if (cell) { this.writeNode(cell.content, context, false); } + writer.ensureNewLine(); + writer.writeLine(); + writer.write('
'); + writer.ensureNewLine(); + writer.writeLine(); this.writeNode(cell.content, context, false); - writer.write(' |'); + writer.ensureNewLine(); + writer.writeLine(); + writer.write('
'); + writer.ensureSkippedLine(); break; } @@ -179,12 +184,12 @@ export class CustomMarkdownEmitter extends MarkdownEmitter { context.writer.write(encodedLinkText); context.writer.write(`](${filename!})`); } else { - console.log(colors.yellow('WARNING: Unable to determine link text')); + console.log(Colorize.yellow('WARNING: Unable to determine link text')); } } } else if (result.errorMessage) { console.log( - colors.yellow( + Colorize.yellow( `WARNING: Unable to resolve reference "${docLinkTag.codeDestination!.emitAsTsdoc()}": ` + result.errorMessage ) diff --git a/apps/api-documenter/src/markdown/MarkdownEmitter.ts b/apps/api-documenter/src/markdown/MarkdownEmitter.ts index 30946b9cb9a..663eee17f1e 100644 --- a/apps/api-documenter/src/markdown/MarkdownEmitter.ts +++ b/apps/api-documenter/src/markdown/MarkdownEmitter.ts @@ -2,21 +2,21 @@ // See LICENSE in the project root for license information. import { - DocNode, + type DocNode, DocNodeKind, - StringBuilder, - DocPlainText, - DocHtmlStartTag, - DocHtmlEndTag, - DocCodeSpan, - DocLinkTag, - DocParagraph, - DocFencedCode, - DocSection, + type StringBuilder, + type DocPlainText, + type DocHtmlStartTag, + type DocHtmlEndTag, + type DocCodeSpan, + type DocLinkTag, + type DocParagraph, + type DocFencedCode, + type DocSection, DocNodeTransforms, - DocEscapedText, - DocErrorText, - DocBlockTag + type DocEscapedText, + type DocErrorText, + type DocBlockTag } from '@microsoft/tsdoc'; import { InternalError } from '@rushstack/node-core-library'; @@ -26,7 +26,6 @@ export interface IMarkdownEmitterOptions {} export interface IMarkdownEmitterContext { writer: IndentedWriter; - insideTable: boolean; boldRequested: boolean; italicRequested: boolean; @@ -47,7 +46,6 @@ export class MarkdownEmitter { const context: IMarkdownEmitterContext = { writer, - insideTable: false, boldRequested: false, italicRequested: false, @@ -106,23 +104,9 @@ export class MarkdownEmitter { } case DocNodeKind.CodeSpan: { const docCodeSpan: DocCodeSpan = docNode as DocCodeSpan; - if (context.insideTable) { - writer.write(''); - } else { - writer.write('`'); - } - if (context.insideTable) { - const code: string = this.getTableEscapedText(docCodeSpan.code); - const parts: string[] = code.split(/\r?\n/g); - writer.write(parts.join('
')); - } else { - writer.write(docCodeSpan.code); - } - if (context.insideTable) { - writer.write(''); - } else { - writer.write('`'); - } + writer.write('`'); + writer.write(docCodeSpan.code); + writer.write('`'); break; } case DocNodeKind.LinkTag: { @@ -139,24 +123,10 @@ export class MarkdownEmitter { case DocNodeKind.Paragraph: { const docParagraph: DocParagraph = docNode as DocParagraph; const trimmedParagraph: DocParagraph = DocNodeTransforms.trimSpacesInParagraph(docParagraph); - if (context.insideTable) { - if (docNodeSiblings) { - // This tentative write is necessary to avoid writing empty paragraph tags (i.e. `

`). At the - // time this code runs, we do not know whether the `writeNodes` call below will actually write - // anything. Thus, we want to only write a `

` tag (as well as eventually a corresponding - // `

` tag) if something ends up being written within the tags. - writer.writeTentative('

', '

', () => { - this.writeNodes(trimmedParagraph.nodes, context); - }); - } else { - // Special case: If we are the only element inside this table cell, then we can omit the

container. - this.writeNodes(trimmedParagraph.nodes, context); - } - } else { - this.writeNodes(trimmedParagraph.nodes, context); - writer.ensureNewLine(); - writer.writeLine(); - } + + this.writeNodes(trimmedParagraph.nodes, context); + writer.ensureNewLine(); + writer.writeLine(); break; } case DocNodeKind.FencedCode: { diff --git a/apps/api-documenter/src/markdown/test/CustomMarkdownEmitter.test.ts b/apps/api-documenter/src/markdown/test/CustomMarkdownEmitter.test.ts index f892d53a36c..5e545c8d39b 100644 --- a/apps/api-documenter/src/markdown/test/CustomMarkdownEmitter.test.ts +++ b/apps/api-documenter/src/markdown/test/CustomMarkdownEmitter.test.ts @@ -3,7 +3,7 @@ import { DocSection, - TSDocConfiguration, + type TSDocConfiguration, DocPlainText, StringBuilder, DocParagraph, @@ -21,7 +21,7 @@ import { DocTable } from '../../nodes/DocTable'; import { DocTableRow } from '../../nodes/DocTableRow'; import { DocTableCell } from '../../nodes/DocTableCell'; import { CustomMarkdownEmitter } from '../CustomMarkdownEmitter'; -import { ApiModel, ApiItem } from '@microsoft/api-extractor-model'; +import { ApiModel, type ApiItem } from '@microsoft/api-extractor-model'; test('render Markdown from TSDoc', () => { const configuration: TSDocConfiguration = CustomDocNodes.configuration; @@ -171,6 +171,13 @@ test('render Markdown from TSDoc', () => { ) ]); + output.appendNodes([ + new DocHeading({ configuration, title: 'After a table' }), + new DocParagraph({ configuration }, [ + new DocPlainText({ configuration, text: 'just checking lines after a table' }) + ]) + ]); + const stringBuilder: StringBuilder = new StringBuilder(); const apiModel: ApiModel = new ApiModel(); const markdownEmitter: CustomMarkdownEmitter = new CustomMarkdownEmitter(apiModel); diff --git a/apps/api-documenter/src/markdown/test/__snapshots__/CustomMarkdownEmitter.test.ts.snap b/apps/api-documenter/src/markdown/test/__snapshots__/CustomMarkdownEmitter.test.ts.snap index 76263562553..2616cbebe24 100644 --- a/apps/api-documenter/src/markdown/test/__snapshots__/CustomMarkdownEmitter.test.ts.snap +++ b/apps/api-documenter/src/markdown/test/__snapshots__/CustomMarkdownEmitter.test.ts.snap @@ -50,9 +50,35 @@ HTML escape: &quot; ## Table -| Header 1 | Header 2 | -| --- | --- | -| Cell 1 |

Cell 2

**bold text**

| + + +
+ +Header 1 + + + + +Header 2 + + +
+ +Cell 1 + + + + +Cell 2 + +**bold text** + + +
+ +## After a table + +just checking lines after a table " `; diff --git a/apps/api-documenter/src/nodes/CustomDocNodeKind.ts b/apps/api-documenter/src/nodes/CustomDocNodeKind.ts index bb2f6731584..9d0af5fdab8 100644 --- a/apps/api-documenter/src/nodes/CustomDocNodeKind.ts +++ b/apps/api-documenter/src/nodes/CustomDocNodeKind.ts @@ -1,4 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + import { TSDocConfiguration, DocNodeKind } from '@microsoft/tsdoc'; + import { DocEmphasisSpan } from './DocEmphasisSpan'; import { DocHeading } from './DocHeading'; import { DocNoteBox } from './DocNoteBox'; @@ -6,13 +10,10 @@ import { DocTable } from './DocTable'; import { DocTableCell } from './DocTableCell'; import { DocTableRow } from './DocTableRow'; -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - /** * Identifies custom subclasses of {@link DocNode}. */ -export const enum CustomDocNodeKind { +export enum CustomDocNodeKind { EmphasisSpan = 'EmphasisSpan', Heading = 'Heading', NoteBox = 'NoteBox', diff --git a/apps/api-documenter/src/nodes/DocEmphasisSpan.ts b/apps/api-documenter/src/nodes/DocEmphasisSpan.ts index 0f1f543b877..bc2197689fc 100644 --- a/apps/api-documenter/src/nodes/DocEmphasisSpan.ts +++ b/apps/api-documenter/src/nodes/DocEmphasisSpan.ts @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { DocNode, DocNodeContainer, IDocNodeContainerParameters } from '@microsoft/tsdoc'; +import { type DocNode, DocNodeContainer, type IDocNodeContainerParameters } from '@microsoft/tsdoc'; + import { CustomDocNodeKind } from './CustomDocNodeKind'; /** diff --git a/apps/api-documenter/src/nodes/DocHeading.ts b/apps/api-documenter/src/nodes/DocHeading.ts index d5d48227667..ee40a8bb6bc 100644 --- a/apps/api-documenter/src/nodes/DocHeading.ts +++ b/apps/api-documenter/src/nodes/DocHeading.ts @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { IDocNodeParameters, DocNode } from '@microsoft/tsdoc'; +import { type IDocNodeParameters, DocNode } from '@microsoft/tsdoc'; + import { CustomDocNodeKind } from './CustomDocNodeKind'; /** diff --git a/apps/api-documenter/src/nodes/DocNoteBox.ts b/apps/api-documenter/src/nodes/DocNoteBox.ts index 85372fb5a29..cccece1460c 100644 --- a/apps/api-documenter/src/nodes/DocNoteBox.ts +++ b/apps/api-documenter/src/nodes/DocNoteBox.ts @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { IDocNodeParameters, DocNode, DocSection } from '@microsoft/tsdoc'; +import { type IDocNodeParameters, DocNode, DocSection } from '@microsoft/tsdoc'; + import { CustomDocNodeKind } from './CustomDocNodeKind'; /** diff --git a/apps/api-documenter/src/nodes/DocTable.ts b/apps/api-documenter/src/nodes/DocTable.ts index 095e39e3b1f..945098a4adb 100644 --- a/apps/api-documenter/src/nodes/DocTable.ts +++ b/apps/api-documenter/src/nodes/DocTable.ts @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { IDocNodeParameters, DocNode } from '@microsoft/tsdoc'; +import { type IDocNodeParameters, DocNode } from '@microsoft/tsdoc'; + import { CustomDocNodeKind } from './CustomDocNodeKind'; import { DocTableRow } from './DocTableRow'; -import { DocTableCell } from './DocTableCell'; +import type { DocTableCell } from './DocTableCell'; /** * Constructor parameters for {@link DocTable}. diff --git a/apps/api-documenter/src/nodes/DocTableCell.ts b/apps/api-documenter/src/nodes/DocTableCell.ts index 8a56c13a836..f0911dbfd59 100644 --- a/apps/api-documenter/src/nodes/DocTableCell.ts +++ b/apps/api-documenter/src/nodes/DocTableCell.ts @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { IDocNodeParameters, DocNode, DocSection } from '@microsoft/tsdoc'; +import { type IDocNodeParameters, DocNode, DocSection } from '@microsoft/tsdoc'; + import { CustomDocNodeKind } from './CustomDocNodeKind'; /** diff --git a/apps/api-documenter/src/nodes/DocTableRow.ts b/apps/api-documenter/src/nodes/DocTableRow.ts index d0d949a7c9b..85bd873d907 100644 --- a/apps/api-documenter/src/nodes/DocTableRow.ts +++ b/apps/api-documenter/src/nodes/DocTableRow.ts @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { IDocNodeParameters, DocNode, DocPlainText } from '@microsoft/tsdoc'; +import { type IDocNodeParameters, DocNode, DocPlainText } from '@microsoft/tsdoc'; + import { CustomDocNodeKind } from './CustomDocNodeKind'; import { DocTableCell } from './DocTableCell'; diff --git a/apps/api-documenter/src/plugin/IApiDocumenterPluginManifest.ts b/apps/api-documenter/src/plugin/IApiDocumenterPluginManifest.ts index 1e047bff133..c01e7c195e9 100644 --- a/apps/api-documenter/src/plugin/IApiDocumenterPluginManifest.ts +++ b/apps/api-documenter/src/plugin/IApiDocumenterPluginManifest.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { MarkdownDocumenterFeature } from './MarkdownDocumenterFeature'; -import { PluginFeatureInitialization } from './PluginFeature'; +import type { MarkdownDocumenterFeature } from './MarkdownDocumenterFeature'; +import type { PluginFeatureInitialization } from './PluginFeature'; /** * Defines a "feature" that is provided by an API Documenter plugin. A feature is a user-defined module diff --git a/apps/api-documenter/src/plugin/MarkdownDocumenterAccessor.ts b/apps/api-documenter/src/plugin/MarkdownDocumenterAccessor.ts index 41a57a8e6df..8f6c176e094 100644 --- a/apps/api-documenter/src/plugin/MarkdownDocumenterAccessor.ts +++ b/apps/api-documenter/src/plugin/MarkdownDocumenterAccessor.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { ApiItem } from '@microsoft/api-extractor-model'; +import type { ApiItem } from '@microsoft/api-extractor-model'; /** @internal */ export interface IMarkdownDocumenterAccessorImplementation { diff --git a/apps/api-documenter/src/plugin/MarkdownDocumenterFeature.ts b/apps/api-documenter/src/plugin/MarkdownDocumenterFeature.ts index 8a15c0f1435..0666b8097b3 100644 --- a/apps/api-documenter/src/plugin/MarkdownDocumenterFeature.ts +++ b/apps/api-documenter/src/plugin/MarkdownDocumenterFeature.ts @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { ApiItem, ApiModel } from '@microsoft/api-extractor-model'; +import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model'; import { TypeUuid } from '@rushstack/node-core-library'; + import { PluginFeature } from './PluginFeature'; -import { MarkdownDocumenterAccessor } from './MarkdownDocumenterAccessor'; +import type { MarkdownDocumenterAccessor } from './MarkdownDocumenterAccessor'; /** * Context object for {@link MarkdownDocumenterFeature}. diff --git a/apps/api-documenter/src/plugin/PluginLoader.ts b/apps/api-documenter/src/plugin/PluginLoader.ts index dbf6caba709..6d926f6ea66 100644 --- a/apps/api-documenter/src/plugin/PluginLoader.ts +++ b/apps/api-documenter/src/plugin/PluginLoader.ts @@ -1,13 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; +import * as path from 'node:path'; + import * as resolve from 'resolve'; -import { IApiDocumenterPluginManifest, IFeatureDefinition } from './IApiDocumenterPluginManifest'; -import { MarkdownDocumenterFeature, MarkdownDocumenterFeatureContext } from './MarkdownDocumenterFeature'; +import type { IApiDocumenterPluginManifest, IFeatureDefinition } from './IApiDocumenterPluginManifest'; +import { + MarkdownDocumenterFeature, + type MarkdownDocumenterFeatureContext +} from './MarkdownDocumenterFeature'; import { PluginFeatureInitialization } from './PluginFeature'; -import { DocumenterConfig } from '../documenters/DocumenterConfig'; +import type { DocumenterConfig } from '../documenters/DocumenterConfig'; interface ILoadedPlugin { packageName: string; diff --git a/apps/api-documenter/src/start.ts b/apps/api-documenter/src/start.ts index 592e84d2ed6..e6266aa99fa 100644 --- a/apps/api-documenter/src/start.ts +++ b/apps/api-documenter/src/start.ts @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as os from 'os'; -import colors from 'colors'; +import * as os from 'node:os'; import { PackageJsonLookup } from '@rushstack/node-core-library'; +import { Colorize } from '@rushstack/terminal'; import { ApiDocumenterCommandLine } from './cli/ApiDocumenterCommandLine'; @@ -12,9 +12,11 @@ const myPackageVersion: string = PackageJsonLookup.loadOwnPackageJson(__dirname) console.log( os.EOL + - colors.bold(`api-documenter ${myPackageVersion} ` + colors.cyan(' - https://api-extractor.com/') + os.EOL) + Colorize.bold( + `api-documenter ${myPackageVersion} ` + Colorize.cyan(' - https://api-extractor.com/') + os.EOL + ) ); const parser: ApiDocumenterCommandLine = new ApiDocumenterCommandLine(); -parser.execute().catch(console.error); // CommandLineParser.execute() should never reject the promise +parser.executeAsync().catch(console.error); // CommandLineParser.executeAsync() should never reject the promise diff --git a/apps/api-documenter/src/utils/IndentedWriter.ts b/apps/api-documenter/src/utils/IndentedWriter.ts index cd5d1a31020..d751ac1682e 100644 --- a/apps/api-documenter/src/utils/IndentedWriter.ts +++ b/apps/api-documenter/src/utils/IndentedWriter.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { StringBuilder, IStringBuilder } from '@rushstack/node-core-library'; +import { StringBuilder, type IStringBuilder } from '@rushstack/node-core-library'; /** * A utility for writing indented text. diff --git a/apps/api-documenter/src/utils/ToSdpConvertHelper.ts b/apps/api-documenter/src/utils/ToSdpConvertHelper.ts index 392ad7b2e1f..8d12f9df7f8 100644 --- a/apps/api-documenter/src/utils/ToSdpConvertHelper.ts +++ b/apps/api-documenter/src/utils/ToSdpConvertHelper.ts @@ -1,11 +1,20 @@ -import { +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import path from 'node:path'; + +import yaml = require('js-yaml'); + +import { FileSystem, Encoding, NewlineKind } from '@rushstack/node-core-library'; + +import type { IYamlItem, IYamlApiFile, IYamlSyntax, IYamlReferenceSpec, IYamlReference } from '../yaml/IYamlApiFile'; -import { +import type { PackageYamlModel, EnumYamlModel, TypeAliasYamlModel, @@ -14,9 +23,6 @@ import { FunctionYamlModel, CommonYamlModel } from '../yaml/ISDPYamlFile'; -import path from 'path'; -import { FileSystem, Encoding, NewlineKind } from '@rushstack/node-core-library'; -import yaml = require('js-yaml'); export function convertUDPYamlToSDP(folderPath: string): void { convert(folderPath, folderPath); @@ -46,10 +52,10 @@ function convert(inputPath: string, outputPath: string): void { console.log(`convert file ${fpath} from udp to sdp`); - const file: IYamlApiFile = yaml.safeLoad(yamlContent) as IYamlApiFile; + const file: IYamlApiFile = yaml.load(yamlContent) as IYamlApiFile; const result: { model: CommonYamlModel; type: string } | undefined = convertToSDP(file); if (result && result.model) { - const stringified: string = `### YamlMime:TS${result.type}\n${yaml.safeDump(result.model, { + const stringified: string = `### YamlMime:TS${result.type}\n${yaml.dump(result.model, { lineWidth: 120 })}`; FileSystem.writeFile(`${outputPath}/${name}`, stringified, { diff --git a/apps/api-documenter/src/utils/Utilities.ts b/apps/api-documenter/src/utils/Utilities.ts index 0416dc7fa86..2c9bc2556a6 100644 --- a/apps/api-documenter/src/utils/Utilities.ts +++ b/apps/api-documenter/src/utils/Utilities.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { ApiParameterListMixin, ApiItem } from '@microsoft/api-extractor-model'; +import { ApiParameterListMixin, type ApiItem } from '@microsoft/api-extractor-model'; export class Utilities { private static readonly _badFilenameCharsRegExp: RegExp = /[^a-z0-9_\-\.]/gi; diff --git a/apps/api-documenter/src/yaml/ISDPYamlFile.ts b/apps/api-documenter/src/yaml/ISDPYamlFile.ts index 250e9c94949..413d73e8d6b 100644 --- a/apps/api-documenter/src/yaml/ISDPYamlFile.ts +++ b/apps/api-documenter/src/yaml/ISDPYamlFile.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + interface IBaseYamlModel { uid: string; name: string; diff --git a/apps/api-documenter/tsconfig.json b/apps/api-documenter/tsconfig.json index a114c3448ed..dac21d04081 100644 --- a/apps/api-documenter/tsconfig.json +++ b/apps/api-documenter/tsconfig.json @@ -1,3 +1,3 @@ { - "extends": "./node_modules/@rushstack/heft-node-rig/profiles/default/tsconfig-base.json" + "extends": "./node_modules/local-node-rig/profiles/default/tsconfig-base.json" } diff --git a/apps/api-extractor/.eslintrc.js b/apps/api-extractor/.eslintrc.js deleted file mode 100644 index 4c934799d67..00000000000 --- a/apps/api-extractor/.eslintrc.js +++ /dev/null @@ -1,10 +0,0 @@ -// This is a workaround for https://github.com/eslint/eslint/issues/3458 -require('@rushstack/eslint-config/patch/modern-module-resolution'); - -module.exports = { - extends: [ - '@rushstack/eslint-config/profile/node-trusted-tool', - '@rushstack/eslint-config/mixins/friendly-locals' - ], - parserOptions: { tsconfigRootDir: __dirname } -}; diff --git a/apps/api-extractor/.npmignore b/apps/api-extractor/.npmignore index 7a29489cbcc..8b61d7caa66 100644 --- a/apps/api-extractor/.npmignore +++ b/apps/api-extractor/.npmignore @@ -8,6 +8,11 @@ !/lib/** !/lib-*/** !/dist/** + +!CHANGELOG.md +!CHANGELOG.json +!heft-plugin.json +!rush-plugin-manifest.json !ThirdPartyNotice.txt # Ignore certain patterns that should not get published. @@ -19,14 +24,10 @@ # NOTE: These don't need to be specified, because NPM includes them automatically. # # package.json -# README (and its variants) -# CHANGELOG (and its variants) -# LICENSE / LICENCE - -#-------------------------------------------- -# DO NOT MODIFY THE TEMPLATE ABOVE THIS LINE -#-------------------------------------------- - -# (Add your project-specific overrides here) +# README.md +# LICENSE +# --------------------------------------------------------------------------- +# DO NOT MODIFY ABOVE THIS LINE! Add any project-specific overrides below. +# --------------------------------------------------------------------------- !/extends/*.json diff --git a/apps/api-extractor/CHANGELOG.json b/apps/api-extractor/CHANGELOG.json index 094c0b03c1e..794fea3be0f 100644 --- a/apps/api-extractor/CHANGELOG.json +++ b/apps/api-extractor/CHANGELOG.json @@ -1,6 +1,1747 @@ { "name": "@microsoft/api-extractor", "entries": [ + { + "version": "7.55.5", + "tag": "@microsoft/api-extractor_v7.55.5", + "date": "Thu, 08 Jan 2026 01:12:30 GMT", + "comments": { + "patch": [ + { + "comment": "Fix missing 'export' keyword for namespace re-exports that produced invalid TypeScript output" + } + ] + } + }, + { + "version": "7.55.4", + "tag": "@microsoft/api-extractor_v7.55.4", + "date": "Wed, 07 Jan 2026 01:12:24 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.21.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.7`" + } + ] + } + }, + { + "version": "7.55.3", + "tag": "@microsoft/api-extractor_v7.55.3", + "date": "Mon, 05 Jan 2026 16:12:49 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.20.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.6`" + } + ] + } + }, + { + "version": "7.55.2", + "tag": "@microsoft/api-extractor_v7.55.2", + "date": "Sat, 06 Dec 2025 01:12:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.32.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.19.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.5`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.5`" + } + ] + } + }, + { + "version": "7.55.1", + "tag": "@microsoft/api-extractor_v7.55.1", + "date": "Fri, 21 Nov 2025 16:13:56 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.32.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.19.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.4`" + } + ] + } + }, + { + "version": "7.55.0", + "tag": "@microsoft/api-extractor_v7.55.0", + "date": "Wed, 12 Nov 2025 01:12:56 GMT", + "comments": { + "minor": [ + { + "comment": "Bump the `@microsoft/tsdoc` dependency to `~0.16.0`." + }, + { + "comment": "Bump the `@microsoft/tsdoc-config` dependency to `~0.18.0`." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.32.0`" + } + ] + } + }, + { + "version": "7.54.0", + "tag": "@microsoft/api-extractor_v7.54.0", + "date": "Tue, 04 Nov 2025 08:15:14 GMT", + "comments": { + "minor": [ + { + "comment": "Add a new setting `IExtractorInvokeOptions.printApiReportDiff` that makes build logs easier to diagnose by printing a diff of any changes to API report files (*.api.md)." + }, + { + "comment": "Add a `--print-api-report-diff` CLI flag that causes a diff of any changes to API report files (*.api.md) to be printed." + } + ] + } + }, + { + "version": "7.53.3", + "tag": "@microsoft/api-extractor_v7.53.3", + "date": "Fri, 24 Oct 2025 00:13:38 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.31.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.18.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.3`" + } + ] + } + }, + { + "version": "7.53.2", + "tag": "@microsoft/api-extractor_v7.53.2", + "date": "Wed, 22 Oct 2025 00:57:54 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.31.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.17.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.2`" + } + ] + } + }, + { + "version": "7.53.1", + "tag": "@microsoft/api-extractor_v7.53.1", + "date": "Wed, 08 Oct 2025 00:13:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.31.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.17.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.1`" + } + ] + } + }, + { + "version": "7.53.0", + "tag": "@microsoft/api-extractor_v7.53.0", + "date": "Fri, 03 Oct 2025 20:09:59 GMT", + "comments": { + "minor": [ + { + "comment": "Normalize import of builtin modules to use the `node:` protocol." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.31.0`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.16.0`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.6.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.0`" + } + ] + } + }, + { + "version": "7.52.15", + "tag": "@microsoft/api-extractor_v7.52.15", + "date": "Tue, 30 Sep 2025 23:57:45 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.9`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.15.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.18.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.5`" + } + ] + } + }, + { + "version": "7.52.14", + "tag": "@microsoft/api-extractor_v7.52.14", + "date": "Tue, 30 Sep 2025 20:33:51 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.8`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.15.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.17.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.4`" + } + ] + } + }, + { + "version": "7.52.13", + "tag": "@microsoft/api-extractor_v7.52.13", + "date": "Fri, 12 Sep 2025 15:13:07 GMT", + "comments": { + "patch": [ + { + "comment": "Fixes a bug in ExtractorAnalyzer._isExternalModulePath where type import declarations were not resolved." + } + ] + } + }, + { + "version": "7.52.12", + "tag": "@microsoft/api-extractor_v7.52.12", + "date": "Thu, 11 Sep 2025 00:22:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.16.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.3`" + } + ] + } + }, + { + "version": "7.52.11", + "tag": "@microsoft/api-extractor_v7.52.11", + "date": "Tue, 19 Aug 2025 20:45:02 GMT", + "comments": { + "patch": [ + { + "comment": "Fix self-package import resolution by removing outDir/declarationDir from compiler options" + } + ] + } + }, + { + "version": "7.52.10", + "tag": "@microsoft/api-extractor_v7.52.10", + "date": "Fri, 01 Aug 2025 00:12:48 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrades the minimatch dependency from ~3.0.3 to 10.0.3 across the entire Rush monorepo to address a Regular Expression Denial of Service (ReDoS) vulnerability in the underlying brace-expansion dependency." + } + ] + } + }, + { + "version": "7.52.9", + "tag": "@microsoft/api-extractor_v7.52.9", + "date": "Wed, 23 Jul 2025 20:55:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.7`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.14.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.15.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.2`" + } + ] + } + }, + { + "version": "7.52.8", + "tag": "@microsoft/api-extractor_v7.52.8", + "date": "Tue, 13 May 2025 02:09:20 GMT", + "comments": { + "patch": [ + { + "comment": "Fixes API extractor error handling when changed APIs are encountered and the \"--local\" flag is not specified" + } + ] + } + }, + { + "version": "7.52.7", + "tag": "@microsoft/api-extractor_v7.52.7", + "date": "Thu, 01 May 2025 15:11:33 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where default exports were sometimes trimmed incorrectly in .api.md files when using `reportVariants` (GitHub #4775)" + } + ] + } + }, + { + "version": "7.52.6", + "tag": "@microsoft/api-extractor_v7.52.6", + "date": "Thu, 01 May 2025 00:11:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.13.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.15.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.1`" + } + ] + } + }, + { + "version": "7.52.5", + "tag": "@microsoft/api-extractor_v7.52.5", + "date": "Mon, 21 Apr 2025 22:24:25 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.0`" + } + ] + } + }, + { + "version": "7.52.4", + "tag": "@microsoft/api-extractor_v7.52.4", + "date": "Thu, 17 Apr 2025 00:11:21 GMT", + "comments": { + "patch": [ + { + "comment": "Update documentation for `extends`" + } + ] + } + }, + { + "version": "7.52.3", + "tag": "@microsoft/api-extractor_v7.52.3", + "date": "Fri, 04 Apr 2025 18:34:35 GMT", + "comments": { + "patch": [ + { + "comment": "Add support for customizing which TSDoc tags appear in API reports" + } + ] + } + }, + { + "version": "7.52.2", + "tag": "@microsoft/api-extractor_v7.52.2", + "date": "Tue, 25 Mar 2025 15:11:15 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.13.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.15.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.7`" + } + ] + } + }, + { + "version": "7.52.1", + "tag": "@microsoft/api-extractor_v7.52.1", + "date": "Tue, 11 Mar 2025 02:12:34 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.12.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.15.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.6`" + } + ] + } + }, + { + "version": "7.52.0", + "tag": "@microsoft/api-extractor_v7.52.0", + "date": "Tue, 11 Mar 2025 00:11:25 GMT", + "comments": { + "minor": [ + { + "comment": "Upgrade the bundled compiler engine to TypeScript 5.8.2" + } + ] + } + }, + { + "version": "7.51.1", + "tag": "@microsoft/api-extractor_v7.51.1", + "date": "Sat, 01 Mar 2025 05:00:09 GMT", + "comments": { + "patch": [ + { + "comment": "Include triple-slash references marked with `preserve=\"true\"` from files that only contain re-exports. There was a behavior change in TypeScript 5.5, where only triple-slash references that are explicitly marked with `preserve=\"true\"` are emitted into declaration files. This change adds support for placing these references in files that only contain re-exports, like the API entrypoint file." + } + ] + } + }, + { + "version": "7.51.0", + "tag": "@microsoft/api-extractor_v7.51.0", + "date": "Thu, 27 Feb 2025 01:10:39 GMT", + "comments": { + "minor": [ + { + "comment": "Add a `docModel.releaseTagsToTrim` property to `api-extractor.json` to specify which release tags should be trimmed when the doc model is produced." + } + ] + } + }, + { + "version": "7.50.1", + "tag": "@microsoft/api-extractor_v7.50.1", + "date": "Sat, 22 Feb 2025 01:11:11 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade the bundled compiler engine to TypeScript 5.7.3" + } + ] + } + }, + { + "version": "7.50.0", + "tag": "@microsoft/api-extractor_v7.50.0", + "date": "Wed, 12 Feb 2025 01:10:52 GMT", + "comments": { + "minor": [ + { + "comment": "Update merge behavior for derived configurations to allow overriding array properties" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.15.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.5`" + } + ] + } + }, + { + "version": "7.49.2", + "tag": "@microsoft/api-extractor_v7.49.2", + "date": "Thu, 30 Jan 2025 01:11:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.11.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.6`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.4`" + } + ] + } + }, + { + "version": "7.49.1", + "tag": "@microsoft/api-extractor_v7.49.1", + "date": "Thu, 09 Jan 2025 01:10:10 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.10.2`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.5`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.3`" + } + ] + } + }, + { + "version": "7.49.0", + "tag": "@microsoft/api-extractor_v7.49.0", + "date": "Tue, 07 Jan 2025 22:17:32 GMT", + "comments": { + "minor": [ + { + "comment": "Upgrade the bundled compiler engine to TypeScript 5.7.2" + } + ] + } + }, + { + "version": "7.48.1", + "tag": "@microsoft/api-extractor_v7.48.1", + "date": "Sat, 14 Dec 2024 01:11:07 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.10.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.2`" + } + ] + } + }, + { + "version": "7.48.0", + "tag": "@microsoft/api-extractor_v7.48.0", + "date": "Sat, 23 Nov 2024 01:18:55 GMT", + "comments": { + "minor": [ + { + "comment": "Update TSDoc dependencies." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.30.0`" + } + ] + } + }, + { + "version": "7.47.12", + "tag": "@microsoft/api-extractor_v7.47.12", + "date": "Fri, 22 Nov 2024 01:10:43 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.9`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.10.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.1`" + } + ] + } + }, + { + "version": "7.47.11", + "tag": "@microsoft/api-extractor_v7.47.11", + "date": "Thu, 17 Oct 2024 08:35:06 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.0`" + } + ] + } + }, + { + "version": "7.47.10", + "tag": "@microsoft/api-extractor_v7.47.10", + "date": "Tue, 15 Oct 2024 00:12:31 GMT", + "comments": { + "patch": [ + { + "comment": "Fix a compatibility issue with usage of `getModeForUsageLocation` in TypeScript 5.6" + } + ] + } + }, + { + "version": "7.47.9", + "tag": "@microsoft/api-extractor_v7.47.9", + "date": "Fri, 13 Sep 2024 00:11:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.8`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.9.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.8`" + } + ] + } + }, + { + "version": "7.47.8", + "tag": "@microsoft/api-extractor_v7.47.8", + "date": "Tue, 10 Sep 2024 20:08:11 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.7`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.8.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.7`" + } + ] + } + }, + { + "version": "7.47.7", + "tag": "@microsoft/api-extractor_v7.47.7", + "date": "Wed, 21 Aug 2024 05:43:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.7.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.6`" + } + ] + } + }, + { + "version": "7.47.6", + "tag": "@microsoft/api-extractor_v7.47.6", + "date": "Mon, 12 Aug 2024 22:16:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.6.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.13.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.5`" + } + ] + } + }, + { + "version": "7.47.5", + "tag": "@microsoft/api-extractor_v7.47.5", + "date": "Fri, 02 Aug 2024 17:26:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.4`" + } + ] + } + }, + { + "version": "7.47.4", + "tag": "@microsoft/api-extractor_v7.47.4", + "date": "Sat, 27 Jul 2024 00:10:27 GMT", + "comments": { + "patch": [ + { + "comment": "Include CHANGELOG.md in published releases again" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.5.1`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.5.3`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.13.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.3`" + } + ] + } + }, + { + "version": "7.47.3", + "tag": "@microsoft/api-extractor_v7.47.3", + "date": "Wed, 24 Jul 2024 00:12:14 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an edge case when discarding the file extension from the \"reportFileName\" setting and improve its documentation" + } + ] + } + }, + { + "version": "7.47.2", + "tag": "@microsoft/api-extractor_v7.47.2", + "date": "Wed, 17 Jul 2024 06:55:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.13.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.2`" + } + ] + } + }, + { + "version": "7.47.1", + "tag": "@microsoft/api-extractor_v7.47.1", + "date": "Tue, 16 Jul 2024 00:36:22 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.5.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.13.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.1`" + } + ] + } + }, + { + "version": "7.47.0", + "tag": "@microsoft/api-extractor_v7.47.0", + "date": "Mon, 03 Jun 2024 23:43:15 GMT", + "comments": { + "minor": [ + { + "comment": "Add support for re-exporting modules using syntax such as `export * as ns from './file'` (GitHub #2780)" + } + ] + } + }, + { + "version": "7.46.2", + "tag": "@microsoft/api-extractor_v7.46.2", + "date": "Thu, 30 May 2024 00:13:05 GMT", + "comments": { + "patch": [ + { + "comment": "Include missing `type` modifiers on type-only exports." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.4.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.13.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.0`" + } + ] + } + }, + { + "version": "7.46.1", + "tag": "@microsoft/api-extractor_v7.46.1", + "date": "Wed, 29 May 2024 02:03:50 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.4.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.12.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.5`" + } + ] + } + }, + { + "version": "7.46.0", + "tag": "@microsoft/api-extractor_v7.46.0", + "date": "Wed, 29 May 2024 00:10:52 GMT", + "comments": { + "minor": [ + { + "comment": "Bump TSDoc dependencies." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.29.0`" + } + ] + } + }, + { + "version": "7.45.1", + "tag": "@microsoft/api-extractor_v7.45.1", + "date": "Tue, 28 May 2024 15:10:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.21`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.3.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.12.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.4`" + } + ] + } + }, + { + "version": "7.45.0", + "tag": "@microsoft/api-extractor_v7.45.0", + "date": "Tue, 28 May 2024 00:09:47 GMT", + "comments": { + "minor": [ + { + "comment": "Improve support for resolving the `tsdoc-metadata.json` to include the folder referenced by a `types` field in an `\"exports\"` field and an `\"typesVersions\"` field in addition to `\"types\"`, `\"typings\"`, and `\"tsdocMetadata\"` fields." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.20`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.2.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.12.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.3`" + } + ] + } + }, + { + "version": "7.44.1", + "tag": "@microsoft/api-extractor_v7.44.1", + "date": "Sat, 25 May 2024 04:54:07 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.19`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.1.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.12.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.2`" + } + ] + } + }, + { + "version": "7.44.0", + "tag": "@microsoft/api-extractor_v7.44.0", + "date": "Fri, 24 May 2024 00:15:08 GMT", + "comments": { + "minor": [ + { + "comment": "Add support for \"variants\" of API reports which include or exclude items by release tag" + } + ] + } + }, + { + "version": "7.43.8", + "tag": "@microsoft/api-extractor_v7.43.8", + "date": "Thu, 23 May 2024 02:26:56 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.18`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.0.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.11.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.1`" + } + ] + } + }, + { + "version": "7.43.7", + "tag": "@microsoft/api-extractor_v7.43.7", + "date": "Thu, 16 May 2024 15:10:22 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.0`" + } + ] + } + }, + { + "version": "7.43.6", + "tag": "@microsoft/api-extractor_v7.43.6", + "date": "Wed, 15 May 2024 23:42:58 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.11.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.20.1`" + } + ] + } + }, + { + "version": "7.43.5", + "tag": "@microsoft/api-extractor_v7.43.5", + "date": "Wed, 15 May 2024 06:04:17 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.17`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.3.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.10.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.20.0`" + } + ] + } + }, + { + "version": "7.43.4", + "tag": "@microsoft/api-extractor_v7.43.4", + "date": "Fri, 10 May 2024 05:33:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.16`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.2.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.10.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.5`" + } + ] + } + }, + { + "version": "7.43.3", + "tag": "@microsoft/api-extractor_v7.43.3", + "date": "Wed, 08 May 2024 22:23:50 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.4`" + } + ] + } + }, + { + "version": "7.43.2", + "tag": "@microsoft/api-extractor_v7.43.2", + "date": "Mon, 06 May 2024 15:11:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.15`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.2.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.10.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.3`" + } + ] + } + }, + { + "version": "7.43.1", + "tag": "@microsoft/api-extractor_v7.43.1", + "date": "Wed, 10 Apr 2024 15:10:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.14`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.1.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.10.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.2`" + } + ] + } + }, + { + "version": "7.43.0", + "tag": "@microsoft/api-extractor_v7.43.0", + "date": "Tue, 19 Mar 2024 15:10:18 GMT", + "comments": { + "minor": [ + { + "comment": "Upgrade the bundled compiler engine to TypeScript 5.4.2" + } + ] + } + }, + { + "version": "7.42.3", + "tag": "@microsoft/api-extractor_v7.42.3", + "date": "Sun, 03 Mar 2024 20:58:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.1`" + } + ] + } + }, + { + "version": "7.42.2", + "tag": "@microsoft/api-extractor_v7.42.2", + "date": "Sat, 02 Mar 2024 02:22:23 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.0`" + } + ] + } + }, + { + "version": "7.42.1", + "tag": "@microsoft/api-extractor_v7.42.1", + "date": "Fri, 01 Mar 2024 01:10:08 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.18.1`" + } + ] + } + }, + { + "version": "7.42.0", + "tag": "@microsoft/api-extractor_v7.42.0", + "date": "Thu, 29 Feb 2024 07:11:45 GMT", + "comments": { + "patch": [ + { + "comment": "Don't mark items documented with {@inheritDoc} references to package-external items as \"undocumented\"" + } + ], + "minor": [ + { + "comment": "Add glob support in `bundledPackages`" + } + ] + } + }, + { + "version": "7.41.1", + "tag": "@microsoft/api-extractor_v7.41.1", + "date": "Wed, 28 Feb 2024 16:09:27 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.18.0`" + } + ] + } + }, + { + "version": "7.41.0", + "tag": "@microsoft/api-extractor_v7.41.0", + "date": "Sat, 24 Feb 2024 23:02:51 GMT", + "comments": { + "minor": [ + { + "comment": "Replace const enums with conventional enums to allow for compatibility with JavaScript consumers." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.10.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.4`" + } + ] + } + }, + { + "version": "7.40.6", + "tag": "@microsoft/api-extractor_v7.40.6", + "date": "Wed, 21 Feb 2024 21:45:28 GMT", + "comments": { + "patch": [ + { + "comment": "Replace the dependency on the `colors` package with `Colorize` from `@rushstack/terminal`." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.13`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.0.2`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.9.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.3`" + } + ] + } + }, + { + "version": "7.40.5", + "tag": "@microsoft/api-extractor_v7.40.5", + "date": "Wed, 21 Feb 2024 08:55:47 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where imports were trimmed from external packages based when generating .d.ts rollups" + } + ] + } + }, + { + "version": "7.40.4", + "tag": "@microsoft/api-extractor_v7.40.4", + "date": "Tue, 20 Feb 2024 21:45:10 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.12`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.0.1`" + } + ] + } + }, + { + "version": "7.40.3", + "tag": "@microsoft/api-extractor_v7.40.3", + "date": "Mon, 19 Feb 2024 21:54:27 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.11`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.0.0`" + } + ] + } + }, + { + "version": "7.40.2", + "tag": "@microsoft/api-extractor_v7.40.2", + "date": "Sat, 17 Feb 2024 06:24:34 GMT", + "comments": { + "patch": [ + { + "comment": "Fix broken link to API documentation" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.10`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.66.1`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.5.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.2`" + } + ] + } + }, + { + "version": "7.40.1", + "tag": "@microsoft/api-extractor_v7.40.1", + "date": "Thu, 08 Feb 2024 01:09:21 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.9`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.66.0`" + } + ] + } + }, + { + "version": "7.40.0", + "tag": "@microsoft/api-extractor_v7.40.0", + "date": "Wed, 07 Feb 2024 01:11:18 GMT", + "comments": { + "minor": [ + { + "comment": "Classify arrow functions as `function` kind in the doc model export." + } + ] + } + }, + { + "version": "7.39.5", + "tag": "@microsoft/api-extractor_v7.39.5", + "date": "Mon, 05 Feb 2024 23:46:52 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.8`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.65.0`" + } + ] + } + }, + { + "version": "7.39.4", + "tag": "@microsoft/api-extractor_v7.39.4", + "date": "Thu, 25 Jan 2024 01:09:30 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.7`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.64.2`" + } + ] + } + }, + { + "version": "7.39.3", + "tag": "@microsoft/api-extractor_v7.39.3", + "date": "Tue, 23 Jan 2024 20:12:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.64.1`" + } + ] + } + }, + { + "version": "7.39.2", + "tag": "@microsoft/api-extractor_v7.39.2", + "date": "Tue, 23 Jan 2024 16:15:05 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.64.0`" + } + ] + } + }, + { + "version": "7.39.1", + "tag": "@microsoft/api-extractor_v7.39.1", + "date": "Wed, 03 Jan 2024 00:31:18 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.63.0`" + } + ] + } + }, + { + "version": "7.39.0", + "tag": "@microsoft/api-extractor_v7.39.0", + "date": "Wed, 20 Dec 2023 01:09:45 GMT", + "comments": { + "minor": [ + { + "comment": "Update API Extractor to support TypeScript 5.3.3" + } + ] + } + }, + { + "version": "7.38.5", + "tag": "@microsoft/api-extractor_v7.38.5", + "date": "Thu, 07 Dec 2023 03:44:13 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.62.0`" + } + ] + } + }, + { + "version": "7.38.4", + "tag": "@microsoft/api-extractor_v7.38.4", + "date": "Tue, 05 Dec 2023 01:10:16 GMT", + "comments": { + "patch": [ + { + "comment": "Don't export trimmed namespace members during rollup (#2791)" + } + ] + } + }, + { + "version": "7.38.3", + "tag": "@microsoft/api-extractor_v7.38.3", + "date": "Fri, 10 Nov 2023 18:02:04 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where \"ae-undocumented\" was incorrectly reported for private members" + } + ] + } + }, + { + "version": "7.38.2", + "tag": "@microsoft/api-extractor_v7.38.2", + "date": "Wed, 01 Nov 2023 23:11:35 GMT", + "comments": { + "patch": [ + { + "comment": "Fix line endings in published package." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.1`" + } + ] + } + }, + { + "version": "7.38.1", + "tag": "@microsoft/api-extractor_v7.38.1", + "date": "Mon, 30 Oct 2023 23:36:38 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.0`" + } + ] + } + }, + { + "version": "7.38.0", + "tag": "@microsoft/api-extractor_v7.38.0", + "date": "Sun, 01 Oct 2023 02:56:29 GMT", + "comments": { + "minor": [ + { + "comment": "Add a new message \"ae-undocumented\" to support logging of undocumented API items" + } + ] + } + }, + { + "version": "7.37.3", + "tag": "@microsoft/api-extractor_v7.37.3", + "date": "Sat, 30 Sep 2023 00:20:51 GMT", + "comments": { + "patch": [ + { + "comment": "Don't strip out @alpha items when generating API reports." + } + ] + } + }, + { + "version": "7.37.2", + "tag": "@microsoft/api-extractor_v7.37.2", + "date": "Thu, 28 Sep 2023 20:53:16 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.61.0`" + } + ] + } + }, + { + "version": "7.37.1", + "tag": "@microsoft/api-extractor_v7.37.1", + "date": "Tue, 26 Sep 2023 09:30:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update type-only imports to include the type modifier." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.60.1`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.5.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.16.1`" + } + ] + } + }, + { + "version": "7.37.0", + "tag": "@microsoft/api-extractor_v7.37.0", + "date": "Fri, 15 Sep 2023 00:36:58 GMT", + "comments": { + "minor": [ + { + "comment": "Update @types/node from 14 to 18" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.0`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.60.0`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.5.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.16.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.4`" + } + ] + } + }, + { + "version": "7.36.4", + "tag": "@microsoft/api-extractor_v7.36.4", + "date": "Tue, 08 Aug 2023 07:10:39 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.7`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.4.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.15.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.3`" + } + ] + } + }, + { + "version": "7.36.3", + "tag": "@microsoft/api-extractor_v7.36.3", + "date": "Wed, 19 Jul 2023 00:20:31 GMT", + "comments": { + "patch": [ + { + "comment": "Updated semver dependency" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.6`" + } + ] + } + }, + { + "version": "7.36.2", + "tag": "@microsoft/api-extractor_v7.36.2", + "date": "Wed, 12 Jul 2023 15:20:39 GMT", + "comments": { + "patch": [ + { + "comment": "Add api-extractor support for .d.mts and .d.cts files" + } + ] + } + }, + { + "version": "7.36.1", + "tag": "@microsoft/api-extractor_v7.36.1", + "date": "Thu, 06 Jul 2023 00:16:19 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.5`" + } + ] + } + }, + { + "version": "7.36.0", + "tag": "@microsoft/api-extractor_v7.36.0", + "date": "Mon, 19 Jun 2023 22:40:21 GMT", + "comments": { + "minor": [ + { + "comment": "Use the `IRigConfig` interface in the `IExtractorConfigLoadForFolderOptions` object insteacd of the `RigConfig` class." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.4.0`" + } + ] + } + }, + { + "version": "7.35.4", + "tag": "@microsoft/api-extractor_v7.35.4", + "date": "Thu, 15 Jun 2023 00:21:01 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.4`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.21`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.15.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.2`" + } + ] + } + }, + { + "version": "7.35.3", + "tag": "@microsoft/api-extractor_v7.35.3", + "date": "Tue, 13 Jun 2023 01:49:01 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.15.0`" + } + ] + } + }, + { + "version": "7.35.2", + "tag": "@microsoft/api-extractor_v7.35.2", + "date": "Wed, 07 Jun 2023 22:45:16 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.3`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.20`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.14.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.1`" + } + ] + } + }, { "version": "7.35.1", "tag": "@microsoft/api-extractor_v7.35.1", diff --git a/apps/api-extractor/CHANGELOG.md b/apps/api-extractor/CHANGELOG.md index 8b7a8fa69fa..35211ed5489 100644 --- a/apps/api-extractor/CHANGELOG.md +++ b/apps/api-extractor/CHANGELOG.md @@ -1,6 +1,610 @@ # Change Log - @microsoft/api-extractor -This log was last generated on Mon, 29 May 2023 15:21:15 GMT and should not be manually modified. +This log was last generated on Thu, 08 Jan 2026 01:12:30 GMT and should not be manually modified. + +## 7.55.5 +Thu, 08 Jan 2026 01:12:30 GMT + +### Patches + +- Fix missing 'export' keyword for namespace re-exports that produced invalid TypeScript output + +## 7.55.4 +Wed, 07 Jan 2026 01:12:24 GMT + +_Version update only_ + +## 7.55.3 +Mon, 05 Jan 2026 16:12:49 GMT + +_Version update only_ + +## 7.55.2 +Sat, 06 Dec 2025 01:12:28 GMT + +_Version update only_ + +## 7.55.1 +Fri, 21 Nov 2025 16:13:56 GMT + +_Version update only_ + +## 7.55.0 +Wed, 12 Nov 2025 01:12:56 GMT + +### Minor changes + +- Bump the `@microsoft/tsdoc` dependency to `~0.16.0`. +- Bump the `@microsoft/tsdoc-config` dependency to `~0.18.0`. + +## 7.54.0 +Tue, 04 Nov 2025 08:15:14 GMT + +### Minor changes + +- Add a new setting `IExtractorInvokeOptions.printApiReportDiff` that makes build logs easier to diagnose by printing a diff of any changes to API report files (*.api.md). +- Add a `--print-api-report-diff` CLI flag that causes a diff of any changes to API report files (*.api.md) to be printed. + +## 7.53.3 +Fri, 24 Oct 2025 00:13:38 GMT + +_Version update only_ + +## 7.53.2 +Wed, 22 Oct 2025 00:57:54 GMT + +_Version update only_ + +## 7.53.1 +Wed, 08 Oct 2025 00:13:28 GMT + +_Version update only_ + +## 7.53.0 +Fri, 03 Oct 2025 20:09:59 GMT + +### Minor changes + +- Normalize import of builtin modules to use the `node:` protocol. + +## 7.52.15 +Tue, 30 Sep 2025 23:57:45 GMT + +_Version update only_ + +## 7.52.14 +Tue, 30 Sep 2025 20:33:51 GMT + +_Version update only_ + +## 7.52.13 +Fri, 12 Sep 2025 15:13:07 GMT + +### Patches + +- Fixes a bug in ExtractorAnalyzer._isExternalModulePath where type import declarations were not resolved. + +## 7.52.12 +Thu, 11 Sep 2025 00:22:31 GMT + +_Version update only_ + +## 7.52.11 +Tue, 19 Aug 2025 20:45:02 GMT + +### Patches + +- Fix self-package import resolution by removing outDir/declarationDir from compiler options + +## 7.52.10 +Fri, 01 Aug 2025 00:12:48 GMT + +### Patches + +- Upgrades the minimatch dependency from ~3.0.3 to 10.0.3 across the entire Rush monorepo to address a Regular Expression Denial of Service (ReDoS) vulnerability in the underlying brace-expansion dependency. + +## 7.52.9 +Wed, 23 Jul 2025 20:55:57 GMT + +_Version update only_ + +## 7.52.8 +Tue, 13 May 2025 02:09:20 GMT + +### Patches + +- Fixes API extractor error handling when changed APIs are encountered and the "--local" flag is not specified + +## 7.52.7 +Thu, 01 May 2025 15:11:33 GMT + +### Patches + +- Fix an issue where default exports were sometimes trimmed incorrectly in .api.md files when using `reportVariants` (GitHub #4775) + +## 7.52.6 +Thu, 01 May 2025 00:11:12 GMT + +_Version update only_ + +## 7.52.5 +Mon, 21 Apr 2025 22:24:25 GMT + +_Version update only_ + +## 7.52.4 +Thu, 17 Apr 2025 00:11:21 GMT + +### Patches + +- Update documentation for `extends` + +## 7.52.3 +Fri, 04 Apr 2025 18:34:35 GMT + +### Patches + +- Add support for customizing which TSDoc tags appear in API reports + +## 7.52.2 +Tue, 25 Mar 2025 15:11:15 GMT + +_Version update only_ + +## 7.52.1 +Tue, 11 Mar 2025 02:12:34 GMT + +_Version update only_ + +## 7.52.0 +Tue, 11 Mar 2025 00:11:25 GMT + +### Minor changes + +- Upgrade the bundled compiler engine to TypeScript 5.8.2 + +## 7.51.1 +Sat, 01 Mar 2025 05:00:09 GMT + +### Patches + +- Include triple-slash references marked with `preserve="true"` from files that only contain re-exports. There was a behavior change in TypeScript 5.5, where only triple-slash references that are explicitly marked with `preserve="true"` are emitted into declaration files. This change adds support for placing these references in files that only contain re-exports, like the API entrypoint file. + +## 7.51.0 +Thu, 27 Feb 2025 01:10:39 GMT + +### Minor changes + +- Add a `docModel.releaseTagsToTrim` property to `api-extractor.json` to specify which release tags should be trimmed when the doc model is produced. + +## 7.50.1 +Sat, 22 Feb 2025 01:11:11 GMT + +### Patches + +- Upgrade the bundled compiler engine to TypeScript 5.7.3 + +## 7.50.0 +Wed, 12 Feb 2025 01:10:52 GMT + +### Minor changes + +- Update merge behavior for derived configurations to allow overriding array properties + +## 7.49.2 +Thu, 30 Jan 2025 01:11:42 GMT + +_Version update only_ + +## 7.49.1 +Thu, 09 Jan 2025 01:10:10 GMT + +_Version update only_ + +## 7.49.0 +Tue, 07 Jan 2025 22:17:32 GMT + +### Minor changes + +- Upgrade the bundled compiler engine to TypeScript 5.7.2 + +## 7.48.1 +Sat, 14 Dec 2024 01:11:07 GMT + +_Version update only_ + +## 7.48.0 +Sat, 23 Nov 2024 01:18:55 GMT + +### Minor changes + +- Update TSDoc dependencies. + +## 7.47.12 +Fri, 22 Nov 2024 01:10:43 GMT + +_Version update only_ + +## 7.47.11 +Thu, 17 Oct 2024 08:35:06 GMT + +_Version update only_ + +## 7.47.10 +Tue, 15 Oct 2024 00:12:31 GMT + +### Patches + +- Fix a compatibility issue with usage of `getModeForUsageLocation` in TypeScript 5.6 + +## 7.47.9 +Fri, 13 Sep 2024 00:11:42 GMT + +_Version update only_ + +## 7.47.8 +Tue, 10 Sep 2024 20:08:11 GMT + +_Version update only_ + +## 7.47.7 +Wed, 21 Aug 2024 05:43:04 GMT + +_Version update only_ + +## 7.47.6 +Mon, 12 Aug 2024 22:16:04 GMT + +_Version update only_ + +## 7.47.5 +Fri, 02 Aug 2024 17:26:42 GMT + +_Version update only_ + +## 7.47.4 +Sat, 27 Jul 2024 00:10:27 GMT + +### Patches + +- Include CHANGELOG.md in published releases again + +## 7.47.3 +Wed, 24 Jul 2024 00:12:14 GMT + +### Patches + +- Fix an edge case when discarding the file extension from the "reportFileName" setting and improve its documentation + +## 7.47.2 +Wed, 17 Jul 2024 06:55:09 GMT + +_Version update only_ + +## 7.47.1 +Tue, 16 Jul 2024 00:36:22 GMT + +_Version update only_ + +## 7.47.0 +Mon, 03 Jun 2024 23:43:15 GMT + +### Minor changes + +- Add support for re-exporting modules using syntax such as `export * as ns from './file'` (GitHub #2780) + +## 7.46.2 +Thu, 30 May 2024 00:13:05 GMT + +### Patches + +- Include missing `type` modifiers on type-only exports. + +## 7.46.1 +Wed, 29 May 2024 02:03:50 GMT + +_Version update only_ + +## 7.46.0 +Wed, 29 May 2024 00:10:52 GMT + +### Minor changes + +- Bump TSDoc dependencies. + +## 7.45.1 +Tue, 28 May 2024 15:10:09 GMT + +_Version update only_ + +## 7.45.0 +Tue, 28 May 2024 00:09:47 GMT + +### Minor changes + +- Improve support for resolving the `tsdoc-metadata.json` to include the folder referenced by a `types` field in an `"exports"` field and an `"typesVersions"` field in addition to `"types"`, `"typings"`, and `"tsdocMetadata"` fields. + +## 7.44.1 +Sat, 25 May 2024 04:54:07 GMT + +_Version update only_ + +## 7.44.0 +Fri, 24 May 2024 00:15:08 GMT + +### Minor changes + +- Add support for "variants" of API reports which include or exclude items by release tag + +## 7.43.8 +Thu, 23 May 2024 02:26:56 GMT + +_Version update only_ + +## 7.43.7 +Thu, 16 May 2024 15:10:22 GMT + +_Version update only_ + +## 7.43.6 +Wed, 15 May 2024 23:42:58 GMT + +_Version update only_ + +## 7.43.5 +Wed, 15 May 2024 06:04:17 GMT + +_Version update only_ + +## 7.43.4 +Fri, 10 May 2024 05:33:33 GMT + +_Version update only_ + +## 7.43.3 +Wed, 08 May 2024 22:23:50 GMT + +_Version update only_ + +## 7.43.2 +Mon, 06 May 2024 15:11:04 GMT + +_Version update only_ + +## 7.43.1 +Wed, 10 Apr 2024 15:10:09 GMT + +_Version update only_ + +## 7.43.0 +Tue, 19 Mar 2024 15:10:18 GMT + +### Minor changes + +- Upgrade the bundled compiler engine to TypeScript 5.4.2 + +## 7.42.3 +Sun, 03 Mar 2024 20:58:12 GMT + +_Version update only_ + +## 7.42.2 +Sat, 02 Mar 2024 02:22:23 GMT + +_Version update only_ + +## 7.42.1 +Fri, 01 Mar 2024 01:10:08 GMT + +_Version update only_ + +## 7.42.0 +Thu, 29 Feb 2024 07:11:45 GMT + +### Minor changes + +- Add glob support in `bundledPackages` + +### Patches + +- Don't mark items documented with {@inheritDoc} references to package-external items as "undocumented" + +## 7.41.1 +Wed, 28 Feb 2024 16:09:27 GMT + +_Version update only_ + +## 7.41.0 +Sat, 24 Feb 2024 23:02:51 GMT + +### Minor changes + +- Replace const enums with conventional enums to allow for compatibility with JavaScript consumers. + +## 7.40.6 +Wed, 21 Feb 2024 21:45:28 GMT + +### Patches + +- Replace the dependency on the `colors` package with `Colorize` from `@rushstack/terminal`. + +## 7.40.5 +Wed, 21 Feb 2024 08:55:47 GMT + +### Patches + +- Fix an issue where imports were trimmed from external packages based when generating .d.ts rollups + +## 7.40.4 +Tue, 20 Feb 2024 21:45:10 GMT + +_Version update only_ + +## 7.40.3 +Mon, 19 Feb 2024 21:54:27 GMT + +_Version update only_ + +## 7.40.2 +Sat, 17 Feb 2024 06:24:34 GMT + +### Patches + +- Fix broken link to API documentation + +## 7.40.1 +Thu, 08 Feb 2024 01:09:21 GMT + +_Version update only_ + +## 7.40.0 +Wed, 07 Feb 2024 01:11:18 GMT + +### Minor changes + +- Classify arrow functions as `function` kind in the doc model export. + +## 7.39.5 +Mon, 05 Feb 2024 23:46:52 GMT + +_Version update only_ + +## 7.39.4 +Thu, 25 Jan 2024 01:09:30 GMT + +_Version update only_ + +## 7.39.3 +Tue, 23 Jan 2024 20:12:57 GMT + +_Version update only_ + +## 7.39.2 +Tue, 23 Jan 2024 16:15:05 GMT + +_Version update only_ + +## 7.39.1 +Wed, 03 Jan 2024 00:31:18 GMT + +_Version update only_ + +## 7.39.0 +Wed, 20 Dec 2023 01:09:45 GMT + +### Minor changes + +- Update API Extractor to support TypeScript 5.3.3 + +## 7.38.5 +Thu, 07 Dec 2023 03:44:13 GMT + +_Version update only_ + +## 7.38.4 +Tue, 05 Dec 2023 01:10:16 GMT + +### Patches + +- Don't export trimmed namespace members during rollup (#2791) + +## 7.38.3 +Fri, 10 Nov 2023 18:02:04 GMT + +### Patches + +- Fix an issue where "ae-undocumented" was incorrectly reported for private members + +## 7.38.2 +Wed, 01 Nov 2023 23:11:35 GMT + +### Patches + +- Fix line endings in published package. + +## 7.38.1 +Mon, 30 Oct 2023 23:36:38 GMT + +_Version update only_ + +## 7.38.0 +Sun, 01 Oct 2023 02:56:29 GMT + +### Minor changes + +- Add a new message "ae-undocumented" to support logging of undocumented API items + +## 7.37.3 +Sat, 30 Sep 2023 00:20:51 GMT + +### Patches + +- Don't strip out @alpha items when generating API reports. + +## 7.37.2 +Thu, 28 Sep 2023 20:53:16 GMT + +_Version update only_ + +## 7.37.1 +Tue, 26 Sep 2023 09:30:33 GMT + +### Patches + +- Update type-only imports to include the type modifier. + +## 7.37.0 +Fri, 15 Sep 2023 00:36:58 GMT + +### Minor changes + +- Update @types/node from 14 to 18 + +## 7.36.4 +Tue, 08 Aug 2023 07:10:39 GMT + +_Version update only_ + +## 7.36.3 +Wed, 19 Jul 2023 00:20:31 GMT + +### Patches + +- Updated semver dependency + +## 7.36.2 +Wed, 12 Jul 2023 15:20:39 GMT + +### Patches + +- Add api-extractor support for .d.mts and .d.cts files + +## 7.36.1 +Thu, 06 Jul 2023 00:16:19 GMT + +_Version update only_ + +## 7.36.0 +Mon, 19 Jun 2023 22:40:21 GMT + +### Minor changes + +- Use the `IRigConfig` interface in the `IExtractorConfigLoadForFolderOptions` object insteacd of the `RigConfig` class. + +## 7.35.4 +Thu, 15 Jun 2023 00:21:01 GMT + +_Version update only_ + +## 7.35.3 +Tue, 13 Jun 2023 01:49:01 GMT + +_Version update only_ + +## 7.35.2 +Wed, 07 Jun 2023 22:45:16 GMT + +_Version update only_ ## 7.35.1 Mon, 29 May 2023 15:21:15 GMT diff --git a/apps/api-extractor/README.md b/apps/api-extractor/README.md index 1f3ebfa7c14..69c1846fc1d 100644 --- a/apps/api-extractor/README.md +++ b/apps/api-extractor/README.md @@ -46,6 +46,6 @@ For more details and support resources, please visit: https://api-extractor.com/ - [CHANGELOG.md]( https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/CHANGELOG.md) - Find out what's new in the latest version -- [API Reference](https://rushstack.io/pages/api/api-extractor/) +- [API Reference](https://api.rushstack.io/pages/api-extractor/) API Extractor is part of the [Rush Stack](https://rushstack.io/) family of projects. diff --git a/apps/api-extractor/bin/api-extractor b/apps/api-extractor/bin/api-extractor index 783bb806fce..aee68e80224 100755 --- a/apps/api-extractor/bin/api-extractor +++ b/apps/api-extractor/bin/api-extractor @@ -1,2 +1,2 @@ #!/usr/bin/env node -require('../lib/start.js') +require('../lib/start.js'); diff --git a/apps/api-extractor/build-tests.cmd b/apps/api-extractor/build-tests.cmd index f2ae7a2cc93..cc33d4a4554 100644 --- a/apps/api-extractor/build-tests.cmd +++ b/apps/api-extractor/build-tests.cmd @@ -1,3 +1,3 @@ @ECHO OFF @SETLOCAL -rush test -t api-extractor-lib1-test -t api-extractor-lib2-test -t api-extractor-lib3-test -t api-extractor-scenarios -t api-extractor-test-01 -t api-extractor-test-02 -t api-extractor-test-03 -t api-extractor-test-04 -t api-documenter-test +rush test -t tag:api-extractor-tests diff --git a/apps/api-extractor/config/heft.json b/apps/api-extractor/config/heft.json new file mode 100644 index 00000000000..a36ac2cf4c0 --- /dev/null +++ b/apps/api-extractor/config/heft.json @@ -0,0 +1,53 @@ +/** + * Defines configuration used by core Heft. + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for standard + * settings to be shared across multiple projects. + * + * To delete an inherited setting, set it to `null` in this file. + */ + "extends": "decoupled-local-node-rig/profiles/default/config/heft.json", + + "phasesByName": { + "build": { + "tasksByName": { + "perform-copy": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft", + "pluginName": "copy-files-plugin", + "options": { + "copyOperations": [ + { + "sourcePath": "src", + "destinationFolders": ["lib"], + "includeGlobs": ["**/test/test-data/**/*"] + } + ] + } + } + }, + + "copy-json-schemas": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft", + "pluginName": "copy-files-plugin", + "options": { + "copyOperations": [ + { + "sourcePath": "src/schemas", + "destinationFolders": ["temp/json-schemas/api-extractor/v7"], + "fileExtensions": [".schema.json"], + "hardlink": true + } + ] + } + } + } + } + } + } +} diff --git a/apps/api-extractor/config/jest.config.json b/apps/api-extractor/config/jest.config.json index 4bb17bde3ee..7c0f9ccc9d6 100644 --- a/apps/api-extractor/config/jest.config.json +++ b/apps/api-extractor/config/jest.config.json @@ -1,3 +1,3 @@ { - "extends": "@rushstack/heft-node-rig/profiles/default/config/jest.config.json" + "extends": "decoupled-local-node-rig/profiles/default/config/jest.config.json" } diff --git a/apps/api-extractor/config/rig.json b/apps/api-extractor/config/rig.json index 6ac88a96368..cc98dea43dd 100644 --- a/apps/api-extractor/config/rig.json +++ b/apps/api-extractor/config/rig.json @@ -3,5 +3,5 @@ // Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@rushstack/heft-node-rig" + "rigPackageName": "decoupled-local-node-rig" } diff --git a/apps/api-extractor/eslint.config.js b/apps/api-extractor/eslint.config.js new file mode 100644 index 00000000000..2b99226b7c4 --- /dev/null +++ b/apps/api-extractor/eslint.config.js @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +const nodeTrustedToolProfile = require('decoupled-local-node-rig/profiles/default/includes/eslint/flat/profile/node-trusted-tool'); +const friendlyLocalsMixin = require('decoupled-local-node-rig/profiles/default/includes/eslint/flat/mixins/friendly-locals'); + +module.exports = [ + ...nodeTrustedToolProfile, + ...friendlyLocalsMixin, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + tsconfigRootDir: __dirname + } + }, + rules: { + 'no-console': 'off' + } + } +]; diff --git a/apps/api-extractor/package.json b/apps/api-extractor/package.json index 00f6ceb9815..27948b9dfcd 100644 --- a/apps/api-extractor/package.json +++ b/apps/api-extractor/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/api-extractor", - "version": "7.35.1", + "version": "7.55.5", "description": "Analyze the exported API for a TypeScript library and generate reviews, documentation, and .d.ts rollups", "keywords": [ "typescript", @@ -33,31 +33,32 @@ "license": "MIT", "scripts": { "build": "heft build --clean", - "_phase:build": "heft build --clean", - "_phase:test": "heft test --no-build" + "_phase:build": "heft run --only build -- --clean", + "_phase:test": "heft run --only test -- --clean" }, "dependencies": { "@microsoft/api-extractor-model": "workspace:*", - "@microsoft/tsdoc": "0.14.2", - "@microsoft/tsdoc-config": "~0.16.1", + "@microsoft/tsdoc-config": "~0.18.0", + "@microsoft/tsdoc": "~0.16.0", "@rushstack/node-core-library": "workspace:*", "@rushstack/rig-package": "workspace:*", + "@rushstack/terminal": "workspace:*", "@rushstack/ts-command-line": "workspace:*", - "colors": "~1.2.1", + "diff": "~8.0.2", "lodash": "~4.17.15", + "minimatch": "10.0.3", "resolve": "~1.22.1", - "semver": "~7.3.0", + "semver": "~7.5.4", "source-map": "~0.6.1", - "typescript": "~5.0.4" + "typescript": "5.8.2" }, "devDependencies": { - "@rushstack/eslint-config": "workspace:*", - "@rushstack/heft": "0.50.6", - "@rushstack/heft-node-rig": "1.13.0", - "@types/heft-jest": "1.0.1", + "@rushstack/heft": "1.1.7", "@types/lodash": "4.14.116", - "@types/node": "14.18.36", "@types/resolve": "1.20.2", - "@types/semver": "7.3.5" + "@types/semver": "7.5.0", + "decoupled-local-node-rig": "workspace:*", + "eslint": "~9.37.0", + "local-eslint-config": "workspace:*" } } diff --git a/apps/api-extractor/src/aedoc/PackageDocComment.ts b/apps/api-extractor/src/aedoc/PackageDocComment.ts index ea0ea4d9def..3cc10e34526 100644 --- a/apps/api-extractor/src/aedoc/PackageDocComment.ts +++ b/apps/api-extractor/src/aedoc/PackageDocComment.ts @@ -2,7 +2,8 @@ // See LICENSE in the project root for license information. import * as ts from 'typescript'; -import { Collector } from '../collector/Collector'; + +import type { Collector } from '../collector/Collector'; import { ExtractorMessageId } from '../api/ExtractorMessageId'; export class PackageDocComment { diff --git a/apps/api-extractor/src/analyzer/AstDeclaration.ts b/apps/api-extractor/src/analyzer/AstDeclaration.ts index 02540312168..78cc4ed7337 100644 --- a/apps/api-extractor/src/analyzer/AstDeclaration.ts +++ b/apps/api-extractor/src/analyzer/AstDeclaration.ts @@ -2,10 +2,12 @@ // See LICENSE in the project root for license information. import * as ts from 'typescript'; -import { AstSymbol } from './AstSymbol'; -import { Span } from './Span'; + import { InternalError } from '@rushstack/node-core-library'; -import { AstEntity } from './AstEntity'; + +import type { AstSymbol } from './AstSymbol'; +import { Span } from './Span'; +import type { AstEntity } from './AstEntity'; /** * Constructor options for AstDeclaration diff --git a/apps/api-extractor/src/analyzer/AstImport.ts b/apps/api-extractor/src/analyzer/AstImport.ts index 3a39a8ff7e6..38d4b1928a9 100644 --- a/apps/api-extractor/src/analyzer/AstImport.ts +++ b/apps/api-extractor/src/analyzer/AstImport.ts @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { AstSymbol } from './AstSymbol'; import { InternalError } from '@rushstack/node-core-library'; + +import type { AstSymbol } from './AstSymbol'; import { AstSyntheticEntity } from './AstEntity'; /** @@ -154,8 +155,8 @@ export class AstImport extends AstSyntheticEntity { const subKey: string = !options.exportName ? '*' // Equivalent to StarImport : options.exportName.includes('.') // Equivalent to a named export - ? options.exportName.split('.')[0] - : options.exportName; + ? options.exportName.split('.')[0] + : options.exportName; return `${options.modulePath}:${subKey}`; } default: diff --git a/apps/api-extractor/src/analyzer/AstModule.ts b/apps/api-extractor/src/analyzer/AstModule.ts index 264d06d5f19..1ae3c2e91a3 100644 --- a/apps/api-extractor/src/analyzer/AstModule.ts +++ b/apps/api-extractor/src/analyzer/AstModule.ts @@ -1,17 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as ts from 'typescript'; +import type * as ts from 'typescript'; -import { AstSymbol } from './AstSymbol'; -import { AstEntity } from './AstEntity'; +import type { AstSymbol } from './AstSymbol'; +import type { AstEntity } from './AstEntity'; /** * Represents information collected by {@link AstSymbolTable.fetchAstModuleExportInfo} */ -export class AstModuleExportInfo { - public readonly exportedLocalEntities: Map = new Map(); - public readonly starExportedExternalModules: Set = new Set(); +export interface IAstModuleExportInfo { + readonly visitedAstModules: Set; + readonly exportedLocalEntities: Map; + readonly starExportedExternalModules: Set; } /** @@ -64,7 +65,7 @@ export class AstModule { /** * Additional state calculated by `AstSymbolTable.fetchWorkingPackageModule()`. */ - public astModuleExportInfo: AstModuleExportInfo | undefined; + public astModuleExportInfo: IAstModuleExportInfo | undefined; public constructor(options: IAstModuleOptions) { this.sourceFile = options.sourceFile; diff --git a/apps/api-extractor/src/analyzer/AstNamespaceExport.ts b/apps/api-extractor/src/analyzer/AstNamespaceExport.ts new file mode 100644 index 00000000000..f339bf6dbbb --- /dev/null +++ b/apps/api-extractor/src/analyzer/AstNamespaceExport.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { AstNamespaceImport, type IAstNamespaceImportOptions } from './AstNamespaceImport'; + +export interface IAstNamespaceExportOptions extends IAstNamespaceImportOptions {} + +/** + * `AstNamespaceExport` represents a namespace that is created implicitly and exported by a statement + * such as `export * as example from "./file";` + * + * @remarks + * + * A typical input looks like this: + * ```ts + * // Suppose that example.ts exports two functions f1() and f2(). + * export * as example from "./file"; + * ``` + * + * API Extractor's .d.ts rollup will transform it into an explicit namespace, like this: + * ```ts + * declare f1(): void; + * declare f2(): void; + * + * export declare namespace example { + * export { + * f1, + * f2 + * } + * } + * ``` + * + * The current implementation does not attempt to relocate f1()/f2() to be inside the `namespace` + * because other type signatures may reference them directly (without using the namespace qualifier). + * The AstNamespaceExports behaves the same as AstNamespaceImport, it just also has the inline export for the craeted namespace. + */ + +export class AstNamespaceExport extends AstNamespaceImport { + public constructor(options: IAstNamespaceExportOptions) { + super(options); + } +} diff --git a/apps/api-extractor/src/analyzer/AstNamespaceImport.ts b/apps/api-extractor/src/analyzer/AstNamespaceImport.ts index acb4bd4c578..09304b9efcb 100644 --- a/apps/api-extractor/src/analyzer/AstNamespaceImport.ts +++ b/apps/api-extractor/src/analyzer/AstNamespaceImport.ts @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as ts from 'typescript'; +import type * as ts from 'typescript'; -import { AstModule, AstModuleExportInfo } from './AstModule'; +import type { AstModule, IAstModuleExportInfo } from './AstModule'; import { AstSyntheticEntity } from './AstEntity'; -import { Collector } from '../collector/Collector'; +import type { Collector } from '../collector/Collector'; export interface IAstNamespaceImportOptions { readonly astModule: AstModule; @@ -87,8 +87,8 @@ export class AstNamespaceImport extends AstSyntheticEntity { return this.namespaceName; } - public fetchAstModuleExportInfo(collector: Collector): AstModuleExportInfo { - const astModuleExportInfo: AstModuleExportInfo = collector.astSymbolTable.fetchAstModuleExportInfo( + public fetchAstModuleExportInfo(collector: Collector): IAstModuleExportInfo { + const astModuleExportInfo: IAstModuleExportInfo = collector.astSymbolTable.fetchAstModuleExportInfo( this.astModule ); return astModuleExportInfo; diff --git a/apps/api-extractor/src/analyzer/AstReferenceResolver.ts b/apps/api-extractor/src/analyzer/AstReferenceResolver.ts index 6d91f6c194f..e51de2f1934 100644 --- a/apps/api-extractor/src/analyzer/AstReferenceResolver.ts +++ b/apps/api-extractor/src/analyzer/AstReferenceResolver.ts @@ -2,15 +2,16 @@ // See LICENSE in the project root for license information. import * as ts from 'typescript'; + import * as tsdoc from '@microsoft/tsdoc'; -import { AstSymbolTable } from './AstSymbolTable'; -import { AstEntity } from './AstEntity'; -import { AstDeclaration } from './AstDeclaration'; -import { WorkingPackage } from '../collector/WorkingPackage'; -import { AstModule } from './AstModule'; -import { Collector } from '../collector/Collector'; -import { DeclarationMetadata } from '../collector/DeclarationMetadata'; +import type { AstSymbolTable } from './AstSymbolTable'; +import type { AstEntity } from './AstEntity'; +import type { AstDeclaration } from './AstDeclaration'; +import type { WorkingPackage } from '../collector/WorkingPackage'; +import type { AstModule } from './AstModule'; +import type { Collector } from '../collector/Collector'; +import type { DeclarationMetadata } from '../collector/DeclarationMetadata'; import { AstSymbol } from './AstSymbol'; /** @@ -245,7 +246,7 @@ export class AstReferenceResolver { memberSelector: tsdoc.DocMemberSelector, astSymbolName: string ): AstDeclaration | ResolverFailure { - const selectorOverloadIndex: number = parseInt(memberSelector.selector); + const selectorOverloadIndex: number = parseInt(memberSelector.selector, 10); const matches: AstDeclaration[] = []; for (const astDeclaration of astDeclarations) { diff --git a/apps/api-extractor/src/analyzer/AstSymbol.ts b/apps/api-extractor/src/analyzer/AstSymbol.ts index fcb3269cf36..c8052d13dbb 100644 --- a/apps/api-extractor/src/analyzer/AstSymbol.ts +++ b/apps/api-extractor/src/analyzer/AstSymbol.ts @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as ts from 'typescript'; -import { AstDeclaration } from './AstDeclaration'; +import type * as ts from 'typescript'; + import { InternalError } from '@rushstack/node-core-library'; + +import type { AstDeclaration } from './AstDeclaration'; import { AstEntity } from './AstEntity'; /** diff --git a/apps/api-extractor/src/analyzer/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index cee070753dc..1bfa982b578 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -4,18 +4,19 @@ /* eslint-disable no-bitwise */ // for ts.SymbolFlags import * as ts from 'typescript'; -import { PackageJsonLookup, InternalError } from '@rushstack/node-core-library'; + +import { type PackageJsonLookup, InternalError } from '@rushstack/node-core-library'; import { AstDeclaration } from './AstDeclaration'; import { TypeScriptHelpers } from './TypeScriptHelpers'; import { AstSymbol } from './AstSymbol'; -import { AstModule, AstModuleExportInfo } from './AstModule'; +import type { AstModule, IAstModuleExportInfo } from './AstModule'; import { PackageMetadataManager } from './PackageMetadataManager'; import { ExportAnalyzer } from './ExportAnalyzer'; -import { AstEntity } from './AstEntity'; +import type { AstEntity } from './AstEntity'; import { AstNamespaceImport } from './AstNamespaceImport'; -import { MessageRouter } from '../collector/MessageRouter'; -import { TypeScriptInternals, IGlobalVariableAnalyzer } from './TypeScriptInternals'; +import type { MessageRouter } from '../collector/MessageRouter'; +import { TypeScriptInternals, type IGlobalVariableAnalyzer } from './TypeScriptInternals'; import { SyntaxHelpers } from './SyntaxHelpers'; import { SourceFileLocationFormatter } from './SourceFileLocationFormatter'; @@ -124,7 +125,7 @@ export class AstSymbolTable { /** * This crawls the specified entry point and collects the full set of exported AstSymbols. */ - public fetchAstModuleExportInfo(astModule: AstModule): AstModuleExportInfo { + public fetchAstModuleExportInfo(astModule: AstModule): IAstModuleExportInfo { return this._exportAnalyzer.fetchAstModuleExportInfo(astModule); } @@ -615,9 +616,6 @@ export class AstSymbolTable { // - but P1 and P2 may be different (e.g. merged namespaces containing merged interfaces) // Is there a parent AstSymbol? First we check to see if there is a parent declaration: - const arbitraryDeclaration: ts.Node | undefined = - TypeScriptHelpers.tryGetADeclaration(followedSymbol); - if (arbitraryDeclaration) { const arbitraryParentDeclaration: ts.Node | undefined = this._tryFindFirstAstDeclarationParent(arbitraryDeclaration); diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index 3610f828c07..0bc6236aa4d 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -2,18 +2,20 @@ // See LICENSE in the project root for license information. import * as ts from 'typescript'; + import { InternalError } from '@rushstack/node-core-library'; import { TypeScriptHelpers } from './TypeScriptHelpers'; import { AstSymbol } from './AstSymbol'; -import { AstImport, IAstImportOptions, AstImportKind } from './AstImport'; -import { AstModule, AstModuleExportInfo } from './AstModule'; +import { AstImport, type IAstImportOptions, AstImportKind } from './AstImport'; +import { AstModule, type IAstModuleExportInfo } from './AstModule'; import { TypeScriptInternals } from './TypeScriptInternals'; import { SourceFileLocationFormatter } from './SourceFileLocationFormatter'; -import { IFetchAstSymbolOptions } from './AstSymbolTable'; -import { AstEntity } from './AstEntity'; +import type { IFetchAstSymbolOptions } from './AstSymbolTable'; +import type { AstEntity } from './AstEntity'; import { AstNamespaceImport } from './AstNamespaceImport'; import { SyntaxHelpers } from './SyntaxHelpers'; +import { AstNamespaceExport } from './AstNamespaceExport'; /** * Exposes the minimal APIs from AstSymbolTable that are needed by ExportAnalyzer. @@ -236,15 +238,19 @@ export class ExportAnalyzer { /** * Implementation of {@link AstSymbolTable.fetchAstModuleExportInfo}. */ - public fetchAstModuleExportInfo(entryPointAstModule: AstModule): AstModuleExportInfo { + public fetchAstModuleExportInfo(entryPointAstModule: AstModule): IAstModuleExportInfo { if (entryPointAstModule.isExternal) { throw new Error('fetchAstModuleExportInfo() is not supported for external modules'); } if (entryPointAstModule.astModuleExportInfo === undefined) { - const astModuleExportInfo: AstModuleExportInfo = new AstModuleExportInfo(); + const astModuleExportInfo: IAstModuleExportInfo = { + visitedAstModules: new Set(), + exportedLocalEntities: new Map(), + starExportedExternalModules: new Set() + }; - this._collectAllExportsRecursive(astModuleExportInfo, entryPointAstModule, new Set()); + this._collectAllExportsRecursive(astModuleExportInfo, entryPointAstModule); entryPointAstModule.astModuleExportInfo = astModuleExportInfo; } @@ -259,15 +265,23 @@ export class ExportAnalyzer { importOrExportDeclaration: ts.ImportDeclaration | ts.ExportDeclaration | ts.ImportTypeNode, moduleSpecifier: string ): boolean { - const specifier: ts.TypeNode | ts.Expression | undefined = ts.isImportTypeNode(importOrExportDeclaration) + let specifier: ts.TypeNode | ts.Expression | undefined = ts.isImportTypeNode(importOrExportDeclaration) ? importOrExportDeclaration.argument : importOrExportDeclaration.moduleSpecifier; + if (specifier && ts.isLiteralTypeNode(specifier)) { + specifier = specifier.literal; + } const mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined = specifier && ts.isStringLiteralLike(specifier) - ? TypeScriptInternals.getModeForUsageLocation(importOrExportDeclaration.getSourceFile(), specifier) + ? TypeScriptInternals.getModeForUsageLocation( + importOrExportDeclaration.getSourceFile(), + specifier, + this._program.getCompilerOptions() + ) : undefined; const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptInternals.getResolvedModule( + this._program, importOrExportDeclaration.getSourceFile(), moduleSpecifier, mode @@ -308,18 +322,15 @@ export class ExportAnalyzer { return this._importableAmbientSourceFiles.has(sourceFile); } - private _collectAllExportsRecursive( - astModuleExportInfo: AstModuleExportInfo, - astModule: AstModule, - visitedAstModules: Set - ): void { + private _collectAllExportsRecursive(astModuleExportInfo: IAstModuleExportInfo, astModule: AstModule): void { + const { visitedAstModules, starExportedExternalModules, exportedLocalEntities } = astModuleExportInfo; if (visitedAstModules.has(astModule)) { return; } visitedAstModules.add(astModule); if (astModule.isExternal) { - astModuleExportInfo.starExportedExternalModules.add(astModule); + starExportedExternalModules.add(astModule); } else { // Fetch each of the explicit exports for this module if (astModule.moduleSymbol.exports) { @@ -331,7 +342,7 @@ export class ExportAnalyzer { default: // Don't collect the "export default" symbol unless this is the entry point module if (exportName !== ts.InternalSymbolName.Default || visitedAstModules.size === 1) { - if (!astModuleExportInfo.exportedLocalEntities.has(exportSymbol.name)) { + if (!exportedLocalEntities.has(exportSymbol.name)) { const astEntity: AstEntity = this._getExportOfAstModule(exportSymbol.name, astModule); if (astEntity instanceof AstSymbol && !astEntity.isExternal) { @@ -342,7 +353,7 @@ export class ExportAnalyzer { this._astSymbolTable.analyze(astEntity); } - astModuleExportInfo.exportedLocalEntities.set(exportSymbol.name, astEntity); + exportedLocalEntities.set(exportSymbol.name, astEntity); } } break; @@ -351,7 +362,7 @@ export class ExportAnalyzer { } for (const starExportedModule of astModule.starExportedModules) { - this._collectAllExportsRecursive(astModuleExportInfo, starExportedModule, visitedAstModules); + this._collectAllExportsRecursive(astModuleExportInfo, starExportedModule); } } } @@ -561,11 +572,9 @@ export class ExportAnalyzer { // SemicolonToken: pre=[;] // Issue tracking this feature: https://github.com/microsoft/rushstack/issues/2780 - throw new Error( - `The "export * as ___" syntax is not supported yet; as a workaround,` + - ` use "import * as ___" with a separate "export { ___ }" declaration\n` + - SourceFileLocationFormatter.formatDeclaration(declaration) - ); + + const astModule: AstModule = this._fetchSpecifierAstModule(exportDeclaration, declarationSymbol); + return this._getAstNamespaceExport(astModule, declarationSymbol, declaration); } else { throw new InternalError( `Unimplemented export declaration kind: ${declaration.getText()}\n` + @@ -593,6 +602,25 @@ export class ExportAnalyzer { return undefined; } + private _getAstNamespaceExport( + astModule: AstModule, + declarationSymbol: ts.Symbol, + declaration: ts.Declaration + ): AstNamespaceExport { + const imoprtNamespace: AstNamespaceImport = this._getAstNamespaceImport( + astModule, + declarationSymbol, + declaration + ); + + return new AstNamespaceExport({ + namespaceName: imoprtNamespace.localName, + astModule: astModule, + declaration, + symbol: declarationSymbol + }); + } + private _tryMatchImportDeclaration( declaration: ts.Declaration, declarationSymbol: ts.Symbol @@ -620,18 +648,7 @@ export class ExportAnalyzer { if (externalModulePath === undefined) { const astModule: AstModule = this._fetchSpecifierAstModule(importDeclaration, declarationSymbol); - let namespaceImport: AstNamespaceImport | undefined = - this._astNamespaceImportByModule.get(astModule); - if (namespaceImport === undefined) { - namespaceImport = new AstNamespaceImport({ - namespaceName: declarationSymbol.name, - astModule: astModule, - declaration: declaration, - symbol: declarationSymbol - }); - this._astNamespaceImportByModule.set(astModule, namespaceImport); - } - return namespaceImport; + return this._getAstNamespaceImport(astModule, declarationSymbol, declaration); } // Here importSymbol=undefined because {@inheritDoc} and such are not going to work correctly for @@ -758,6 +775,25 @@ export class ExportAnalyzer { return undefined; } + private _getAstNamespaceImport( + astModule: AstModule, + declarationSymbol: ts.Symbol, + declaration: ts.Declaration + ): AstNamespaceImport { + let namespaceImport: AstNamespaceImport | undefined = this._astNamespaceImportByModule.get(astModule); + if (namespaceImport === undefined) { + namespaceImport = new AstNamespaceImport({ + namespaceName: declarationSymbol.name, + astModule: astModule, + declaration: declaration, + symbol: declarationSymbol + }); + this._astNamespaceImportByModule.set(astModule, namespaceImport); + } + + return namespaceImport; + } + private static _getIsTypeOnly(importDeclaration: ts.ImportDeclaration): boolean { if (importDeclaration.importClause) { return !!importDeclaration.importClause.isTypeOnly; @@ -878,10 +914,12 @@ export class ExportAnalyzer { ts.isStringLiteralLike(importOrExportDeclaration.moduleSpecifier) ? TypeScriptInternals.getModeForUsageLocation( importOrExportDeclaration.getSourceFile(), - importOrExportDeclaration.moduleSpecifier + importOrExportDeclaration.moduleSpecifier, + this._program.getCompilerOptions() ) : undefined; const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptInternals.getResolvedModule( + this._program, importOrExportDeclaration.getSourceFile(), moduleSpecifier, mode diff --git a/apps/api-extractor/src/analyzer/PackageMetadataManager.ts b/apps/api-extractor/src/analyzer/PackageMetadataManager.ts index 3a3e031296d..22b9be7e891 100644 --- a/apps/api-extractor/src/analyzer/PackageMetadataManager.ts +++ b/apps/api-extractor/src/analyzer/PackageMetadataManager.ts @@ -1,18 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; +import path from 'node:path'; + +import semver from 'semver'; import { - PackageJsonLookup, + type PackageJsonLookup, FileSystem, JsonFile, - NewlineKind, - INodePackageJson, - JsonObject + type NewlineKind, + type INodePackageJson, + type JsonObject, + type IPackageJsonExports } from '@rushstack/node-core-library'; + import { Extractor } from '../api/Extractor'; -import { MessageRouter } from '../collector/MessageRouter'; +import type { MessageRouter } from '../collector/MessageRouter'; import { ConsoleMessageId } from '../api/ConsoleMessageId'; /** @@ -42,6 +46,134 @@ export class PackageMetadata { } } +const TSDOC_METADATA_FILENAME: 'tsdoc-metadata.json' = 'tsdoc-metadata.json'; + +/** + * 1. If package.json a `"tsdocMetadata": "./path1/path2/tsdoc-metadata.json"` field + * then that takes precedence. This convention will be rarely needed, since the other rules below generally + * produce a good result. + */ +function _tryResolveTsdocMetadataFromTsdocMetadataField({ + tsdocMetadata +}: INodePackageJson): string | undefined { + return tsdocMetadata; +} + +/** + * 2. If package.json contains a `"exports": { ".": { "types": "./path1/path2/index.d.ts" } }` field, + * then we look for the file under "./path1/path2/tsdoc-metadata.json" + * + * This always looks for a "." and then a "*" entry in the exports field, and then evaluates for + * a "types" field in that entry. + */ + +function _tryResolveTsdocMetadataFromExportsField({ exports }: INodePackageJson): string | undefined { + switch (typeof exports) { + case 'string': { + return `${path.dirname(exports)}/${TSDOC_METADATA_FILENAME}`; + } + + case 'object': { + if (Array.isArray(exports)) { + const [firstExport] = exports; + // Take the first entry in the array + if (firstExport) { + return `${path.dirname(exports[0])}/${TSDOC_METADATA_FILENAME}`; + } + } else { + const rootExport: IPackageJsonExports | string | null | undefined = exports['.'] ?? exports['*']; + switch (typeof rootExport) { + case 'string': { + return `${path.dirname(rootExport)}/${TSDOC_METADATA_FILENAME}`; + } + + case 'object': { + let typesExport: IPackageJsonExports | string | undefined = rootExport?.types; + while (typesExport) { + switch (typeof typesExport) { + case 'string': { + return `${path.dirname(typesExport)}/${TSDOC_METADATA_FILENAME}`; + } + + case 'object': { + typesExport = typesExport?.types; + break; + } + } + } + } + } + } + break; + } + } +} + +/** + * 3. If package.json contains a `typesVersions` field, look for the version + * matching the highest minimum version that either includes a "." or "*" entry. + */ +function _tryResolveTsdocMetadataFromTypesVersionsField({ + typesVersions +}: INodePackageJson): string | undefined { + if (typesVersions) { + let highestMinimumMatchingSemver: semver.SemVer | undefined; + let latestMatchingPath: string | undefined; + for (const [version, paths] of Object.entries(typesVersions)) { + let range: semver.Range; + try { + range = new semver.Range(version); + } catch { + continue; + } + + const minimumMatchingSemver: semver.SemVer | null = semver.minVersion(range); + if ( + minimumMatchingSemver && + (!highestMinimumMatchingSemver || semver.gt(minimumMatchingSemver, highestMinimumMatchingSemver)) + ) { + const pathEntry: string[] | undefined = paths['.'] ?? paths['*']; + const firstPath: string | undefined = pathEntry?.[0]; + if (firstPath) { + highestMinimumMatchingSemver = minimumMatchingSemver; + latestMatchingPath = firstPath; + } + } + } + + if (latestMatchingPath) { + return `${path.dirname(latestMatchingPath)}/${TSDOC_METADATA_FILENAME}`; + } + } +} + +/** + * 4. If package.json contains a `"types": "./path1/path2/index.d.ts"` or a `"typings": "./path1/path2/index.d.ts"` + * field, then we look for the file under "./path1/path2/tsdoc-metadata.json". + * + * @remarks + * `types` takes precedence over `typings`. + */ +function _tryResolveTsdocMetadataFromTypesOrTypingsFields({ + typings, + types +}: INodePackageJson): string | undefined { + const typesField: string | undefined = types ?? typings; + if (typesField) { + return `${path.dirname(typesField)}/${TSDOC_METADATA_FILENAME}`; + } +} + +/** + * 5. If package.json contains a `"main": "./path1/path2/index.js"` field, then we look for the file under + * "./path1/path2/tsdoc-metadata.json". + */ +function _tryResolveTsdocMetadataFromMainField({ main }: INodePackageJson): string | undefined { + if (main) { + return `${path.dirname(main)}/${TSDOC_METADATA_FILENAME}`; + } +} + /** * This class maintains a cache of analyzed information obtained from package.json * files. It is built on top of the PackageJsonLookup class. @@ -56,7 +188,7 @@ export class PackageMetadata { * Use ts.program.isSourceFileFromExternalLibrary() to test source files before passing the to PackageMetadataManager. */ export class PackageMetadataManager { - public static tsdocMetadataFilename: string = 'tsdoc-metadata.json'; + public static tsdocMetadataFilename: string = TSDOC_METADATA_FILENAME; private readonly _packageJsonLookup: PackageJsonLookup; private readonly _messageRouter: MessageRouter; @@ -70,37 +202,30 @@ export class PackageMetadataManager { this._messageRouter = messageRouter; } - // This feature is still being standardized: https://github.com/microsoft/tsdoc/issues/7 - // In the future we will use the @microsoft/tsdoc library to read this file. + /** + * This feature is still being standardized: https://github.com/microsoft/tsdoc/issues/7 + * In the future we will use the @microsoft/tsdoc library to read this file. + */ private static _resolveTsdocMetadataPathFromPackageJson( packageFolder: string, packageJson: INodePackageJson ): string { - const tsdocMetadataFilename: string = PackageMetadataManager.tsdocMetadataFilename; - - let tsdocMetadataRelativePath: string; - - if (packageJson.tsdocMetadata) { - // 1. If package.json contains a field such as "tsdocMetadata": "./path1/path2/tsdoc-metadata.json", - // then that takes precedence. This convention will be rarely needed, since the other rules below generally - // produce a good result. - tsdocMetadataRelativePath = packageJson.tsdocMetadata; - } else if (packageJson.typings) { - // 2. If package.json contains a field such as "typings": "./path1/path2/index.d.ts", then we look - // for the file under "./path1/path2/tsdoc-metadata.json" - tsdocMetadataRelativePath = path.join(path.dirname(packageJson.typings), tsdocMetadataFilename); - } else if (packageJson.main) { - // 3. If package.json contains a field such as "main": "./path1/path2/index.js", then we look for - // the file under "./path1/path2/tsdoc-metadata.json" - tsdocMetadataRelativePath = path.join(path.dirname(packageJson.main), tsdocMetadataFilename); - } else { - // 4. If none of the above rules apply, then by default we look for the file under "./tsdoc-metadata.json" - // since the default entry point is "./index.js" - tsdocMetadataRelativePath = tsdocMetadataFilename; - } + const tsdocMetadataRelativePath: string = + _tryResolveTsdocMetadataFromTsdocMetadataField(packageJson) ?? + _tryResolveTsdocMetadataFromExportsField(packageJson) ?? + _tryResolveTsdocMetadataFromTypesVersionsField(packageJson) ?? + _tryResolveTsdocMetadataFromTypesOrTypingsFields(packageJson) ?? + _tryResolveTsdocMetadataFromMainField(packageJson) ?? + // As a final fallback, place the file in the root of the package. + TSDOC_METADATA_FILENAME; // Always resolve relative to the package folder. - const tsdocMetadataPath: string = path.resolve(packageFolder, tsdocMetadataRelativePath); + const tsdocMetadataPath: string = path.resolve( + packageFolder, + // This non-null assertion is safe because the last entry in TSDOC_METADATA_RESOLUTION_FUNCTIONS + // returns a non-undefined value. + tsdocMetadataRelativePath! + ); return tsdocMetadataPath; } @@ -117,6 +242,7 @@ export class PackageMetadataManager { if (tsdocMetadataPath) { return path.resolve(packageFolder, tsdocMetadataPath); } + return PackageMetadataManager._resolveTsdocMetadataPathFromPackageJson(packageFolder, packageJson); } diff --git a/apps/api-extractor/src/analyzer/SourceFileLocationFormatter.ts b/apps/api-extractor/src/analyzer/SourceFileLocationFormatter.ts index 3521334e915..939f9aa5ba2 100644 --- a/apps/api-extractor/src/analyzer/SourceFileLocationFormatter.ts +++ b/apps/api-extractor/src/analyzer/SourceFileLocationFormatter.ts @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as ts from 'typescript'; -import * as path from 'path'; +import * as path from 'node:path'; + +import type * as ts from 'typescript'; + import { Path, Text } from '@rushstack/node-core-library'; export interface ISourceFileLocationFormatOptions { diff --git a/apps/api-extractor/src/analyzer/Span.ts b/apps/api-extractor/src/analyzer/Span.ts index 06afaf7b1a3..e4d1f2970ab 100644 --- a/apps/api-extractor/src/analyzer/Span.ts +++ b/apps/api-extractor/src/analyzer/Span.ts @@ -2,7 +2,8 @@ // See LICENSE in the project root for license information. import * as ts from 'typescript'; -import { InternalError, Sort } from '@rushstack/node-core-library'; + +import { InternalError, Sort, Text } from '@rushstack/node-core-library'; import { IndentedWriter } from '../generators/IndentedWriter'; @@ -637,12 +638,7 @@ export class Span { } private _getTrimmed(text: string): string { - const trimmed: string = text.replace(/\r?\n/g, '\\n'); - - if (trimmed.length > 100) { - return trimmed.substr(0, 97) + '...'; - } - return trimmed; + return Text.truncateWithEllipsis(Text.convertToLf(text), 100); } private _getSubstring(startIndex: number, endIndex: number): string { diff --git a/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts index d63e3ffe8d1..5c28b30d354 100644 --- a/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts +++ b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts @@ -4,9 +4,11 @@ /* eslint-disable no-bitwise */ import * as ts from 'typescript'; + +import { InternalError } from '@rushstack/node-core-library'; + import { SourceFileLocationFormatter } from './SourceFileLocationFormatter'; import { TypeScriptInternals } from './TypeScriptInternals'; -import { InternalError } from '@rushstack/node-core-library'; export class TypeScriptHelpers { // Matches TypeScript's encoded names for well-known ECMAScript symbols like diff --git a/apps/api-extractor/src/analyzer/TypeScriptInternals.ts b/apps/api-extractor/src/analyzer/TypeScriptInternals.ts index e8b74f8844a..00d04aa07fe 100644 --- a/apps/api-extractor/src/analyzer/TypeScriptInternals.ts +++ b/apps/api-extractor/src/analyzer/TypeScriptInternals.ts @@ -4,6 +4,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as ts from 'typescript'; + import { InternalError } from '@rushstack/node-core-library'; /** @@ -17,7 +18,7 @@ export class TypeScriptInternals { public static getImmediateAliasedSymbol(symbol: ts.Symbol, typeChecker: ts.TypeChecker): ts.Symbol { // Compiler internal: // https://github.com/microsoft/TypeScript/blob/v3.2.2/src/compiler/checker.ts - return (typeChecker as any).getImmediateAliasedSymbol(symbol); // eslint-disable-line @typescript-eslint/no-explicit-any + return (typeChecker as any).getImmediateAliasedSymbol(symbol); } /** @@ -82,27 +83,33 @@ export class TypeScriptInternals { * The compiler populates this cache as part of analyzing the source file. */ public static getResolvedModule( + program: ts.Program, sourceFile: ts.SourceFile, moduleNameText: string, mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined ): ts.ResolvedModuleFull | undefined { // Compiler internal: - // https://github.com/microsoft/TypeScript/blob/v4.7.2/src/compiler/utilities.ts#L161 - - return (ts as any).getResolvedModule(sourceFile, moduleNameText, mode); + // https://github.com/microsoft/TypeScript/blob/v5.3.3/src/compiler/types.ts#L4698 + const result: ts.ResolvedModuleWithFailedLookupLocations | undefined = (program as any).getResolvedModule( + sourceFile, + moduleNameText, + mode + ); + return result?.resolvedModule; } /** * Gets the mode required for module resolution required with the addition of Node16/nodenext */ public static getModeForUsageLocation( - file: { impliedNodeFormat?: ts.SourceFile['impliedNodeFormat'] }, - usage: ts.StringLiteralLike | undefined + file: ts.SourceFile, + usage: ts.StringLiteralLike, + compilerOptions: ts.CompilerOptions ): ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined { // Compiler internal: - // https://github.com/microsoft/TypeScript/blob/v4.7.2/src/compiler/program.ts#L568 + // https://github.com/microsoft/TypeScript/blob/v5.8.2/src/compiler/program.ts#L931 - return (ts as any).getModeForUsageLocation?.(file, usage); + return ts.getModeForUsageLocation?.(file, usage, compilerOptions); } /** diff --git a/apps/api-extractor/src/analyzer/test/PackageMetadataManager.test.ts b/apps/api-extractor/src/analyzer/test/PackageMetadataManager.test.ts index d6cf4cf4c82..7f5acef9c2f 100644 --- a/apps/api-extractor/src/analyzer/test/PackageMetadataManager.test.ts +++ b/apps/api-extractor/src/analyzer/test/PackageMetadataManager.test.ts @@ -1,34 +1,23 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; -import { PackageMetadataManager } from '../PackageMetadataManager'; -import { FileSystem, PackageJsonLookup, INodePackageJson, NewlineKind } from '@rushstack/node-core-library'; - -const packageJsonLookup: PackageJsonLookup = new PackageJsonLookup(); - -function resolveInTestPackage(testPackageName: string, ...args: string[]): string { - return path.resolve(__dirname, 'test-data/tsdoc-metadata-path-inference', testPackageName, ...args); -} +jest.mock('node:path', () => { + const actualPath: typeof import('path') = jest.requireActual('node:path'); + return { + ...actualPath, + resolve: actualPath.posix.resolve + }; +}); -function getPackageMetadata(testPackageName: string): { - packageFolder: string; - packageJson: INodePackageJson; -} { - const packageFolder: string = resolveInTestPackage(testPackageName); - const packageJson: INodePackageJson | undefined = packageJsonLookup.tryLoadPackageJsonFor(packageFolder); - if (!packageJson) { - throw new Error('There should be a package.json file in the test package'); - } - return { packageFolder, packageJson }; -} +import { PackageMetadataManager } from '../PackageMetadataManager'; +import { FileSystem, type INodePackageJson, NewlineKind } from '@rushstack/node-core-library'; // eslint-disable-next-line @typescript-eslint/no-explicit-any function firstArgument(mockFn: jest.Mock): any { return mockFn.mock.calls[0][0]; } -/* eslint-disable @typescript-eslint/typedef */ +const PACKAGE_FOLDER: '/pkg' = '/pkg'; describe(PackageMetadataManager.name, () => { describe(PackageMetadataManager.writeTsdocMetadataFile.name, () => { @@ -37,9 +26,11 @@ describe(PackageMetadataManager.name, () => { beforeAll(() => { FileSystem.writeFile = mockWriteFile; }); + afterEach(() => { mockWriteFile.mockClear(); }); + afterAll(() => { FileSystem.writeFile = originalWriteFile; }); @@ -51,77 +42,403 @@ describe(PackageMetadataManager.name, () => { }); describe(PackageMetadataManager.resolveTsdocMetadataPath.name, () => { - describe('when an empty tsdocMetadataPath is provided', () => { - const tsdocMetadataPath: string = ''; + describe.each([ + { + tsdocMetadataPath: '', + label: 'when an empty tsdocMetadataPath is provided' + }, + { + tsdocMetadataPath: 'path/to/custom-tsdoc-metadata.json', + label: 'when a non-empty tsdocMetadataPath is provided', + itValue: + 'outputs the tsdoc metadata file at the provided path in the folder where package.json is located', + overrideExpected: `${PACKAGE_FOLDER}/path/to/custom-tsdoc-metadata.json` + } + ])('$label', ({ tsdocMetadataPath, itValue, overrideExpected }) => { + function testForPackageJson( + packageJson: INodePackageJson, + options: + | { expectsPackageRoot: true } + | { + expectedPathInsidePackage: string; + } + ): void { + const { expectsPackageRoot, expectedPathInsidePackage } = options as { + expectsPackageRoot: true; + } & { + expectedPathInsidePackage: string; + }; + const resolvedTsdocMetadataPath: string = PackageMetadataManager.resolveTsdocMetadataPath( + PACKAGE_FOLDER, + packageJson, + tsdocMetadataPath + ); + if (overrideExpected) { + expect(resolvedTsdocMetadataPath).toBe(overrideExpected); + } else if (expectsPackageRoot) { + expect(resolvedTsdocMetadataPath).toBe(`${PACKAGE_FOLDER}/tsdoc-metadata.json`); + } else { + expect(resolvedTsdocMetadataPath).toBe( + `${PACKAGE_FOLDER}/${expectedPathInsidePackage}/tsdoc-metadata.json` + ); + } + } + describe('given a package.json where the field "tsdocMetadata" is defined', () => { - it('outputs the tsdoc metadata path as given by "tsdocMetadata" relative to the folder of package.json', () => { - const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-tsdoc-metadata'); - expect( - PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath) - ).toBe(path.resolve(packageFolder, packageJson.tsdocMetadata as string)); - }); + it( + itValue ?? + 'outputs the tsdoc metadata path as given by "tsdocMetadata" relative to the folder of package.json', + () => { + testForPackageJson( + { + name: 'package-inferred-from-tsdoc-metadata', + version: '1.0.0', + main: 'path/to/main.js', + typings: 'path/to/typings/typings.d.ts', + tsdocMetadata: 'path/to/tsdoc-metadata/tsdoc-metadata.json' + }, + { + expectedPathInsidePackage: 'path/to/tsdoc-metadata' + } + ); + } + ); }); - describe('given a package.json where the field "typings" is defined and "tsdocMetadata" is not defined', () => { - it('outputs the tsdoc metadata file "tsdoc-metadata.json" in the same folder as the path of "typings"', () => { - const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-typings'); - expect( - PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath) - ).toBe(path.resolve(packageFolder, path.dirname(packageJson.typings!), 'tsdoc-metadata.json')); + + describe('given a package.json where the field "exports" is defined', () => { + describe('with a string value', () => { + testForPackageJson( + { + name: 'package-inferred-from-exports', + version: '1.0.0', + exports: 'path/to/exports/exports.js' + }, + { expectedPathInsidePackage: 'path/to/exports' } + ); }); - }); - describe('given a package.json where the field "main" is defined but not "typings" nor "tsdocMetadata"', () => { - it('outputs the tsdoc metadata file "tsdoc-metadata.json" in the same folder as the path of "main"', () => { - const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-main'); - expect( - PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath) - ).toBe(path.resolve(packageFolder, path.dirname(packageJson.main!), 'tsdoc-metadata.json')); + + describe('with an array value', () => { + testForPackageJson( + { + name: 'package-inferred-from-exports', + version: '1.0.0', + exports: ['path/to/exports/exports.js', 'path/to/exports2/exports.js'] + }, + { expectedPathInsidePackage: 'path/to/exports' } + ); }); - }); - describe('given a package.json where the fields "main", "typings" and "tsdocMetadata" are not defined', () => { - it('outputs the tsdoc metadata file "tsdoc-metadata.json" in the folder where package.json is located', () => { - const { packageFolder, packageJson } = getPackageMetadata('package-default'); - expect( - PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath) - ).toBe(path.resolve(packageFolder, 'tsdoc-metadata.json')); + + describe.each(['.', '*'])('with an exports field that contains a "%s" key', (exportsKey) => { + describe('with a string value', () => { + it( + itValue ?? + `outputs the tsdoc metadata file "tsdoc-metadata.json" in the same folder as the path of "${exportsKey}"`, + () => { + testForPackageJson( + { + name: 'package-inferred-from-exports', + version: '1.0.0', + exports: { + [exportsKey]: 'path/to/exports/exports.js' + } + }, + { expectedPathInsidePackage: 'path/to/exports' } + ); + } + ); + }); + + describe('with an object value that does not include a "types" key', () => { + it(itValue ?? 'outputs the tsdoc metadata file "tsdoc-metadata.json" in the package root', () => { + testForPackageJson( + { + name: 'package-inferred-from-exports', + version: '1.0.0', + exports: { + [exportsKey]: { + import: 'path/to/exports/exports.js' + } + } + }, + { expectsPackageRoot: true } + ); + }); + }); + + describe('with an object value that does include a "types" key', () => { + it( + itValue ?? + `outputs the tsdoc metadata file "tsdoc-metadata.json" in the same folder as the path of "${exportsKey}"`, + () => { + testForPackageJson( + { + name: 'package-inferred-from-exports', + version: '1.0.0', + exports: { + [exportsKey]: { + types: 'path/to/types-exports/exports.d.ts' + } + } + }, + { expectedPathInsidePackage: 'path/to/types-exports' } + ); + } + ); + }); + + describe('that nests into an object that doesn\'t contain a "types" key', () => { + it(itValue ?? 'outputs the tsdoc metadata file "tsdoc-metadata.json" package root', () => { + testForPackageJson( + { + name: 'package-inferred-from-exports', + version: '1.0.0', + exports: { + [exportsKey]: { + types: { + import: 'path/to/types-exports/exports.js' + } + } + } + }, + { expectsPackageRoot: true } + ); + }); + }); + + describe('that nests into an object that contains a "types" key', () => { + it(itValue ?? 'outputs the tsdoc metadata file "tsdoc-metadata.json" package root', () => { + testForPackageJson( + { + name: 'package-inferred-from-exports', + version: '1.0.0', + exports: { + [exportsKey]: { + types: { + types: 'path/to/types-exports/exports.d.ts' + } + } + } + }, + { expectedPathInsidePackage: 'path/to/types-exports' } + ); + }); + }); }); }); - }); - describe('when a non-empty tsdocMetadataPath is provided', () => { - const tsdocMetadataPath: string = 'path/to/custom-tsdoc-metadata.json'; - describe('given a package.json where the field "tsdocMetadata" is defined', () => { - it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => { - const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-tsdocMetadata'); - expect( - PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath) - ).toBe(path.resolve(packageFolder, tsdocMetadataPath)); + + describe('given a package.json where the field "typesVersions" is defined', () => { + describe('with an exports field that contains a "%s" key', () => { + describe('with no selectors', () => { + it(itValue ?? 'outputs the tsdoc metadata file "tsdoc-metadata.json" in the package root', () => { + testForPackageJson( + { + name: 'package-inferred-from-typesVersions', + version: '1.0.0', + typesVersions: {} + }, + { expectsPackageRoot: true } + ); + }); + }); + + describe.each(['.', '*'])('with a %s selector', (pathSelector) => { + it( + itValue ?? 'outputs the tsdoc metadata file "tsdoc-metadata.json" in the path selected', + () => { + testForPackageJson( + { + name: 'package-inferred-from-typesVersions', + version: '1.0.0', + typesVersions: { + '>=3.0': { + [pathSelector]: ['path/to/types-exports/exports.d.ts'] + } + } + }, + { expectedPathInsidePackage: 'path/to/types-exports' } + ); + } + ); + }); + + describe('with multiple TypeScript versions', () => { + describe.each(['.', '*'])('with a %s selector', (pathSelector) => { + it( + itValue ?? + 'outputs the tsdoc metadata file "tsdoc-metadata.json" in the path selected for the latest TypeScript version', + () => { + testForPackageJson( + { + name: 'package-inferred-from-typesVersions', + version: '1.0.0', + typesVersions: { + '>=3.6': { + [pathSelector]: ['path/to/types-exports-3.6/exports.d.ts'] + }, + '>=3.0': { + [pathSelector]: ['path/to/types-exports-3.0/exports.d.ts'] + }, + '~4.0': { + [pathSelector]: ['path/to/types-exports-4.0/exports.d.ts'] + } + } + }, + { expectedPathInsidePackage: 'path/to/types-exports-4.0' } + ); + } + ); + }); + }); }); }); - describe('given a package.json where the field "typings" is defined and "tsdocMetadata" is not defined', () => { - it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => { - const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-typings'); - expect( - PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath) - ).toBe(path.resolve(packageFolder, tsdocMetadataPath)); - }); + + describe('given a package.json where the field "types" is defined', () => { + it( + itValue ?? + 'outputs the tsdoc metadata file "tsdoc-metadata.json" in the same folder as the path of "types"', + () => { + testForPackageJson( + { + name: 'package-inferred-from-types', + version: '1.0.0', + main: 'path/to/main.js', + types: 'path/to/types/types.d.ts' + }, + { expectedPathInsidePackage: 'path/to/types' } + ); + } + ); }); - describe('given a package.json where the field "main" is defined but not "typings" nor "tsdocMetadata"', () => { - it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => { - const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-main'); - expect( - PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath) - ).toBe(path.resolve(packageFolder, tsdocMetadataPath)); - }); + + describe('given a package.json where the field "types" and "typings" are defined', () => { + it( + itValue ?? + 'outputs the tsdoc metadata file "tsdoc-metadata.json" in the same folder as the path of "types"', + () => { + testForPackageJson( + { + name: 'package-inferred-from-types', + version: '1.0.0', + main: 'path/to/main.js', + types: 'path/to/types/types.d.ts', + typings: 'path/to/typings/typings.d.ts' + }, + { expectedPathInsidePackage: 'path/to/types' } + ); + } + ); }); - describe('given a package.json where the fields "main", "typings" and "tsdocMetadata" are not defined', () => { - it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => { - const { packageFolder, packageJson } = getPackageMetadata('package-default'); - expect( - PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath) - ).toBe(path.resolve(packageFolder, tsdocMetadataPath)); - }); + + describe('given a package.json where the field "typings" is defined and "types" is not defined', () => { + it( + itValue ?? + 'outputs the tsdoc metadata file "tsdoc-metadata.json" in the same folder as the path of "typings"', + () => { + testForPackageJson( + { + name: 'package-inferred-from-typings', + version: '1.0.0', + main: 'path/to/main.js', + typings: 'path/to/typings/typings.d.ts' + }, + { expectedPathInsidePackage: 'path/to/typings' } + ); + } + ); }); + + describe('given a package.json where the field "main" is defined', () => { + it( + itValue ?? + 'outputs the tsdoc metadata file "tsdoc-metadata.json" in the same folder as the path of "main"', + () => { + testForPackageJson( + { + name: 'package-inferred-from-main', + version: '1.0.0', + main: 'path/to/main/main.js' + }, + { expectedPathInsidePackage: 'path/to/main' } + ); + } + ); + }); + + describe( + 'given a package.json where the fields "exports", "typesVersions", "types", "main", "typings" ' + + 'and "tsdocMetadata" are not defined', + () => { + it( + itValue ?? + 'outputs the tsdoc metadata file "tsdoc-metadata.json" in the folder where package.json is located', + () => { + testForPackageJson( + { + name: 'package-default', + version: '1.0.0' + }, + { expectsPackageRoot: true } + ); + } + ); + } + ); + }); + + it('correctly resolves the tsdoc-metadata with the right precedence', () => { + const packageJson: INodePackageJson = { + name: 'package-inferred-tsdoc-metadata', + tsdocMetadata: 'path/to/tsdoc-metadata/tsdoc-metadata.json', + exports: { + '.': 'path/to/exports-dot/exports.js', + '*': 'path/to/exports-star/*.js' + }, + typesVersions: { + '>=3.0': { + '.': ['path/to/typesVersions-dot/exports.d.ts'], + '*': ['path/to/typesVersions-star/*.d.ts'] + } + }, + types: 'path/to/types/types.d.ts', + typings: 'path/to/typings/typings.d.ts', + main: 'path/to/main/main.js' + }; + + expect(PackageMetadataManager.resolveTsdocMetadataPath(PACKAGE_FOLDER, packageJson)).toBe( + `${PACKAGE_FOLDER}/path/to/tsdoc-metadata/tsdoc-metadata.json` + ); + delete packageJson.tsdocMetadata; + expect(PackageMetadataManager.resolveTsdocMetadataPath(PACKAGE_FOLDER, packageJson)).toBe( + `${PACKAGE_FOLDER}/path/to/exports-dot/tsdoc-metadata.json` + ); + delete (packageJson.exports as { '.': unknown })!['.']; + expect(PackageMetadataManager.resolveTsdocMetadataPath(PACKAGE_FOLDER, packageJson)).toBe( + `${PACKAGE_FOLDER}/path/to/exports-star/tsdoc-metadata.json` + ); + delete packageJson.exports; + expect(PackageMetadataManager.resolveTsdocMetadataPath(PACKAGE_FOLDER, packageJson)).toBe( + `${PACKAGE_FOLDER}/path/to/typesVersions-dot/tsdoc-metadata.json` + ); + delete packageJson.typesVersions!['>=3.0']!['.']; + expect(PackageMetadataManager.resolveTsdocMetadataPath(PACKAGE_FOLDER, packageJson)).toBe( + `${PACKAGE_FOLDER}/path/to/typesVersions-star/tsdoc-metadata.json` + ); + delete packageJson.typesVersions; + expect(PackageMetadataManager.resolveTsdocMetadataPath(PACKAGE_FOLDER, packageJson)).toBe( + `${PACKAGE_FOLDER}/path/to/types/tsdoc-metadata.json` + ); + delete packageJson.types; + expect(PackageMetadataManager.resolveTsdocMetadataPath(PACKAGE_FOLDER, packageJson)).toBe( + `${PACKAGE_FOLDER}/path/to/typings/tsdoc-metadata.json` + ); + delete packageJson.typings; + expect(PackageMetadataManager.resolveTsdocMetadataPath(PACKAGE_FOLDER, packageJson)).toBe( + `${PACKAGE_FOLDER}/path/to/main/tsdoc-metadata.json` + ); + delete packageJson.main; + expect(PackageMetadataManager.resolveTsdocMetadataPath(PACKAGE_FOLDER, packageJson)).toBe( + `${PACKAGE_FOLDER}/tsdoc-metadata.json` + ); }); }); }); - -/* eslint-enable @typescript-eslint/typedef */ diff --git a/apps/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-default/package.json b/apps/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-default/package.json deleted file mode 100644 index 1a58930cee9..00000000000 --- a/apps/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-default/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "package-default", - "version": "1.0.0" -} diff --git a/apps/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-main/package.json b/apps/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-main/package.json deleted file mode 100644 index 30a7f604a96..00000000000 --- a/apps/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-main/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "package-inferred-from-main", - "version": "1.0.0", - "main": "path/to/main.js" -} diff --git a/apps/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-tsdoc-metadata/package.json b/apps/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-tsdoc-metadata/package.json deleted file mode 100644 index fbb048f47ef..00000000000 --- a/apps/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-tsdoc-metadata/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "package-inferred-from-tsdoc-metadata", - "version": "1.0.0", - "main": "path/to/main.js", - "typings": "path/to/typings.d.ts", - "tsdocMetadata": "path/to/tsdoc-metadata.json" -} diff --git a/apps/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-typings/package.json b/apps/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-typings/package.json deleted file mode 100644 index bb73979d054..00000000000 --- a/apps/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-typings/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "package-inferred-from-typings", - "version": "1.0.0", - "main": "path/to/main.js", - "typings": "path/to/typings.d.ts" -} diff --git a/apps/api-extractor/src/api/CompilerState.ts b/apps/api-extractor/src/api/CompilerState.ts index fa2d78bef02..7862c1c2e85 100644 --- a/apps/api-extractor/src/api/CompilerState.ts +++ b/apps/api-extractor/src/api/CompilerState.ts @@ -1,14 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; +import * as path from 'node:path'; + import * as ts from 'typescript'; -import colors = require('colors'); import { JsonFile } from '@rushstack/node-core-library'; +import { Colorize } from '@rushstack/terminal'; import { ExtractorConfig } from './ExtractorConfig'; -import { IExtractorInvokeOptions } from './Extractor'; +import type { IExtractorInvokeOptions } from './Extractor'; /** * Options for {@link CompilerState.create} @@ -60,13 +61,22 @@ export class CompilerState { if (!commandLine.options.skipLibCheck && extractorConfig.skipLibCheck) { commandLine.options.skipLibCheck = true; console.log( - colors.cyan( + Colorize.cyan( 'API Extractor was invoked with skipLibCheck. This is not recommended and may cause ' + 'incorrect type analysis.' ) ); } + // Delete outDir and declarationDir to prevent TypeScript from redirecting self-package + // imports to source files. When these options are set, TypeScript's module resolution + // tries to map output .d.ts files back to their source .ts files to avoid analyzing + // build outputs during compilation. However, API Extractor specifically wants to analyze + // the .d.ts build artifacts, not the source files. Since API Extractor doesn't emit any + // files, these options are unnecessary and interfere with correct module resolution. + delete commandLine.options.outDir; + delete commandLine.options.declarationDir; + const inputFilePaths: string[] = commandLine.fileNames.concat(extractorConfig.mainEntryPointFilePath); if (options && options.additionalEntryPoints) { inputFilePaths.push(...options.additionalEntryPoints); diff --git a/apps/api-extractor/src/api/ConsoleMessageId.ts b/apps/api-extractor/src/api/ConsoleMessageId.ts index 8c02dabc0b9..5d1345a2093 100644 --- a/apps/api-extractor/src/api/ConsoleMessageId.ts +++ b/apps/api-extractor/src/api/ConsoleMessageId.ts @@ -11,7 +11,7 @@ * * @public */ -export const enum ConsoleMessageId { +export enum ConsoleMessageId { /** * "Analysis will use the bundled TypeScript version ___" */ @@ -43,6 +43,11 @@ export const enum ConsoleMessageId { */ WritingDtsRollup = 'console-writing-dts-rollup', + /** + * "Generating ___ API report: ___" + */ + WritingApiReport = 'console-writing-api-report', + /** * "You have changed the public API signature for this project. * Please copy the file ___ to ___, or perform a local build (which does this automatically). @@ -56,6 +61,12 @@ export const enum ConsoleMessageId { */ ApiReportNotCopied = 'console-api-report-not-copied', + /** + * Changes to the API report: + * ___ + */ + ApiReportDiff = 'console-api-report-diff', + /** * "You have changed the public API signature for this project. Updating ___" */ diff --git a/apps/api-extractor/src/api/Extractor.ts b/apps/api-extractor/src/api/Extractor.ts index 6059fd9756f..b48a323b094 100644 --- a/apps/api-extractor/src/api/Extractor.ts +++ b/apps/api-extractor/src/api/Extractor.ts @@ -1,33 +1,35 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; +import * as path from 'node:path'; + import * as semver from 'semver'; import * as ts from 'typescript'; import * as resolve from 'resolve'; + +import type { ApiPackage } from '@microsoft/api-extractor-model'; +import { TSDocConfigFile } from '@microsoft/tsdoc-config'; import { FileSystem, NewlineKind, PackageJsonLookup, - IPackageJson, - INodePackageJson, + type IPackageJson, + type INodePackageJson, Path } from '@rushstack/node-core-library'; -import { ExtractorConfig } from './ExtractorConfig'; +import { ExtractorConfig, type IExtractorConfigApiReport } from './ExtractorConfig'; import { Collector } from '../collector/Collector'; import { DtsRollupGenerator, DtsRollupKind } from '../generators/DtsRollupGenerator'; import { ApiModelGenerator } from '../generators/ApiModelGenerator'; -import { ApiPackage } from '@microsoft/api-extractor-model'; import { ApiReportGenerator } from '../generators/ApiReportGenerator'; import { PackageMetadataManager } from '../analyzer/PackageMetadataManager'; import { ValidationEnhancer } from '../enhancers/ValidationEnhancer'; import { DocCommentEnhancer } from '../enhancers/DocCommentEnhancer'; import { CompilerState } from './CompilerState'; -import { ExtractorMessage } from './ExtractorMessage'; +import type { ExtractorMessage } from './ExtractorMessage'; import { MessageRouter } from '../collector/MessageRouter'; import { ConsoleMessageId } from './ConsoleMessageId'; -import { TSDocConfigFile } from '@microsoft/tsdoc-config'; import { SourceMapper } from '../collector/SourceMapper'; /** @@ -89,6 +91,15 @@ export interface IExtractorInvokeOptions { * the STDERR/STDOUT console. */ messageCallback?: (message: ExtractorMessage) => void; + + /** + * If true, then any differences between the actual and expected API reports will be + * printed on the console. + * + * @remarks + * The diff is not printed if the expected API report file has not been created yet. + */ + printApiReportDiff?: boolean; } /** @@ -190,36 +201,52 @@ export class Extractor { * Invoke API Extractor using an already prepared `ExtractorConfig` object. */ public static invoke(extractorConfig: ExtractorConfig, options?: IExtractorInvokeOptions): ExtractorResult { - if (!options) { - options = {}; - } - - const localBuild: boolean = options.localBuild || false; - - let compilerState: CompilerState | undefined; - if (options.compilerState) { - compilerState = options.compilerState; - } else { - compilerState = CompilerState.create(extractorConfig, options); - } + const { + packageFolder, + messages, + tsdocConfiguration, + tsdocConfigFile: { filePath: tsdocConfigFilePath, fileNotFound: tsdocConfigFileNotFound }, + apiJsonFilePath, + newlineKind, + reportTempFolder, + reportFolder, + apiReportEnabled, + reportConfigs, + testMode, + rollupEnabled, + publicTrimmedFilePath, + alphaTrimmedFilePath, + betaTrimmedFilePath, + untrimmedFilePath, + tsdocMetadataEnabled, + tsdocMetadataFilePath + } = extractorConfig; + const { + localBuild = false, + compilerState = CompilerState.create(extractorConfig, options), + messageCallback, + showVerboseMessages = false, + showDiagnostics = false, + printApiReportDiff = false + } = options ?? {}; const sourceMapper: SourceMapper = new SourceMapper(); const messageRouter: MessageRouter = new MessageRouter({ - workingPackageFolder: extractorConfig.packageFolder, - messageCallback: options.messageCallback, - messagesConfig: extractorConfig.messages || {}, - showVerboseMessages: !!options.showVerboseMessages, - showDiagnostics: !!options.showDiagnostics, - tsdocConfiguration: extractorConfig.tsdocConfiguration, + workingPackageFolder: packageFolder, + messageCallback, + messagesConfig: messages || {}, + showVerboseMessages, + showDiagnostics, + tsdocConfiguration, sourceMapper }); - if (extractorConfig.tsdocConfigFile.filePath && !extractorConfig.tsdocConfigFile.fileNotFound) { - if (!Path.isEqual(extractorConfig.tsdocConfigFile.filePath, ExtractorConfig._tsdocBaseFilePath)) { + if (tsdocConfigFilePath && !tsdocConfigFileNotFound) { + if (!Path.isEqual(tsdocConfigFilePath, ExtractorConfig._tsdocBaseFilePath)) { messageRouter.logVerbose( ConsoleMessageId.UsingCustomTSDocConfig, - 'Using custom TSDoc config from ' + extractorConfig.tsdocConfigFile.filePath + `Using custom TSDoc config from ${tsdocConfigFilePath}` ); } } @@ -241,9 +268,7 @@ export class Extractor { messageRouter.logDiagnosticHeader('TSDoc configuration'); // Convert the TSDocConfiguration into a tsdoc.json representation - const combinedConfigFile: TSDocConfigFile = TSDocConfigFile.loadFromParser( - extractorConfig.tsdocConfiguration - ); + const combinedConfigFile: TSDocConfigFile = TSDocConfigFile.loadFromParser(tsdocConfiguration); const serializedTSDocConfig: object = MessageRouter.buildJsonDumpObject( combinedConfigFile.saveToObject() ); @@ -254,7 +279,7 @@ export class Extractor { const collector: Collector = new Collector({ program: compilerState.program as ts.Program, messageRouter, - extractorConfig: extractorConfig, + extractorConfig, sourceMapper }); @@ -263,158 +288,75 @@ export class Extractor { DocCommentEnhancer.analyze(collector); ValidationEnhancer.analyze(collector); - const modelBuilder: ApiModelGenerator = new ApiModelGenerator(collector); + const modelBuilder: ApiModelGenerator = new ApiModelGenerator(collector, extractorConfig); const apiPackage: ApiPackage = modelBuilder.buildApiPackage(); if (messageRouter.showDiagnostics) { messageRouter.logDiagnostic(''); // skip a line after any diagnostic messages } - if (extractorConfig.docModelEnabled) { - messageRouter.logVerbose( - ConsoleMessageId.WritingDocModelFile, - 'Writing: ' + extractorConfig.apiJsonFilePath - ); - apiPackage.saveToJsonFile(extractorConfig.apiJsonFilePath, { + if (modelBuilder.docModelEnabled) { + messageRouter.logVerbose(ConsoleMessageId.WritingDocModelFile, `Writing: ${apiJsonFilePath}`); + apiPackage.saveToJsonFile(apiJsonFilePath, { toolPackage: Extractor.packageName, toolVersion: Extractor.version, - newlineConversion: extractorConfig.newlineKind, + newlineConversion: newlineKind, ensureFolderExists: true, - testMode: extractorConfig.testMode + testMode }); } - let apiReportChanged: boolean = false; - - if (extractorConfig.apiReportEnabled) { - const actualApiReportPath: string = extractorConfig.reportTempFilePath; - const actualApiReportShortPath: string = extractorConfig._getShortFilePath( - extractorConfig.reportTempFilePath - ); - - const expectedApiReportPath: string = extractorConfig.reportFilePath; - const expectedApiReportShortPath: string = extractorConfig._getShortFilePath( - extractorConfig.reportFilePath + function writeApiReport(reportConfig: IExtractorConfigApiReport): boolean { + return Extractor._writeApiReport( + collector, + extractorConfig, + messageRouter, + reportTempFolder, + reportFolder, + reportConfig, + localBuild, + printApiReportDiff ); + } - const actualApiReportContent: string = ApiReportGenerator.generateReviewFileContent(collector); - - // Write the actual file - FileSystem.writeFile(actualApiReportPath, actualApiReportContent, { - ensureFolderExists: true, - convertLineEndings: extractorConfig.newlineKind - }); - - // Compare it against the expected file - if (FileSystem.exists(expectedApiReportPath)) { - const expectedApiReportContent: string = FileSystem.readFile(expectedApiReportPath); - - if ( - !ApiReportGenerator.areEquivalentApiFileContents(actualApiReportContent, expectedApiReportContent) - ) { - apiReportChanged = true; - - if (!localBuild) { - // For a production build, issue a warning that will break the CI build. - messageRouter.logWarning( - ConsoleMessageId.ApiReportNotCopied, - 'You have changed the public API signature for this project.' + - ` Please copy the file "${actualApiReportShortPath}" to "${expectedApiReportShortPath}",` + - ` or perform a local build (which does this automatically).` + - ` See the Git repo documentation for more info.` - ); - } else { - // For a local build, just copy the file automatically. - messageRouter.logWarning( - ConsoleMessageId.ApiReportCopied, - 'You have changed the public API signature for this project.' + - ` Updating ${expectedApiReportShortPath}` - ); - - FileSystem.writeFile(expectedApiReportPath, actualApiReportContent, { - ensureFolderExists: true, - convertLineEndings: extractorConfig.newlineKind - }); - } - } else { - messageRouter.logVerbose( - ConsoleMessageId.ApiReportUnchanged, - `The API report is up to date: ${actualApiReportShortPath}` - ); - } - } else { - // The target file does not exist, so we are setting up the API review file for the first time. - // - // NOTE: People sometimes make a mistake where they move a project and forget to update the "reportFolder" - // setting, which causes a new file to silently get written to the wrong place. This can be confusing. - // Thus we treat the initial creation of the file specially. - apiReportChanged = true; - - if (!localBuild) { - // For a production build, issue a warning that will break the CI build. - messageRouter.logWarning( - ConsoleMessageId.ApiReportNotCopied, - 'The API report file is missing.' + - ` Please copy the file "${actualApiReportShortPath}" to "${expectedApiReportShortPath}",` + - ` or perform a local build (which does this automatically).` + - ` See the Git repo documentation for more info.` - ); - } else { - const expectedApiReportFolder: string = path.dirname(expectedApiReportPath); - if (!FileSystem.exists(expectedApiReportFolder)) { - messageRouter.logError( - ConsoleMessageId.ApiReportFolderMissing, - 'Unable to create the API report file. Please make sure the target folder exists:\n' + - expectedApiReportFolder - ); - } else { - FileSystem.writeFile(expectedApiReportPath, actualApiReportContent, { - convertLineEndings: extractorConfig.newlineKind - }); - messageRouter.logWarning( - ConsoleMessageId.ApiReportCreated, - 'The API report file was missing, so a new file was created. Please add this file to Git:\n' + - expectedApiReportPath - ); - } - } + let anyReportChanged: boolean = false; + if (apiReportEnabled) { + for (const reportConfig of reportConfigs) { + anyReportChanged = writeApiReport(reportConfig) || anyReportChanged; } } - if (extractorConfig.rollupEnabled) { + if (rollupEnabled) { Extractor._generateRollupDtsFile( collector, - extractorConfig.publicTrimmedFilePath, + publicTrimmedFilePath, DtsRollupKind.PublicRelease, - extractorConfig.newlineKind + newlineKind ); Extractor._generateRollupDtsFile( collector, - extractorConfig.alphaTrimmedFilePath, + alphaTrimmedFilePath, DtsRollupKind.AlphaRelease, - extractorConfig.newlineKind + newlineKind ); Extractor._generateRollupDtsFile( collector, - extractorConfig.betaTrimmedFilePath, + betaTrimmedFilePath, DtsRollupKind.BetaRelease, - extractorConfig.newlineKind + newlineKind ); Extractor._generateRollupDtsFile( collector, - extractorConfig.untrimmedFilePath, + untrimmedFilePath, DtsRollupKind.InternalRelease, - extractorConfig.newlineKind + newlineKind ); } - if (extractorConfig.tsdocMetadataEnabled) { + if (tsdocMetadataEnabled) { // Write the tsdoc-metadata.json file for this project - PackageMetadataManager.writeTsdocMetadataFile( - extractorConfig.tsdocMetadataFilePath, - extractorConfig.newlineKind - ); + PackageMetadataManager.writeTsdocMetadataFile(tsdocMetadataFilePath, newlineKind); } // Show all the messages that we collected during analysis @@ -434,12 +376,157 @@ export class Extractor { compilerState, extractorConfig, succeeded, - apiReportChanged, + apiReportChanged: anyReportChanged, errorCount: messageRouter.errorCount, warningCount: messageRouter.warningCount }); } + /** + * Generates the API report at the specified release level, writes it to the specified file path, and compares + * the output to the existing report (if one exists). + * + * @param reportTempDirectoryPath - The path to the directory under which the temp report file will be written prior + * to comparison with an existing report. + * @param reportDirectoryPath - The path to the directory under which the existing report file is located, and to + * which the new report will be written post-comparison. + * @param reportConfig - API report configuration, including its file name and {@link ApiReportVariant}. + * @param printApiReportDiff - {@link IExtractorInvokeOptions.printApiReportDiff} + * + * @returns Whether or not the newly generated report differs from the existing report (if one exists). + */ + private static _writeApiReport( + collector: Collector, + extractorConfig: ExtractorConfig, + messageRouter: MessageRouter, + reportTempDirectoryPath: string, + reportDirectoryPath: string, + reportConfig: IExtractorConfigApiReport, + localBuild: boolean, + printApiReportDiff: boolean + ): boolean { + let apiReportChanged: boolean = false; + + const actualApiReportPath: string = path.resolve(reportTempDirectoryPath, reportConfig.fileName); + const actualApiReportShortPath: string = extractorConfig._getShortFilePath(actualApiReportPath); + + const expectedApiReportPath: string = path.resolve(reportDirectoryPath, reportConfig.fileName); + const expectedApiReportShortPath: string = extractorConfig._getShortFilePath(expectedApiReportPath); + + collector.messageRouter.logVerbose( + ConsoleMessageId.WritingApiReport, + `Generating ${reportConfig.variant} API report: ${expectedApiReportPath}` + ); + + const actualApiReportContent: string = ApiReportGenerator.generateReviewFileContent( + collector, + reportConfig.variant + ); + + // Write the actual file + FileSystem.writeFile(actualApiReportPath, actualApiReportContent, { + ensureFolderExists: true, + convertLineEndings: extractorConfig.newlineKind + }); + + // Compare it against the expected file + if (FileSystem.exists(expectedApiReportPath)) { + const expectedApiReportContent: string = FileSystem.readFile(expectedApiReportPath, { + convertLineEndings: NewlineKind.Lf + }); + + if ( + !ApiReportGenerator.areEquivalentApiFileContents(actualApiReportContent, expectedApiReportContent) + ) { + apiReportChanged = true; + + if (!localBuild) { + // For a production build, issue a warning that will break the CI build. + messageRouter.logWarning( + ConsoleMessageId.ApiReportNotCopied, + 'You have changed the API signature for this project.' + + ` Please copy the file "${actualApiReportShortPath}" to "${expectedApiReportShortPath}",` + + ` or perform a local build (which does this automatically).` + + ` See the Git repo documentation for more info.` + ); + } else { + // For a local build, just copy the file automatically. + messageRouter.logWarning( + ConsoleMessageId.ApiReportCopied, + `You have changed the API signature for this project. Updating ${expectedApiReportShortPath}` + ); + + FileSystem.writeFile(expectedApiReportPath, actualApiReportContent, { + ensureFolderExists: true, + convertLineEndings: extractorConfig.newlineKind + }); + } + + if (messageRouter.showVerboseMessages || printApiReportDiff) { + const Diff: typeof import('diff') = require('diff'); + const patch: import('diff').StructuredPatch = Diff.structuredPatch( + expectedApiReportShortPath, + actualApiReportShortPath, + expectedApiReportContent, + actualApiReportContent + ); + const logFunction: + | (typeof MessageRouter.prototype)['logWarning'] + | (typeof MessageRouter.prototype)['logVerbose'] = printApiReportDiff + ? messageRouter.logWarning.bind(messageRouter) + : messageRouter.logVerbose.bind(messageRouter); + + logFunction( + ConsoleMessageId.ApiReportDiff, + 'Changes to the API report:\n\n' + Diff.formatPatch(patch) + ); + } + } else { + messageRouter.logVerbose( + ConsoleMessageId.ApiReportUnchanged, + `The API report is up to date: ${actualApiReportShortPath}` + ); + } + } else { + // The target file does not exist, so we are setting up the API review file for the first time. + // + // NOTE: People sometimes make a mistake where they move a project and forget to update the "reportFolder" + // setting, which causes a new file to silently get written to the wrong place. This can be confusing. + // Thus we treat the initial creation of the file specially. + apiReportChanged = true; + + if (!localBuild) { + // For a production build, issue a warning that will break the CI build. + messageRouter.logWarning( + ConsoleMessageId.ApiReportNotCopied, + 'The API report file is missing.' + + ` Please copy the file "${actualApiReportShortPath}" to "${expectedApiReportShortPath}",` + + ` or perform a local build (which does this automatically).` + + ` See the Git repo documentation for more info.` + ); + } else { + const expectedApiReportFolder: string = path.dirname(expectedApiReportPath); + if (!FileSystem.exists(expectedApiReportFolder)) { + messageRouter.logError( + ConsoleMessageId.ApiReportFolderMissing, + 'Unable to create the API report file. Please make sure the target folder exists:\n' + + expectedApiReportFolder + ); + } else { + FileSystem.writeFile(expectedApiReportPath, actualApiReportContent, { + convertLineEndings: extractorConfig.newlineKind + }); + messageRouter.logWarning( + ConsoleMessageId.ApiReportCreated, + 'The API report file was missing, so a new file was created. Please add this file to Git:\n' + + expectedApiReportPath + ); + } + } + } + return apiReportChanged; + } + private static _checkCompilerCompatibility( extractorConfig: ExtractorConfig, messageRouter: MessageRouter diff --git a/apps/api-extractor/src/api/ExtractorConfig.ts b/apps/api-extractor/src/api/ExtractorConfig.ts index 95d5542df99..47f16e6c020 100644 --- a/apps/api-extractor/src/api/ExtractorConfig.ts +++ b/apps/api-extractor/src/api/ExtractorConfig.ts @@ -1,29 +1,38 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; +import * as path from 'node:path'; + import * as resolve from 'resolve'; import lodash = require('lodash'); + +import { EnumMemberOrder, ReleaseTag } from '@microsoft/api-extractor-model'; +import { TSDocConfiguration, TSDocTagDefinition } from '@microsoft/tsdoc'; +import { TSDocConfigFile } from '@microsoft/tsdoc-config'; +import { type IRigConfig, RigConfig } from '@rushstack/rig-package'; import { JsonFile, JsonSchema, FileSystem, PackageJsonLookup, - INodePackageJson, + type INodePackageJson, PackageName, Text, InternalError, Path, NewlineKind } from '@rushstack/node-core-library'; -import { RigConfig } from '@rushstack/rig-package'; -import { IConfigFile, IExtractorMessagesConfig } from './IConfigFile'; +import type { + ApiReportVariant, + IConfigApiReport, + IConfigFile, + IExtractorMessagesConfig +} from './IConfigFile'; import { PackageMetadataManager } from '../analyzer/PackageMetadataManager'; import { MessageRouter } from '../collector/MessageRouter'; -import { EnumMemberOrder } from '@microsoft/api-extractor-model'; -import { TSDocConfiguration } from '@microsoft/tsdoc'; -import { TSDocConfigFile } from '@microsoft/tsdoc-config'; +import type { IApiModelGenerationOptions } from '../generators/ApiModelGenerator'; +import apiExtractorSchema from '../schemas/api-extractor.schema.json'; /** * Tokens used during variable expansion of path fields from api-extractor.json. @@ -71,7 +80,7 @@ export interface IExtractorConfigLoadForFolderOptions { /** * An already constructed `RigConfig` object. If omitted, then a new `RigConfig` object will be constructed. */ - rigConfig?: RigConfig; + rigConfig?: IRigConfig; } /** @@ -146,6 +155,44 @@ export interface IExtractorConfigPrepareOptions { ignoreMissingEntryPoint?: boolean; } +/** + * Configuration for a single API report, including its {@link IExtractorConfigApiReport.variant}. + * + * @public + */ +export interface IExtractorConfigApiReport { + /** + * Report variant. + * Determines which API items will be included in the report output, based on their tagged release levels. + */ + variant: ApiReportVariant; + + /** + * Name of the output report file. + * @remarks Relative to the configured report directory path. + */ + fileName: string; +} + +/** Default {@link IConfigApiReport.reportVariants} */ +const defaultApiReportVariants: readonly ApiReportVariant[] = ['complete']; + +/** + * Default {@link IConfigApiReport.tagsToReport}. + * + * @remarks + * Note that this list is externally documented, and directly affects report output. + * Also note that the order of tags in this list is significant, as it determines the order of tags in the report. + * Any changes to this list should be considered breaking. + */ +const defaultTagsToReport: Readonly> = { + '@sealed': true, + '@virtual': true, + '@override': true, + '@eventProperty': true, + '@deprecated': true +}; + interface IExtractorConfigParameters { projectFolder: string; packageJson: INodePackageJson | undefined; @@ -156,10 +203,12 @@ interface IExtractorConfigParameters { overrideTsconfig: {} | undefined; skipLibCheck: boolean; apiReportEnabled: boolean; - reportFilePath: string; - reportTempFilePath: string; + reportConfigs: readonly IExtractorConfigApiReport[]; + reportFolder: string; + reportTempFolder: string; apiReportIncludeForgottenExports: boolean; - docModelEnabled: boolean; + tagsToReport: Readonly>; + docModelGenerationOptions: IApiModelGenerationOptions | undefined; apiJsonFilePath: string; docModelIncludeForgottenExports: boolean; projectFolderUrl: string | undefined; @@ -181,15 +230,14 @@ interface IExtractorConfigParameters { /** * The `ExtractorConfig` class loads, validates, interprets, and represents the api-extractor.json config file. + * @sealed * @public */ export class ExtractorConfig { /** * The JSON Schema for API Extractor config file (api-extractor.schema.json). */ - public static readonly jsonSchema: JsonSchema = JsonSchema.fromFile( - path.join(__dirname, '../schemas/api-extractor.schema.json') - ); + public static readonly jsonSchema: JsonSchema = JsonSchema.fromLoadedObject(apiExtractorSchema); /** * The config file name "api-extractor.json". @@ -210,7 +258,8 @@ export class ExtractorConfig { path.join(__dirname, '../schemas/api-extractor-defaults.json') ); - private static readonly _declarationFileExtensionRegExp: RegExp = /\.d\.ts$/i; + /** Match all three flavors for type declaration files (.d.ts, .d.mts, .d.cts) */ + private static readonly _declarationFileExtensionRegExp: RegExp = /\.d\.(c|m)?ts$/i; /** {@inheritDoc IConfigFile.projectFolder} */ public readonly projectFolder: string; @@ -245,15 +294,46 @@ export class ExtractorConfig { /** {@inheritDoc IConfigApiReport.enabled} */ public readonly apiReportEnabled: boolean; - /** The `reportFolder` path combined with the `reportFileName`. */ - public readonly reportFilePath: string; - /** The `reportTempFolder` path combined with the `reportFileName`. */ - public readonly reportTempFilePath: string; + /** + * List of configurations for report files to be generated. + * @remarks Derived from {@link IConfigApiReport.reportFileName} and {@link IConfigApiReport.reportVariants}. + */ + public readonly reportConfigs: readonly IExtractorConfigApiReport[]; + /** {@inheritDoc IConfigApiReport.reportFolder} */ + public readonly reportFolder: string; + /** {@inheritDoc IConfigApiReport.reportTempFolder} */ + public readonly reportTempFolder: string; + /** {@inheritDoc IConfigApiReport.tagsToReport} */ + public readonly tagsToReport: Readonly>; + + /** + * Gets the file path for the "complete" (default) report configuration, if one was specified. + * Otherwise, returns an empty string. + * @deprecated Use {@link ExtractorConfig.reportConfigs} to access all report configurations. + */ + public get reportFilePath(): string { + const completeConfig: IExtractorConfigApiReport | undefined = this._getCompleteReportConfig(); + return completeConfig === undefined ? '' : path.join(this.reportFolder, completeConfig.fileName); + } + + /** + * Gets the temp file path for the "complete" (default) report configuration, if one was specified. + * Otherwise, returns an empty string. + * @deprecated Use {@link ExtractorConfig.reportConfigs} to access all report configurations. + */ + public get reportTempFilePath(): string { + const completeConfig: IExtractorConfigApiReport | undefined = this._getCompleteReportConfig(); + return completeConfig === undefined ? '' : path.join(this.reportTempFolder, completeConfig.fileName); + } + /** {@inheritDoc IConfigApiReport.includeForgottenExports} */ public readonly apiReportIncludeForgottenExports: boolean; - /** {@inheritDoc IConfigDocModel.enabled} */ - public readonly docModelEnabled: boolean; + /** + * If specified, the doc model is enabled and the specified options will be used. + * @beta + */ + public readonly docModelGenerationOptions: IApiModelGenerationOptions | undefined; /** {@inheritDoc IConfigDocModel.apiJsonFilePath} */ public readonly apiJsonFilePath: string; /** {@inheritDoc IConfigDocModel.includeForgottenExports} */ @@ -304,37 +384,72 @@ export class ExtractorConfig { /** {@inheritDoc IConfigFile.enumMemberOrder} */ public readonly enumMemberOrder: EnumMemberOrder; - private constructor(parameters: IExtractorConfigParameters) { - this.projectFolder = parameters.projectFolder; - this.packageJson = parameters.packageJson; - this.packageFolder = parameters.packageFolder; - this.mainEntryPointFilePath = parameters.mainEntryPointFilePath; - this.bundledPackages = parameters.bundledPackages; - this.tsconfigFilePath = parameters.tsconfigFilePath; - this.overrideTsconfig = parameters.overrideTsconfig; - this.skipLibCheck = parameters.skipLibCheck; - this.apiReportEnabled = parameters.apiReportEnabled; - this.reportFilePath = parameters.reportFilePath; - this.reportTempFilePath = parameters.reportTempFilePath; - this.apiReportIncludeForgottenExports = parameters.apiReportIncludeForgottenExports; - this.docModelEnabled = parameters.docModelEnabled; - this.apiJsonFilePath = parameters.apiJsonFilePath; - this.docModelIncludeForgottenExports = parameters.docModelIncludeForgottenExports; - this.projectFolderUrl = parameters.projectFolderUrl; - this.rollupEnabled = parameters.rollupEnabled; - this.untrimmedFilePath = parameters.untrimmedFilePath; - this.alphaTrimmedFilePath = parameters.alphaTrimmedFilePath; - this.betaTrimmedFilePath = parameters.betaTrimmedFilePath; - this.publicTrimmedFilePath = parameters.publicTrimmedFilePath; - this.omitTrimmingComments = parameters.omitTrimmingComments; - this.tsdocMetadataEnabled = parameters.tsdocMetadataEnabled; - this.tsdocMetadataFilePath = parameters.tsdocMetadataFilePath; - this.tsdocConfigFile = parameters.tsdocConfigFile; - this.tsdocConfiguration = parameters.tsdocConfiguration; - this.newlineKind = parameters.newlineKind; - this.messages = parameters.messages; - this.testMode = parameters.testMode; - this.enumMemberOrder = parameters.enumMemberOrder; + private constructor({ + projectFolder, + packageJson, + packageFolder, + mainEntryPointFilePath, + bundledPackages, + tsconfigFilePath, + overrideTsconfig, + skipLibCheck, + apiReportEnabled, + apiReportIncludeForgottenExports, + reportConfigs, + reportFolder, + reportTempFolder, + tagsToReport, + docModelGenerationOptions, + apiJsonFilePath, + docModelIncludeForgottenExports, + projectFolderUrl, + rollupEnabled, + untrimmedFilePath, + alphaTrimmedFilePath, + betaTrimmedFilePath, + publicTrimmedFilePath, + omitTrimmingComments, + tsdocMetadataEnabled, + tsdocMetadataFilePath, + tsdocConfigFile, + tsdocConfiguration, + newlineKind, + messages, + testMode, + enumMemberOrder + }: IExtractorConfigParameters) { + this.projectFolder = projectFolder; + this.packageJson = packageJson; + this.packageFolder = packageFolder; + this.mainEntryPointFilePath = mainEntryPointFilePath; + this.bundledPackages = bundledPackages; + this.tsconfigFilePath = tsconfigFilePath; + this.overrideTsconfig = overrideTsconfig; + this.skipLibCheck = skipLibCheck; + this.apiReportEnabled = apiReportEnabled; + this.apiReportIncludeForgottenExports = apiReportIncludeForgottenExports; + this.reportConfigs = reportConfigs; + this.reportFolder = reportFolder; + this.reportTempFolder = reportTempFolder; + this.tagsToReport = tagsToReport; + this.docModelGenerationOptions = docModelGenerationOptions; + this.apiJsonFilePath = apiJsonFilePath; + this.docModelIncludeForgottenExports = docModelIncludeForgottenExports; + this.projectFolderUrl = projectFolderUrl; + this.rollupEnabled = rollupEnabled; + this.untrimmedFilePath = untrimmedFilePath; + this.alphaTrimmedFilePath = alphaTrimmedFilePath; + this.betaTrimmedFilePath = betaTrimmedFilePath; + this.publicTrimmedFilePath = publicTrimmedFilePath; + this.omitTrimmingComments = omitTrimmingComments; + this.tsdocMetadataEnabled = tsdocMetadataEnabled; + this.tsdocMetadataFilePath = tsdocMetadataFilePath; + this.tsdocConfigFile = tsdocConfigFile; + this.tsdocConfiguration = tsdocConfiguration; + this.newlineKind = newlineKind; + this.messages = messages; + this.testMode = testMode; + this.enumMemberOrder = enumMemberOrder; } /** @@ -426,7 +541,7 @@ export class ExtractorConfig { // If We didn't find it in /api-extractor.json or /config/api-extractor.json // then check for a rig package if (packageFolder) { - let rigConfig: RigConfig; + let rigConfig: IRigConfig; if (options.rigConfig) { // The caller provided an already solved RigConfig. Double-check that it is for the right project. if (!Path.isEqual(options.rigConfig.projectFolderPath, packageFolder)) { @@ -514,6 +629,17 @@ export class ExtractorConfig { let currentConfigFilePath: string = path.resolve(jsonFilePath); let configObject: Partial = {}; + // Lodash merges array values by default, which is unintuitive for config files (and makes it impossible for derived configurations to overwrite arrays). + // For example, given a base config containing an array property with value ["foo", "bar"] and a derived config that specifies ["baz"] for that property, lodash will produce ["baz", "bar"], which is unintuitive. + // This customizer function ensures that arrays are always overwritten. + const mergeCustomizer: lodash.MergeWithCustomizer = (objValue, srcValue) => { + if (Array.isArray(srcValue)) { + return srcValue; + } + // Fall back to default merge behavior. + return undefined; + }; + try { do { // Check if this file was already processed. @@ -558,7 +684,7 @@ export class ExtractorConfig { ExtractorConfig._resolveConfigFileRelativePaths(baseConfig, currentConfigFolderPath); // Merge extractorConfig into baseConfig, mutating baseConfig - lodash.merge(baseConfig, configObject); + lodash.mergeWith(baseConfig, configObject, mergeCustomizer); configObject = baseConfig; currentConfigFilePath = extendsField; @@ -568,7 +694,11 @@ export class ExtractorConfig { } // Lastly, apply the defaults - configObject = lodash.merge(lodash.cloneDeep(ExtractorConfig._defaultConfig), configObject); + configObject = lodash.mergeWith( + lodash.cloneDeep(ExtractorConfig._defaultConfig), + configObject, + mergeCustomizer + ); ExtractorConfig.jsonSchema.validateObject(configObject, jsonFilePath); @@ -836,11 +966,9 @@ export class ExtractorConfig { } const bundledPackages: string[] = configObject.bundledPackages || []; - for (const bundledPackage of bundledPackages) { - if (!PackageName.isValidName(bundledPackage)) { - throw new Error(`The "bundledPackages" list contains an invalid package name: "${bundledPackage}"`); - } - } + + // Note: we cannot fully validate package name patterns, as the strings may contain wildcards. + // We won't know if the entries are valid until we can compare them against the package.json "dependencies" contents. const tsconfigFilePath: string = ExtractorConfig._resolvePathWithTokens( 'tsconfigFilePath', @@ -857,50 +985,96 @@ export class ExtractorConfig { } } - let apiReportEnabled: boolean = false; - let reportFilePath: string = ''; - let reportTempFilePath: string = ''; - let apiReportIncludeForgottenExports: boolean = false; - if (configObject.apiReport) { - apiReportEnabled = !!configObject.apiReport.enabled; + if (configObject.apiReport?.tagsToReport) { + _validateTagsToReport(configObject.apiReport.tagsToReport); + } - const reportFilename: string = ExtractorConfig._expandStringWithTokens( - 'reportFileName', - configObject.apiReport.reportFileName || '', - tokenContext - ); + const apiReportEnabled: boolean = configObject.apiReport?.enabled ?? false; + const apiReportIncludeForgottenExports: boolean = + configObject.apiReport?.includeForgottenExports ?? false; + let reportFolder: string = tokenContext.projectFolder; + let reportTempFolder: string = tokenContext.projectFolder; + const reportConfigs: IExtractorConfigApiReport[] = []; + let tagsToReport: Record<`@${string}`, boolean> = {}; + if (apiReportEnabled) { + // Undefined case checked above where we assign `apiReportEnabled` + const apiReportConfig: IConfigApiReport = configObject.apiReport!; + + const reportFileNameSuffix: string = '.api.md'; + let reportFileNameBase: string; + if (apiReportConfig.reportFileName) { + if ( + apiReportConfig.reportFileName.indexOf('/') >= 0 || + apiReportConfig.reportFileName.indexOf('\\') >= 0 + ) { + throw new Error( + `The "reportFileName" setting contains invalid characters: "${apiReportConfig.reportFileName}"` + ); + } - if (!reportFilename) { - // A merged configuration should have this - throw new Error('The "reportFilename" setting is missing'); + if (!apiReportConfig.reportFileName.endsWith(reportFileNameSuffix)) { + // `.api.md` extension was not specified. Use provided file name base as is. + reportFileNameBase = apiReportConfig.reportFileName; + } else { + // The system previously asked users to specify their filenames in a form containing the `.api.md` extension. + // This guidance has changed, but to maintain backwards compatibility, we will temporarily support input + // that ends with the `.api.md` extension specially, by stripping it out. + // This should be removed in version 8, possibly replaced with an explicit error to help users + // migrate their configs. + reportFileNameBase = apiReportConfig.reportFileName.slice(0, -reportFileNameSuffix.length); + } + } else { + // Default value + reportFileNameBase = ''; } - if (reportFilename.indexOf('/') >= 0 || reportFilename.indexOf('\\') >= 0) { - // A merged configuration should have this - throw new Error(`The "reportFilename" setting contains invalid characters: "${reportFilename}"`); + + const reportVariantKinds: readonly ApiReportVariant[] = + apiReportConfig.reportVariants ?? defaultApiReportVariants; + + for (const reportVariantKind of reportVariantKinds) { + // Omit the variant kind from the "complete" report file name for simplicity and for backwards compatibility. + const fileNameWithTokens: string = `${reportFileNameBase}${ + reportVariantKind === 'complete' ? '' : `.${reportVariantKind}` + }${reportFileNameSuffix}`; + const normalizedFileName: string = ExtractorConfig._expandStringWithTokens( + 'reportFileName', + fileNameWithTokens, + tokenContext + ); + + reportConfigs.push({ + fileName: normalizedFileName, + variant: reportVariantKind + }); } - const reportFolder: string = ExtractorConfig._resolvePathWithTokens( - 'reportFolder', - configObject.apiReport.reportFolder, - tokenContext - ); - const reportTempFolder: string = ExtractorConfig._resolvePathWithTokens( - 'reportTempFolder', - configObject.apiReport.reportTempFolder, - tokenContext - ); + if (apiReportConfig.reportFolder) { + reportFolder = ExtractorConfig._resolvePathWithTokens( + 'reportFolder', + apiReportConfig.reportFolder, + tokenContext + ); + } + + if (apiReportConfig.reportTempFolder) { + reportTempFolder = ExtractorConfig._resolvePathWithTokens( + 'reportTempFolder', + apiReportConfig.reportTempFolder, + tokenContext + ); + } - reportFilePath = path.join(reportFolder, reportFilename); - reportTempFilePath = path.join(reportTempFolder, reportFilename); - apiReportIncludeForgottenExports = !!configObject.apiReport.includeForgottenExports; + tagsToReport = { + ...defaultTagsToReport, + ...apiReportConfig.tagsToReport + }; } - let docModelEnabled: boolean = false; + let docModelGenerationOptions: IApiModelGenerationOptions | undefined = undefined; let apiJsonFilePath: string = ''; let docModelIncludeForgottenExports: boolean = false; let projectFolderUrl: string | undefined; - if (configObject.docModel) { - docModelEnabled = !!configObject.docModel.enabled; + if (configObject.docModel?.enabled) { apiJsonFilePath = ExtractorConfig._resolvePathWithTokens( 'apiJsonFilePath', configObject.docModel.apiJsonFilePath, @@ -908,6 +1082,43 @@ export class ExtractorConfig { ); docModelIncludeForgottenExports = !!configObject.docModel.includeForgottenExports; projectFolderUrl = configObject.docModel.projectFolderUrl; + + const releaseTagsToTrim: Set = new Set(); + const releaseTagsToTrimOption: string[] = configObject.docModel.releaseTagsToTrim || ['@internal']; + for (const releaseTagToTrim of releaseTagsToTrimOption) { + let releaseTag: ReleaseTag; + switch (releaseTagToTrim) { + case '@internal': { + releaseTag = ReleaseTag.Internal; + break; + } + + case '@alpha': { + releaseTag = ReleaseTag.Alpha; + break; + } + + case '@beta': { + releaseTag = ReleaseTag.Beta; + break; + } + + case '@public': { + releaseTag = ReleaseTag.Public; + break; + } + + default: { + throw new Error(`The release tag "${releaseTagToTrim}" is not supported`); + } + } + + releaseTagsToTrim.add(releaseTag); + } + + docModelGenerationOptions = { + releaseTagsToTrim + }; } let tsdocMetadataEnabled: boolean = false; @@ -1009,10 +1220,12 @@ export class ExtractorConfig { overrideTsconfig: configObject.compiler.overrideTsconfig, skipLibCheck: !!configObject.compiler.skipLibCheck, apiReportEnabled, - reportFilePath, - reportTempFilePath, + reportConfigs, + reportFolder, + reportTempFolder, apiReportIncludeForgottenExports, - docModelEnabled, + tagsToReport, + docModelGenerationOptions, apiJsonFilePath, docModelIncludeForgottenExports, projectFolderUrl, @@ -1067,6 +1280,13 @@ export class ExtractorConfig { return new ExtractorConfig({ ...extractorConfigParameters, tsdocConfigFile, tsdocConfiguration }); } + /** + * Gets the report configuration for the "complete" (default) report configuration, if one was specified. + */ + private _getCompleteReportConfig(): IExtractorConfigApiReport | undefined { + return this.reportConfigs.find((x) => x.variant === 'complete'); + } + private static _resolvePathWithTokens( fieldName: string, value: string | undefined, @@ -1136,3 +1356,47 @@ export class ExtractorConfig { throw new Error(`The "${fieldName}" value contains extra token characters ("<" or ">"): ${value}`); } } + +const releaseTags: Set = new Set(['@public', '@alpha', '@beta', '@internal']); + +/** + * Validate {@link ExtractorConfig.tagsToReport}. + */ +function _validateTagsToReport( + tagsToReport: Record +): asserts tagsToReport is Record<`@${string}`, boolean> { + const includedReleaseTags: string[] = []; + const invalidTags: [string, string][] = []; // tag name, error + for (const tag of Object.keys(tagsToReport)) { + if (releaseTags.has(tag)) { + // If a release tags is specified, regardless of whether it is enabled, we will throw an error. + // Release tags must not be specified. + includedReleaseTags.push(tag); + } + + // If the tag is invalid, generate an error string from the inner error message. + try { + TSDocTagDefinition.validateTSDocTagName(tag); + } catch (error) { + invalidTags.push([tag, (error as Error).message]); + } + } + + const errorMessages: string[] = []; + for (const includedReleaseTag of includedReleaseTags) { + errorMessages.push( + `${includedReleaseTag}: Release tags are always included in API reports and must not be specified` + ); + } + for (const [invalidTag, innerError] of invalidTags) { + errorMessages.push(`${invalidTag}: ${innerError}`); + } + + if (errorMessages.length > 0) { + const errorMessage: string = [ + `"tagsToReport" contained one or more invalid tags:`, + ...errorMessages + ].join('\n\t- '); + throw new Error(errorMessage); + } +} diff --git a/apps/api-extractor/src/api/ExtractorLogLevel.ts b/apps/api-extractor/src/api/ExtractorLogLevel.ts index 40a687aa3a0..9670889bf78 100644 --- a/apps/api-extractor/src/api/ExtractorLogLevel.ts +++ b/apps/api-extractor/src/api/ExtractorLogLevel.ts @@ -9,7 +9,7 @@ * * @public */ -export const enum ExtractorLogLevel { +export enum ExtractorLogLevel { /** * The message will be displayed as an error. * diff --git a/apps/api-extractor/src/api/ExtractorMessage.ts b/apps/api-extractor/src/api/ExtractorMessage.ts index 4e64d31cdba..1238a1a9c04 100644 --- a/apps/api-extractor/src/api/ExtractorMessage.ts +++ b/apps/api-extractor/src/api/ExtractorMessage.ts @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as tsdoc from '@microsoft/tsdoc'; -import { ExtractorMessageId } from './ExtractorMessageId'; +import type * as tsdoc from '@microsoft/tsdoc'; + +import type { ExtractorMessageId } from './ExtractorMessageId'; import { ExtractorLogLevel } from './ExtractorLogLevel'; -import { ConsoleMessageId } from './ConsoleMessageId'; +import type { ConsoleMessageId } from './ConsoleMessageId'; import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter'; /** @@ -28,7 +29,7 @@ export interface IExtractorMessageProperties { * Specifies a category of messages for use with {@link ExtractorMessage}. * @public */ -export const enum ExtractorMessageCategory { +export enum ExtractorMessageCategory { /** * Messages originating from the TypeScript compiler. * diff --git a/apps/api-extractor/src/api/ExtractorMessageId.ts b/apps/api-extractor/src/api/ExtractorMessageId.ts index 9af37df1ff9..9e423d9a420 100644 --- a/apps/api-extractor/src/api/ExtractorMessageId.ts +++ b/apps/api-extractor/src/api/ExtractorMessageId.ts @@ -11,12 +11,31 @@ * * @public */ -export const enum ExtractorMessageId { +export enum ExtractorMessageId { /** * "The doc comment should not contain more than one release tag." */ ExtraReleaseTag = 'ae-extra-release-tag', + /** + * "Missing documentation for ___." + * @remarks + * The `ae-undocumented` message is only generated if the API report feature is enabled. + * + * Because the API report file already annotates undocumented items with `// (undocumented)`, + * the `ae-undocumented` message is not logged by default. To see it, add a setting such as: + * ```json + * "messages": { + * "extractorMessageReporting": { + * "ae-undocumented": { + * "logLevel": "warning" + * } + * } + * } + * ``` + */ + Undocumented = 'ae-undocumented', + /** * "This symbol has another declaration with a different release tag." */ @@ -106,6 +125,7 @@ export const enum ExtractorMessageId { export const allExtractorMessageIds: Set = new Set([ 'ae-extra-release-tag', + 'ae-undocumented', 'ae-different-release-tags', 'ae-incompatible-release-tags', 'ae-missing-release-tag', diff --git a/apps/api-extractor/src/api/IConfigFile.ts b/apps/api-extractor/src/api/IConfigFile.ts index 328f76f1445..f05b943f796 100644 --- a/apps/api-extractor/src/api/IConfigFile.ts +++ b/apps/api-extractor/src/api/IConfigFile.ts @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { EnumMemberOrder } from '@microsoft/api-extractor-model'; -import { ExtractorLogLevel } from './ExtractorLogLevel'; +import type { EnumMemberOrder } from '@microsoft/api-extractor-model'; + +import type { ExtractorLogLevel } from './ExtractorLogLevel'; /** * Determines how the TypeScript compiler engine will be invoked by API Extractor. @@ -48,6 +49,13 @@ export interface IConfigCompiler { skipLibCheck?: boolean; } +/** + * The allowed variations of API reports. + * + * @public + */ +export type ApiReportVariant = 'public' | 'beta' | 'alpha' | 'complete'; + /** * Configures how the API report files (*.api.md) will be generated. * @@ -63,14 +71,39 @@ export interface IConfigApiReport { enabled: boolean; /** - * The filename for the API report files. It will be combined with `reportFolder` or `reportTempFolder` to produce - * a full output filename. + * The base filename for the API report files, to be combined with {@link IConfigApiReport.reportFolder} or + * {@link IConfigApiReport.reportTempFolder} to produce the full file path. * * @remarks - * The file extension should be ".api.md", and the string should not contain a path separator such as `\` or `/`. + * The `reportFileName` should not include any path separators such as `\` or `/`. The `reportFileName` should + * not include a file extension, since API Extractor will automatically append an appropriate file extension such + * as `.api.md`. If the {@link IConfigApiReport.reportVariants} setting is used, then the file extension includes + * the variant name, for example `my-report.public.api.md` or `my-report.beta.api.md`. The `complete` variant always + * uses the simple extension `my-report.api.md`. + * + * Previous versions of API Extractor required `reportFileName` to include the `.api.md` extension explicitly; + * for backwards compatibility, that is still accepted but will be discarded before applying the above rules. + * + * @defaultValue `` */ reportFileName?: string; + /** + * The set of report variants to generate. + * + * @remarks + * To support different approval requirements for different API levels, multiple "variants" of the API report can + * be generated. The `reportVariants` setting specifies a list of variants to be generated. If omitted, + * by default only the `complete` variant will be generated, which includes all `@internal`, `@alpha`, `@beta`, + * and `@public` items. Other possible variants are `alpha` (`@alpha` + `@beta` + `@public`), + * `beta` (`@beta` + `@public`), and `public` (`@public only`). + * + * The resulting API report file names will be derived from the {@link IConfigApiReport.reportFileName}. + * + * @defaultValue `[ "complete" ]` + */ + reportVariants?: ApiReportVariant[]; + /** * Specifies the folder where the API report file is written. The file name portion is determined by * the `reportFileName` setting. @@ -107,8 +140,49 @@ export interface IConfigApiReport { * @defaultValue `false` */ includeForgottenExports?: boolean; + + /** + * Specifies a list of {@link https://tsdoc.org/ | TSDoc} tags that should be reported in the API report file for + * items whose documentation contains them. + * + * @remarks + * Tag names must begin with `@`. + * + * This list may include standard TSDoc tags as well as custom ones. + * For more information on defining custom TSDoc tags, see + * {@link https://api-extractor.com/pages/configs/tsdoc_json/#defining-your-own-tsdoc-tags | here}. + * + * Note that an item's release tag will always reported; this behavior cannot be overridden. + * + * @defaultValue `@sealed`, `@virtual`, `@override`, `@eventProperty`, and `@deprecated` + * + * @example Omitting default tags + * To omit the `@sealed` and `@virtual` tags from API reports, you would specify `tagsToReport` as follows: + * ```json + * "tagsToReport": { + * "@sealed": false, + * "@virtual": false + * } + * ``` + * + * @example Including additional tags + * To include additional tags to the set included in API reports, you could specify `tagsToReport` like this: + * ```json + * "tagsToReport": { + * "@customTag": true + * } + * ``` + * This will result in `@customTag` being included in addition to the default tags. + */ + tagsToReport?: Readonly>; } +/** + * The allowed release tags that can be used to mark API items. + * @public + */ +export type ReleaseTagForTrim = '@internal' | '@alpha' | '@beta' | '@public'; + /** * Configures how the doc model file (*.api.json) will be generated. * @@ -156,6 +230,13 @@ export interface IConfigDocModel { * Can be omitted if you don't need source code links in your API documentation reference. */ projectFolderUrl?: string; + + /** + * Specifies a list of release tags that will be trimmed from the doc model. + * + * @defaultValue `["@internal"]` + */ + releaseTagsToTrim?: ReleaseTagForTrim[]; } /** @@ -378,8 +459,17 @@ export interface IConfigFile { * A list of NPM package names whose exports should be treated as part of this package. * * @remarks + * Also supports glob patterns. + * Note: glob patterns will **only** be resolved against dependencies listed in the project's package.json file. + * + * * This is both a safety and a performance precaution. + * + * Exact package names will be applied against any dependency encountered while walking the type graph, regardless of + * dependencies listed in the package.json. + * + * @example * - * For example, suppose that Webpack is used to generate a distributed bundle for the project `library1`, + * Suppose that Webpack is used to generate a distributed bundle for the project `library1`, * and another NPM package `library2` is embedded in this bundle. Some types from `library2` may become part * of the exported API for `library1`, but by default API Extractor would generate a .d.ts rollup that explicitly * imports `library2`. To avoid this, we can specify: diff --git a/apps/api-extractor/src/api/test/Extractor-custom-tags.test.ts b/apps/api-extractor/src/api/test/Extractor-custom-tags.test.ts index 720f60a6e21..357e7c1dc00 100644 --- a/apps/api-extractor/src/api/test/Extractor-custom-tags.test.ts +++ b/apps/api-extractor/src/api/test/Extractor-custom-tags.test.ts @@ -2,7 +2,7 @@ // See LICENSE in the project root for license information. import { StandardTags } from '@microsoft/tsdoc'; -import * as path from 'path'; +import * as path from 'node:path'; import { ExtractorConfig } from '../ExtractorConfig'; @@ -10,7 +10,7 @@ const testDataFolder: string = path.join(__dirname, 'test-data'); describe('Extractor-custom-tags', () => { describe('should use a TSDocConfiguration', () => { - it.only("with custom TSDoc tags defined in the package's tsdoc.json", () => { + it("with custom TSDoc tags defined in the package's tsdoc.json", () => { const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare( path.join(testDataFolder, 'custom-tsdoc-tags/api-extractor.json') ); @@ -20,7 +20,7 @@ describe('Extractor-custom-tags', () => { expect(tsdocConfiguration.tryGetTagDefinition('@inline')).not.toBe(undefined); expect(tsdocConfiguration.tryGetTagDefinition('@modifier')).not.toBe(undefined); }); - it.only("with custom TSDoc tags enabled per the package's tsdoc.json", () => { + it("with custom TSDoc tags enabled per the package's tsdoc.json", () => { const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare( path.join(testDataFolder, 'custom-tsdoc-tags/api-extractor.json') ); @@ -33,7 +33,7 @@ describe('Extractor-custom-tags', () => { expect(tsdocConfiguration.isTagSupported(inline)).toBe(true); expect(tsdocConfiguration.isTagSupported(modifier)).toBe(false); }); - it.only("with standard tags and API Extractor custom tags defined and supported when the package's tsdoc.json extends API Extractor's tsdoc.json", () => { + it("with standard tags and API Extractor custom tags defined and supported when the package's tsdoc.json extends API Extractor's tsdoc.json", () => { const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare( path.join(testDataFolder, 'custom-tsdoc-tags/api-extractor.json') ); diff --git a/apps/api-extractor/src/api/test/ExtractorConfig-lookup.test.ts b/apps/api-extractor/src/api/test/ExtractorConfig-lookup.test.ts index 9fb0a6e6dde..e93e1d8b4ed 100644 --- a/apps/api-extractor/src/api/test/ExtractorConfig-lookup.test.ts +++ b/apps/api-extractor/src/api/test/ExtractorConfig-lookup.test.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; +import * as path from 'node:path'; import { Path } from '@rushstack/node-core-library'; import { ExtractorConfig } from '../ExtractorConfig'; @@ -16,19 +16,19 @@ function expectEqualPaths(path1: string, path2: string): void { // Tests for expanding the "" token for the "projectFolder" setting in api-extractor.json describe(`${ExtractorConfig.name}.${ExtractorConfig.loadFileAndPrepare.name}`, () => { - it.only('config-lookup1: looks up ./api-extractor.json', () => { + it('config-lookup1: looks up ./api-extractor.json', () => { const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare( path.join(testDataFolder, 'config-lookup1/api-extractor.json') ); expectEqualPaths(extractorConfig.projectFolder, path.join(testDataFolder, 'config-lookup1')); }); - it.only('config-lookup2: looks up ./config/api-extractor.json', () => { + it('config-lookup2: looks up ./config/api-extractor.json', () => { const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare( path.join(testDataFolder, 'config-lookup2/config/api-extractor.json') ); expectEqualPaths(extractorConfig.projectFolder, path.join(testDataFolder, 'config-lookup2')); }); - it.only('config-lookup3a: looks up ./src/test/config/api-extractor.json', () => { + it('config-lookup3a: looks up ./src/test/config/api-extractor.json', () => { const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare( path.join(testDataFolder, 'config-lookup3/src/test/config/api-extractor.json') ); diff --git a/apps/api-extractor/src/api/test/ExtractorConfig-merge.test.ts b/apps/api-extractor/src/api/test/ExtractorConfig-merge.test.ts new file mode 100644 index 00000000000..d422c2fd54f --- /dev/null +++ b/apps/api-extractor/src/api/test/ExtractorConfig-merge.test.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as path from 'node:path'; + +import { ExtractorConfig } from '../ExtractorConfig'; + +const testDataFolder: string = path.join(__dirname, 'test-data'); + +// Tests verifying the merge behavior of ExtractorConfig.loadFile +describe(`${ExtractorConfig.name}.${ExtractorConfig.loadFile.name}`, () => { + it('array properties completely override array properties in the base config', () => { + const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare( + path.join(testDataFolder, 'override-array-properties', 'api-extractor.json') + ); + // Base config specifies: ["alpha", "beta", "public"] + // Derived config specifies: ["complete"] + // By default, lodash's merge() function would generate ["complete", "beta", "public"], + // but we instead want the derived config's array property to completely override that of the base. + expect(extractorConfig.reportConfigs).toEqual([ + { + variant: 'complete', + fileName: 'override-array-properties.api.md' + } + ]); + }); +}); diff --git a/apps/api-extractor/src/api/test/ExtractorConfig-tagsToReport.test.ts b/apps/api-extractor/src/api/test/ExtractorConfig-tagsToReport.test.ts new file mode 100644 index 00000000000..c941fd94308 --- /dev/null +++ b/apps/api-extractor/src/api/test/ExtractorConfig-tagsToReport.test.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as path from 'node:path'; + +import { ExtractorConfig } from '../ExtractorConfig'; + +const testDataFolder: string = path.join(__dirname, 'test-data'); + +describe('ExtractorConfig-tagsToReport', () => { + it('tagsToReport merge correctly', () => { + const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare( + path.join(testDataFolder, 'tags-to-report/api-extractor.json') + ); + const { tagsToReport } = extractorConfig; + expect(tagsToReport).toEqual({ + '@deprecated': true, + '@eventProperty': true, + '@myCustomTag': true, + '@myCustomTag2': false, + '@override': false, + '@sealed': true, + '@virtual': true + }); + }); + it('Invalid tagsToReport values', () => { + const expectedErrorMessage = `"tagsToReport" contained one or more invalid tags: +\t- @public: Release tags are always included in API reports and must not be specified +\t- @-invalid-tag-2: A TSDoc tag name must start with a letter and contain only letters and numbers`; + expect(() => + ExtractorConfig.loadFileAndPrepare( + path.join(testDataFolder, 'invalid-tags-to-report/api-extractor.json') + ) + ).toThrowError(expectedErrorMessage); + }); +}); diff --git a/apps/api-extractor/src/api/test/test-data/invalid-tags-to-report/README.md b/apps/api-extractor/src/api/test/test-data/invalid-tags-to-report/README.md new file mode 100644 index 00000000000..48328444ea7 --- /dev/null +++ b/apps/api-extractor/src/api/test/test-data/invalid-tags-to-report/README.md @@ -0,0 +1 @@ +Test case to ensure that merging of `apiReport.tagsToReport` is correct. diff --git a/apps/api-extractor/src/api/test/test-data/invalid-tags-to-report/api-extractor.json b/apps/api-extractor/src/api/test/test-data/invalid-tags-to-report/api-extractor.json new file mode 100644 index 00000000000..17c021c3e64 --- /dev/null +++ b/apps/api-extractor/src/api/test/test-data/invalid-tags-to-report/api-extractor.json @@ -0,0 +1,23 @@ +{ + "$schema": "../../../../schemas/api-extractor.schema.json", + + "mainEntryPointFilePath": "index.d.ts", + + "apiReport": { + "enabled": true, + "tagsToReport": { + "@validTag1": true, // Valid custom tag + "@-invalid-tag-2": true, // Invalid tag - invalid characters + "@public": false, // Release tags must not be specified + "@override": false // Valid (override base tag) + } + }, + + "docModel": { + "enabled": true + }, + + "dtsRollup": { + "enabled": true + } +} diff --git a/apps/api-extractor/src/api/test/test-data/invalid-tags-to-report/index.d.ts b/apps/api-extractor/src/api/test/test-data/invalid-tags-to-report/index.d.ts new file mode 100644 index 00000000000..4bd8276f349 --- /dev/null +++ b/apps/api-extractor/src/api/test/test-data/invalid-tags-to-report/index.d.ts @@ -0,0 +1 @@ +// empty file diff --git a/apps/api-extractor/src/api/test/test-data/invalid-tags-to-report/package.json b/apps/api-extractor/src/api/test/test-data/invalid-tags-to-report/package.json new file mode 100644 index 00000000000..dc906e7d69b --- /dev/null +++ b/apps/api-extractor/src/api/test/test-data/invalid-tags-to-report/package.json @@ -0,0 +1,4 @@ +{ + "name": "tags-to-report", + "version": "1.0.0" +} diff --git a/apps/api-extractor/src/api/test/test-data/override-array-properties/README.md b/apps/api-extractor/src/api/test/test-data/override-array-properties/README.md new file mode 100644 index 00000000000..22f9b309e36 --- /dev/null +++ b/apps/api-extractor/src/api/test/test-data/override-array-properties/README.md @@ -0,0 +1,2 @@ +Reproduction of #4786. +Merging of configs should *not* merge arrays - the overriding config file's array properties should completely overwrite the base config's array properties. diff --git a/apps/api-extractor/src/api/test/test-data/override-array-properties/api-extractor-base.json b/apps/api-extractor/src/api/test/test-data/override-array-properties/api-extractor-base.json new file mode 100644 index 00000000000..f41027c72e4 --- /dev/null +++ b/apps/api-extractor/src/api/test/test-data/override-array-properties/api-extractor-base.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "index.d.ts", + + "apiReport": { + "enabled": true, + "reportVariants": ["alpha", "beta", "public"] + }, + + "docModel": { + "enabled": true + }, + + "dtsRollup": { + "enabled": true + } +} diff --git a/apps/api-extractor/src/api/test/test-data/override-array-properties/api-extractor.json b/apps/api-extractor/src/api/test/test-data/override-array-properties/api-extractor.json new file mode 100644 index 00000000000..fd4839e09fd --- /dev/null +++ b/apps/api-extractor/src/api/test/test-data/override-array-properties/api-extractor.json @@ -0,0 +1,7 @@ +{ + "extends": "./api-extractor-base.json", + + "apiReport": { + "reportVariants": ["complete"] + } +} diff --git a/apps/api-extractor/src/api/test/test-data/override-array-properties/index.d.ts b/apps/api-extractor/src/api/test/test-data/override-array-properties/index.d.ts new file mode 100644 index 00000000000..4bd8276f349 --- /dev/null +++ b/apps/api-extractor/src/api/test/test-data/override-array-properties/index.d.ts @@ -0,0 +1 @@ +// empty file diff --git a/apps/api-extractor/src/api/test/test-data/override-array-properties/package.json b/apps/api-extractor/src/api/test/test-data/override-array-properties/package.json new file mode 100644 index 00000000000..29ba54f2ce1 --- /dev/null +++ b/apps/api-extractor/src/api/test/test-data/override-array-properties/package.json @@ -0,0 +1,4 @@ +{ + "name": "override-array-properties", + "version": "1.0.0" +} diff --git a/apps/api-extractor/src/api/test/test-data/tags-to-report/README.md b/apps/api-extractor/src/api/test/test-data/tags-to-report/README.md new file mode 100644 index 00000000000..48328444ea7 --- /dev/null +++ b/apps/api-extractor/src/api/test/test-data/tags-to-report/README.md @@ -0,0 +1 @@ +Test case to ensure that merging of `apiReport.tagsToReport` is correct. diff --git a/apps/api-extractor/src/api/test/test-data/tags-to-report/api-extractor-base.json b/apps/api-extractor/src/api/test/test-data/tags-to-report/api-extractor-base.json new file mode 100644 index 00000000000..6ab11ff3957 --- /dev/null +++ b/apps/api-extractor/src/api/test/test-data/tags-to-report/api-extractor-base.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "index.d.ts", + + "apiReport": { + "enabled": true + }, + + "docModel": { + "enabled": true + }, + + "dtsRollup": { + "enabled": true + } +} diff --git a/apps/api-extractor/src/api/test/test-data/tags-to-report/api-extractor.json b/apps/api-extractor/src/api/test/test-data/tags-to-report/api-extractor.json new file mode 100644 index 00000000000..10df8061cce --- /dev/null +++ b/apps/api-extractor/src/api/test/test-data/tags-to-report/api-extractor.json @@ -0,0 +1,11 @@ +{ + "extends": "./api-extractor-base.json", + + "apiReport": { + "tagsToReport": { + "@myCustomTag": true, // Enable reporting of custom tag + "@override": false, // Disable default reporting of `@override` tag + "@myCustomTag2": false // Disable reporting of custom tag (not included by base config) + } + } +} diff --git a/apps/api-extractor/src/api/test/test-data/tags-to-report/index.d.ts b/apps/api-extractor/src/api/test/test-data/tags-to-report/index.d.ts new file mode 100644 index 00000000000..4bd8276f349 --- /dev/null +++ b/apps/api-extractor/src/api/test/test-data/tags-to-report/index.d.ts @@ -0,0 +1 @@ +// empty file diff --git a/apps/api-extractor/src/api/test/test-data/tags-to-report/package.json b/apps/api-extractor/src/api/test/test-data/tags-to-report/package.json new file mode 100644 index 00000000000..dc906e7d69b --- /dev/null +++ b/apps/api-extractor/src/api/test/test-data/tags-to-report/package.json @@ -0,0 +1,4 @@ +{ + "name": "tags-to-report", + "version": "1.0.0" +} diff --git a/apps/api-extractor/src/cli/ApiExtractorCommandLine.ts b/apps/api-extractor/src/cli/ApiExtractorCommandLine.ts index 7480f460a3e..2c434e32cec 100644 --- a/apps/api-extractor/src/cli/ApiExtractorCommandLine.ts +++ b/apps/api-extractor/src/cli/ApiExtractorCommandLine.ts @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import colors from 'colors'; -import * as os from 'os'; +import * as os from 'node:os'; -import { CommandLineParser, CommandLineFlagParameter } from '@rushstack/ts-command-line'; -import { InternalError } from '@rushstack/node-core-library'; +import { CommandLineParser, type CommandLineFlagParameter } from '@rushstack/ts-command-line'; +import { AlreadyReportedError, InternalError } from '@rushstack/node-core-library'; +import { Colorize } from '@rushstack/terminal'; import { RunAction } from './RunAction'; import { InitAction } from './InitAction'; @@ -32,21 +32,24 @@ export class ApiExtractorCommandLine extends CommandLineParser { }); } - protected onExecute(): Promise { - // override + protected override async onExecuteAsync(): Promise { if (this._debugParameter.value) { InternalError.breakInDebugger = true; } - return super.onExecute().catch((error) => { - if (this._debugParameter.value) { - console.error(os.EOL + error.stack); - } else { - console.error(os.EOL + colors.red('ERROR: ' + error.message.trim())); + process.exitCode = 1; + try { + await super.onExecuteAsync(); + process.exitCode = 0; + } catch (error) { + if (!(error instanceof AlreadyReportedError)) { + if (this._debugParameter.value) { + console.error(os.EOL + error.stack); + } else { + console.error(os.EOL + Colorize.red('ERROR: ' + error.message.trim())); + } } - - process.exitCode = 1; - }); + } } private _populateActions(): void { diff --git a/apps/api-extractor/src/cli/InitAction.ts b/apps/api-extractor/src/cli/InitAction.ts index f97561fa9ab..834e2a7660f 100644 --- a/apps/api-extractor/src/cli/InitAction.ts +++ b/apps/api-extractor/src/cli/InitAction.ts @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import colors from 'colors'; -import * as path from 'path'; +import * as path from 'node:path'; + import { FileSystem } from '@rushstack/node-core-library'; import { CommandLineAction } from '@rushstack/ts-command-line'; +import { Colorize } from '@rushstack/terminal'; -import { ApiExtractorCommandLine } from './ApiExtractorCommandLine'; +import type { ApiExtractorCommandLine } from './ApiExtractorCommandLine'; import { ExtractorConfig } from '../api/ExtractorConfig'; export class InitAction extends CommandLineAction { @@ -21,18 +22,17 @@ export class InitAction extends CommandLineAction { }); } - protected async onExecute(): Promise { - // override + protected override async onExecuteAsync(): Promise { const inputFilePath: string = path.resolve(__dirname, '../schemas/api-extractor-template.json'); const outputFilePath: string = path.resolve(ExtractorConfig.FILENAME); if (FileSystem.exists(outputFilePath)) { - console.log(colors.red('The output file already exists:')); + console.log(Colorize.red('The output file already exists:')); console.log('\n ' + outputFilePath + '\n'); throw new Error('Unable to write output file'); } - console.log(colors.green('Writing file: ') + outputFilePath); + console.log(Colorize.green('Writing file: ') + outputFilePath); FileSystem.copyFile({ sourcePath: inputFilePath, destinationPath: outputFilePath diff --git a/apps/api-extractor/src/cli/RunAction.ts b/apps/api-extractor/src/cli/RunAction.ts index e9c4c878441..d0e60bb9326 100644 --- a/apps/api-extractor/src/cli/RunAction.ts +++ b/apps/api-extractor/src/cli/RunAction.ts @@ -1,28 +1,34 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import colors from 'colors'; -import * as os from 'os'; -import * as path from 'path'; -import { PackageJsonLookup, FileSystem, IPackageJson, Path } from '@rushstack/node-core-library'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { + PackageJsonLookup, + FileSystem, + type IPackageJson, + Path, + AlreadyReportedError +} from '@rushstack/node-core-library'; +import { Colorize } from '@rushstack/terminal'; import { CommandLineAction, - CommandLineStringParameter, - CommandLineFlagParameter + type CommandLineStringParameter, + type CommandLineFlagParameter } from '@rushstack/ts-command-line'; -import { Extractor, ExtractorResult } from '../api/Extractor'; - -import { ApiExtractorCommandLine } from './ApiExtractorCommandLine'; -import { ExtractorConfig, IExtractorConfigPrepareOptions } from '../api/ExtractorConfig'; +import { Extractor, type ExtractorResult } from '../api/Extractor'; +import type { ApiExtractorCommandLine } from './ApiExtractorCommandLine'; +import { ExtractorConfig, type IExtractorConfigPrepareOptions } from '../api/ExtractorConfig'; export class RunAction extends CommandLineAction { private readonly _configFileParameter: CommandLineStringParameter; - private readonly _localParameter: CommandLineFlagParameter; - private readonly _verboseParameter: CommandLineFlagParameter; + private readonly _localFlag: CommandLineFlagParameter; + private readonly _verboseFlag: CommandLineFlagParameter; private readonly _diagnosticsParameter: CommandLineFlagParameter; - private readonly _typescriptCompilerFolder: CommandLineStringParameter; + private readonly _typescriptCompilerFolderParameter: CommandLineStringParameter; + private readonly _printApiReportDiffFlag: CommandLineFlagParameter; public constructor(parser: ApiExtractorCommandLine) { super({ @@ -38,7 +44,7 @@ export class RunAction extends CommandLineAction { description: `Use the specified ${ExtractorConfig.FILENAME} file path, rather than guessing its location` }); - this._localParameter = this.defineFlagParameter({ + this._localFlag = this.defineFlagParameter({ parameterLongName: '--local', parameterShortName: '-l', description: @@ -48,7 +54,7 @@ export class RunAction extends CommandLineAction { ' report file is automatically copied in a local build.' }); - this._verboseParameter = this.defineFlagParameter({ + this._verboseFlag = this.defineFlagParameter({ parameterLongName: '--verbose', parameterShortName: '-v', description: 'Show additional informational messages in the output.' @@ -61,7 +67,7 @@ export class RunAction extends CommandLineAction { ' This flag also enables the "--verbose" flag.' }); - this._typescriptCompilerFolder = this.defineStringParameter({ + this._typescriptCompilerFolderParameter = this.defineStringParameter({ parameterLongName: '--typescript-compiler-folder', argumentName: 'PATH', description: @@ -71,14 +77,21 @@ export class RunAction extends CommandLineAction { ' "--typescriptCompilerFolder" option to specify the folder path where you installed the TypeScript package,' + " and API Extractor's compiler will use those system typings instead." }); + + this._printApiReportDiffFlag = this.defineFlagParameter({ + parameterLongName: '--print-api-report-diff', + description: + 'If provided, then any differences between the actual and expected API reports will be ' + + 'printed on the console. Note that the diff is not printed if the expected API report file has not been ' + + 'created yet.' + }); } - protected async onExecute(): Promise { - // override + protected override async onExecuteAsync(): Promise { const lookup: PackageJsonLookup = new PackageJsonLookup(); let configFilename: string; - let typescriptCompilerFolder: string | undefined = this._typescriptCompilerFolder.value; + let typescriptCompilerFolder: string | undefined = this._typescriptCompilerFolderParameter.value; if (typescriptCompilerFolder) { typescriptCompilerFolder = path.normalize(typescriptCompilerFolder); @@ -89,17 +102,17 @@ export class RunAction extends CommandLineAction { : undefined; if (!typescriptCompilerPackageJson) { throw new Error( - `The path specified in the ${this._typescriptCompilerFolder.longName} parameter is not a package.` + `The path specified in the ${this._typescriptCompilerFolderParameter.longName} parameter is not a package.` ); } else if (typescriptCompilerPackageJson.name !== 'typescript') { throw new Error( - `The path specified in the ${this._typescriptCompilerFolder.longName} parameter is not a TypeScript` + + `The path specified in the ${this._typescriptCompilerFolderParameter.longName} parameter is not a TypeScript` + ' compiler package.' ); } } else { throw new Error( - `The path specified in the ${this._typescriptCompilerFolder.longName} parameter does not exist.` + `The path specified in the ${this._typescriptCompilerFolderParameter.longName} parameter does not exist.` ); } } @@ -132,22 +145,22 @@ export class RunAction extends CommandLineAction { } const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, { - localBuild: this._localParameter.value, - showVerboseMessages: this._verboseParameter.value, + localBuild: this._localFlag.value, + showVerboseMessages: this._verboseFlag.value, showDiagnostics: this._diagnosticsParameter.value, - typescriptCompilerFolder: typescriptCompilerFolder + typescriptCompilerFolder: typescriptCompilerFolder, + printApiReportDiff: this._printApiReportDiffFlag.value }); if (extractorResult.succeeded) { console.log(os.EOL + 'API Extractor completed successfully'); } else { - process.exitCode = 1; - if (extractorResult.errorCount > 0) { - console.log(os.EOL + colors.red('API Extractor completed with errors')); + console.log(os.EOL + Colorize.red('API Extractor completed with errors')); } else { - console.log(os.EOL + colors.yellow('API Extractor completed with warnings')); + console.log(os.EOL + Colorize.yellow('API Extractor completed with warnings')); } + throw new AlreadyReportedError(); } } } diff --git a/apps/api-extractor/src/collector/ApiItemMetadata.ts b/apps/api-extractor/src/collector/ApiItemMetadata.ts index 738e58619dc..4448f416b63 100644 --- a/apps/api-extractor/src/collector/ApiItemMetadata.ts +++ b/apps/api-extractor/src/collector/ApiItemMetadata.ts @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as tsdoc from '@microsoft/tsdoc'; -import { ReleaseTag } from '@microsoft/api-extractor-model'; +import type * as tsdoc from '@microsoft/tsdoc'; +import type { ReleaseTag } from '@microsoft/api-extractor-model'; + import { VisitorState } from './VisitorState'; /** @@ -75,8 +76,20 @@ export class ApiItemMetadata { */ public tsdocComment: tsdoc.DocComment | undefined; - // Assigned by DocCommentEnhancer - public needsDocumentation: boolean = true; + /** + * Tracks whether or not the associated API item is known to be missing sufficient documentation. + * + * @remarks + * + * An "undocumented" item is one whose TSDoc comment which either does not contain a summary comment block, or + * has an `@inheritDoc` tag that resolves to another "undocumented" API member. + * + * If there is any ambiguity (e.g. if an `@inheritDoc` comment points to an external API member, whose documentation, + * we can't parse), "undocumented" will be `false`. + * + * @remarks Assigned by {@link DocCommentEnhancer}. + */ + public undocumented: boolean = true; public docCommentEnhancerVisitorState: VisitorState = VisitorState.Unvisited; diff --git a/apps/api-extractor/src/collector/Collector.ts b/apps/api-extractor/src/collector/Collector.ts index 4e6e1efa70a..b2e872565f6 100644 --- a/apps/api-extractor/src/collector/Collector.ts +++ b/apps/api-extractor/src/collector/Collector.ts @@ -2,31 +2,38 @@ // See LICENSE in the project root for license information. import * as ts from 'typescript'; +import { minimatch } from 'minimatch'; + import * as tsdoc from '@microsoft/tsdoc'; -import { PackageJsonLookup, Sort, InternalError } from '@rushstack/node-core-library'; import { ReleaseTag } from '@microsoft/api-extractor-model'; +import { + PackageJsonLookup, + Sort, + InternalError, + type INodePackageJson, + PackageName +} from '@rushstack/node-core-library'; import { ExtractorMessageId } from '../api/ExtractorMessageId'; - import { CollectorEntity } from './CollectorEntity'; import { AstSymbolTable } from '../analyzer/AstSymbolTable'; -import { AstEntity } from '../analyzer/AstEntity'; -import { AstModule, AstModuleExportInfo } from '../analyzer/AstModule'; +import type { AstEntity } from '../analyzer/AstEntity'; +import type { AstModule, IAstModuleExportInfo } from '../analyzer/AstModule'; import { AstSymbol } from '../analyzer/AstSymbol'; -import { AstDeclaration } from '../analyzer/AstDeclaration'; +import type { AstDeclaration } from '../analyzer/AstDeclaration'; import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers'; import { WorkingPackage } from './WorkingPackage'; import { PackageDocComment } from '../aedoc/PackageDocComment'; -import { DeclarationMetadata, InternalDeclarationMetadata } from './DeclarationMetadata'; -import { ApiItemMetadata, IApiItemMetadataOptions } from './ApiItemMetadata'; +import { type DeclarationMetadata, InternalDeclarationMetadata } from './DeclarationMetadata'; +import { ApiItemMetadata, type IApiItemMetadataOptions } from './ApiItemMetadata'; import { SymbolMetadata } from './SymbolMetadata'; -import { TypeScriptInternals, IGlobalVariableAnalyzer } from '../analyzer/TypeScriptInternals'; -import { MessageRouter } from './MessageRouter'; +import { TypeScriptInternals, type IGlobalVariableAnalyzer } from '../analyzer/TypeScriptInternals'; +import type { MessageRouter } from './MessageRouter'; import { AstReferenceResolver } from '../analyzer/AstReferenceResolver'; import { ExtractorConfig } from '../api/ExtractorConfig'; import { AstNamespaceImport } from '../analyzer/AstNamespaceImport'; import { AstImport } from '../analyzer/AstImport'; -import { SourceMapper } from './SourceMapper'; +import type { SourceMapper } from './SourceMapper'; /** * Options for Collector constructor. @@ -132,7 +139,11 @@ export class Collector { this._tsdocParser = new tsdoc.TSDocParser(this.extractorConfig.tsdocConfiguration); - this.bundledPackageNames = new Set(this.extractorConfig.bundledPackages); + // Resolve package name patterns and store concrete set of bundled package dependency names + this.bundledPackageNames = Collector._resolveBundledPackagePatterns( + this.extractorConfig.bundledPackages, + this.extractorConfig.packageJson + ); this.astSymbolTable = new AstSymbolTable( this.program, @@ -147,6 +158,55 @@ export class Collector { } /** + * Resolve provided `bundledPackages` names and glob patterns to a list of explicit package names. + * + * @remarks + * Explicit package names will be included in the output unconditionally. However, wildcard patterns will + * only be matched against the various dependencies listed in the provided package.json (if there was one). + * Patterns will be matched against `dependencies`, `devDependencies`, `optionalDependencies`, and `peerDependencies`. + * + * @param bundledPackages - The list of package names and/or glob patterns to resolve. + * @param packageJson - The package.json of the package being processed (if there is one). + * @returns The set of resolved package names to be bundled during analysis. + */ + private static _resolveBundledPackagePatterns( + bundledPackages: string[], + packageJson: INodePackageJson | undefined + ): ReadonlySet { + if (bundledPackages.length === 0) { + // If no `bundledPackages` were specified, then there is nothing to resolve. + // Return an empty set. + return new Set(); + } + + // Accumulate all declared dependencies. + // Any wildcard patterns in `bundledPackages` will be resolved against these. + const dependencyNames: Set = new Set(); + Object.keys(packageJson?.dependencies ?? {}).forEach((dep) => dependencyNames.add(dep)); + Object.keys(packageJson?.devDependencies ?? {}).forEach((dep) => dependencyNames.add(dep)); + Object.keys(packageJson?.peerDependencies ?? {}).forEach((dep) => dependencyNames.add(dep)); + Object.keys(packageJson?.optionalDependencies ?? {}).forEach((dep) => dependencyNames.add(dep)); + + // The set of resolved package names to be populated and returned + const resolvedPackageNames: Set = new Set(); + + for (const packageNameOrPattern of bundledPackages) { + // If the string is an exact package name, use it regardless of package.json contents + if (PackageName.isValidName(packageNameOrPattern)) { + resolvedPackageNames.add(packageNameOrPattern); + } else { + // If the entry isn't an exact package name, assume glob pattern and search for matches + for (const dependencyName of dependencyNames) { + if (minimatch(dependencyName, packageNameOrPattern)) { + resolvedPackageNames.add(dependencyName); + } + } + } + } + return resolvedPackageNames; + } + + /**a * Returns a list of names (e.g. "example-library") that should appear in a reference like this: * * ``` @@ -253,12 +313,12 @@ export class Collector { this.workingPackage.tsdocComment = this.workingPackage.tsdocParserContext!.docComment; } - const astModuleExportInfo: AstModuleExportInfo = + const { exportedLocalEntities, starExportedExternalModules, visitedAstModules }: IAstModuleExportInfo = this.astSymbolTable.fetchAstModuleExportInfo(astEntryPoint); // Create a CollectorEntity for each top-level export. const processedAstEntities: AstEntity[] = []; - for (const [exportName, astEntity] of astModuleExportInfo.exportedLocalEntities) { + for (const [exportName, astEntity] of exportedLocalEntities) { this._createCollectorEntity(astEntity, exportName); processedAstEntities.push(astEntity); } @@ -273,9 +333,33 @@ export class Collector { } } + // Ensure references are collected from any intermediate files that + // only include exports + const nonExternalSourceFiles: Set = new Set(); + for (const { sourceFile, isExternal } of visitedAstModules) { + if (!nonExternalSourceFiles.has(sourceFile) && !isExternal) { + nonExternalSourceFiles.add(sourceFile); + } + } + + // Here, we're collecting reference directives from all non-external source files + // that were encountered while looking for exports, but only those references that + // were explicitly written by the developer and marked with the `preserve="true"` + // attribute. In TS >= 5.5, only references that are explicitly authored and marked + // with `preserve="true"` are included in the output. See https://github.com/microsoft/TypeScript/pull/57681 + // + // The `_collectReferenceDirectives` function pulls in all references in files that + // contain definitions, but does not examine files that only reexport from other + // files. Here, we're looking through files that were missed by `_collectReferenceDirectives`, + // but only collecting references that were explicitly marked with `preserve="true"`. + // It is intuitive for developers to include references that they explicitly want part of + // their public API in a file like the entrypoint, which is likely to only contain reexports, + // and this picks those up. + this._collectReferenceDirectivesFromSourceFiles(nonExternalSourceFiles, true); + this._makeUniqueNames(); - for (const starExportedExternalModule of astModuleExportInfo.starExportedExternalModules) { + for (const starExportedExternalModule of starExportedExternalModules) { if (starExportedExternalModule.externalModulePath !== undefined) { this._starExportedExternalModulePaths.push(starExportedExternalModule.externalModulePath); } @@ -479,7 +563,7 @@ export class Collector { } if (astEntity instanceof AstNamespaceImport) { - const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(this); + const astModuleExportInfo: IAstModuleExportInfo = astEntity.fetchAstModuleExportInfo(this); const parentEntity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astEntity); if (!parentEntity) { // This should never happen, as we've already created entities for all AstNamespaceImports. @@ -932,44 +1016,82 @@ export class Collector { } private _collectReferenceDirectives(astEntity: AstEntity): void { + // Here, we're collecting reference directives from source files that contain extracted + // definitions (i.e. - files that contain `export class ...`, `export interface ...`, ...). + // These references may or may not include the `preserve="true" attribute. In TS < 5.5, + // references that end up in .D.TS files may or may not be explicity written by the developer. + // In TS >= 5.5, only references that are explicitly authored and are marked with + // `preserve="true"` are included in the output. See https://github.com/microsoft/TypeScript/pull/57681 + // + // The calls to `_collectReferenceDirectivesFromSourceFiles` in this function are + // preserving existing behavior, which is to include all reference directives + // regardless of whether they are explicitly authored or not, but only in files that + // contain definitions. + if (astEntity instanceof AstSymbol) { const sourceFiles: ts.SourceFile[] = astEntity.astDeclarations.map((astDeclaration) => astDeclaration.declaration.getSourceFile() ); - return this._collectReferenceDirectivesFromSourceFiles(sourceFiles); + return this._collectReferenceDirectivesFromSourceFiles(sourceFiles, false); } if (astEntity instanceof AstNamespaceImport) { const sourceFiles: ts.SourceFile[] = [astEntity.astModule.sourceFile]; - return this._collectReferenceDirectivesFromSourceFiles(sourceFiles); + return this._collectReferenceDirectivesFromSourceFiles(sourceFiles, false); } } - private _collectReferenceDirectivesFromSourceFiles(sourceFiles: ts.SourceFile[]): void { + private _collectReferenceDirectivesFromSourceFiles( + sourceFiles: Iterable, + onlyIncludeExplicitlyPreserved: boolean + ): void { const seenFilenames: Set = new Set(); for (const sourceFile of sourceFiles) { - if (sourceFile && sourceFile.fileName) { - if (!seenFilenames.has(sourceFile.fileName)) { - seenFilenames.add(sourceFile.fileName); - - for (const typeReferenceDirective of sourceFile.typeReferenceDirectives) { - const name: string = sourceFile.text.substring( - typeReferenceDirective.pos, - typeReferenceDirective.end + if (sourceFile?.fileName) { + const { + fileName, + typeReferenceDirectives, + libReferenceDirectives, + text: sourceFileText + } = sourceFile; + if (!seenFilenames.has(fileName)) { + seenFilenames.add(fileName); + + for (const typeReferenceDirective of typeReferenceDirectives) { + const name: string | undefined = this._getReferenceDirectiveFromSourceFile( + sourceFileText, + typeReferenceDirective, + onlyIncludeExplicitlyPreserved ); - this._dtsTypeReferenceDirectives.add(name); + if (name) { + this._dtsTypeReferenceDirectives.add(name); + } } - for (const libReferenceDirective of sourceFile.libReferenceDirectives) { - const name: string = sourceFile.text.substring( - libReferenceDirective.pos, - libReferenceDirective.end + for (const libReferenceDirective of libReferenceDirectives) { + const reference: string | undefined = this._getReferenceDirectiveFromSourceFile( + sourceFileText, + libReferenceDirective, + onlyIncludeExplicitlyPreserved ); - this._dtsLibReferenceDirectives.add(name); + if (reference) { + this._dtsLibReferenceDirectives.add(reference); + } } } } } } + + private _getReferenceDirectiveFromSourceFile( + sourceFileText: string, + { pos, end, preserve }: ts.FileReference, + onlyIncludeExplicitlyPreserved: boolean + ): string | undefined { + const reference: string = sourceFileText.substring(pos, end); + if (preserve || !onlyIncludeExplicitlyPreserved) { + return reference; + } + } } diff --git a/apps/api-extractor/src/collector/CollectorEntity.ts b/apps/api-extractor/src/collector/CollectorEntity.ts index db52e1f4ad9..36bb07353d9 100644 --- a/apps/api-extractor/src/collector/CollectorEntity.ts +++ b/apps/api-extractor/src/collector/CollectorEntity.ts @@ -3,10 +3,12 @@ import * as ts from 'typescript'; +import { Sort } from '@rushstack/node-core-library'; + import { AstSymbol } from '../analyzer/AstSymbol'; import { Collector } from './Collector'; -import { Sort } from '@rushstack/node-core-library'; -import { AstEntity } from '../analyzer/AstEntity'; +import type { AstEntity } from '../analyzer/AstEntity'; +import { AstNamespaceExport } from '../analyzer/AstNamespaceExport'; /** * This is a data structure used by the Collector to track an AstEntity that may be emitted in the *.d.ts file. @@ -82,6 +84,11 @@ export class CollectorEntity { * such as "export class X { }" instead of "export { X }". */ public get shouldInlineExport(): boolean { + // We export the namespace directly + if (this.astEntity instanceof AstNamespaceExport) { + return true; + } + // We don't inline an AstImport if (this.astEntity instanceof AstSymbol) { // We don't inline a symbol with more than one exported name diff --git a/apps/api-extractor/src/collector/DeclarationMetadata.ts b/apps/api-extractor/src/collector/DeclarationMetadata.ts index f4510795a8b..f3d44de3a17 100644 --- a/apps/api-extractor/src/collector/DeclarationMetadata.ts +++ b/apps/api-extractor/src/collector/DeclarationMetadata.ts @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as tsdoc from '@microsoft/tsdoc'; -import { AstDeclaration } from '../analyzer/AstDeclaration'; +import type * as tsdoc from '@microsoft/tsdoc'; + +import type { AstDeclaration } from '../analyzer/AstDeclaration'; /** * Stores the Collector's additional analysis for a specific `AstDeclaration` signature. This object is assigned to diff --git a/apps/api-extractor/src/collector/MessageRouter.ts b/apps/api-extractor/src/collector/MessageRouter.ts index 1bb5dd8d3d4..bc09397c084 100644 --- a/apps/api-extractor/src/collector/MessageRouter.ts +++ b/apps/api-extractor/src/collector/MessageRouter.ts @@ -1,22 +1,23 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import colors from 'colors'; import * as ts from 'typescript'; -import * as tsdoc from '@microsoft/tsdoc'; + +import type * as tsdoc from '@microsoft/tsdoc'; import { Sort, InternalError } from '@rushstack/node-core-library'; +import { Colorize } from '@rushstack/terminal'; import { AstDeclaration } from '../analyzer/AstDeclaration'; -import { AstSymbol } from '../analyzer/AstSymbol'; +import type { AstSymbol } from '../analyzer/AstSymbol'; import { ExtractorMessage, ExtractorMessageCategory, - IExtractorMessageOptions, - IExtractorMessageProperties + type IExtractorMessageOptions, + type IExtractorMessageProperties } from '../api/ExtractorMessage'; -import { ExtractorMessageId, allExtractorMessageIds } from '../api/ExtractorMessageId'; -import { IExtractorMessagesConfig, IConfigMessageReportingRule } from '../api/IConfigFile'; -import { ISourceLocation, SourceMapper } from './SourceMapper'; +import { type ExtractorMessageId, allExtractorMessageIds } from '../api/ExtractorMessageId'; +import type { IExtractorMessagesConfig, IConfigMessageReportingRule } from '../api/IConfigFile'; +import type { ISourceLocation, SourceMapper } from './SourceMapper'; import { ExtractorLogLevel } from '../api/ExtractorLogLevel'; import { ConsoleMessageId } from '../api/ConsoleMessageId'; @@ -421,7 +422,7 @@ export class MessageRouter { /** * This returns all remaining messages that were flagged with `addToApiReportFile`, but which were not - * retreieved using `fetchAssociatedMessagesForReviewFile()`. + * retrieved using `fetchAssociatedMessagesForReviewFile()`. */ public fetchUnassociatedMessagesForReviewFile(): ExtractorMessage[] { const messagesForApiReportFile: ExtractorMessage[] = []; @@ -597,17 +598,17 @@ export class MessageRouter { switch (message.logLevel) { case ExtractorLogLevel.Error: - console.error(colors.red('Error: ' + messageText)); + console.error(Colorize.red('Error: ' + messageText)); break; case ExtractorLogLevel.Warning: - console.warn(colors.yellow('Warning: ' + messageText)); + console.warn(Colorize.yellow('Warning: ' + messageText)); break; case ExtractorLogLevel.Info: console.log(messageText); break; case ExtractorLogLevel.Verbose: if (this.showVerboseMessages) { - console.log(colors.cyan(messageText)); + console.log(Colorize.cyan(messageText)); } break; default: diff --git a/apps/api-extractor/src/collector/SourceMapper.ts b/apps/api-extractor/src/collector/SourceMapper.ts index e566c774b50..368ea15fa07 100644 --- a/apps/api-extractor/src/collector/SourceMapper.ts +++ b/apps/api-extractor/src/collector/SourceMapper.ts @@ -1,10 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; -import { SourceMapConsumer, RawSourceMap, MappingItem, Position } from 'source-map'; +import * as path from 'node:path'; + +import { SourceMapConsumer, type RawSourceMap, type MappingItem, type Position } from 'source-map'; +import type ts from 'typescript'; + import { FileSystem, InternalError, JsonFile, NewlineKind } from '@rushstack/node-core-library'; -import ts from 'typescript'; interface ISourceMap { sourceMapConsumer: SourceMapConsumer; diff --git a/apps/api-extractor/src/collector/SymbolMetadata.ts b/apps/api-extractor/src/collector/SymbolMetadata.ts index d28d731cc4d..8a467219360 100644 --- a/apps/api-extractor/src/collector/SymbolMetadata.ts +++ b/apps/api-extractor/src/collector/SymbolMetadata.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { ReleaseTag } from '@microsoft/api-extractor-model'; +import type { ReleaseTag } from '@microsoft/api-extractor-model'; /** * Constructor parameters for `SymbolMetadata`. diff --git a/apps/api-extractor/src/collector/WorkingPackage.ts b/apps/api-extractor/src/collector/WorkingPackage.ts index 7cd76fad9c8..d85e88e4c11 100644 --- a/apps/api-extractor/src/collector/WorkingPackage.ts +++ b/apps/api-extractor/src/collector/WorkingPackage.ts @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as ts from 'typescript'; -import * as tsdoc from '@microsoft/tsdoc'; +import type * as ts from 'typescript'; -import { INodePackageJson } from '@rushstack/node-core-library'; +import type * as tsdoc from '@microsoft/tsdoc'; +import type { INodePackageJson } from '@rushstack/node-core-library'; /** * Constructor options for WorkingPackage diff --git a/apps/api-extractor/src/enhancers/DocCommentEnhancer.ts b/apps/api-extractor/src/enhancers/DocCommentEnhancer.ts index ba96c9db803..17b228afe35 100644 --- a/apps/api-extractor/src/enhancers/DocCommentEnhancer.ts +++ b/apps/api-extractor/src/enhancers/DocCommentEnhancer.ts @@ -2,13 +2,14 @@ // See LICENSE in the project root for license information. import * as ts from 'typescript'; + import * as tsdoc from '@microsoft/tsdoc'; +import { ReleaseTag } from '@microsoft/api-extractor-model'; -import { Collector } from '../collector/Collector'; +import type { Collector } from '../collector/Collector'; import { AstSymbol } from '../analyzer/AstSymbol'; -import { AstDeclaration } from '../analyzer/AstDeclaration'; -import { ApiItemMetadata } from '../collector/ApiItemMetadata'; -import { ReleaseTag } from '@microsoft/api-extractor-model'; +import type { AstDeclaration } from '../analyzer/AstDeclaration'; +import type { ApiItemMetadata } from '../collector/ApiItemMetadata'; import { ExtractorMessageId } from '../api/ExtractorMessageId'; import { VisitorState } from '../collector/VisitorState'; import { ResolverFailure } from '../analyzer/AstReferenceResolver'; @@ -73,7 +74,7 @@ export class DocCommentEnhancer { // Constructors always do pretty much the same thing, so it's annoying to require people to write // descriptions for them. Instead, if the constructor lacks a TSDoc summary, then API Extractor // will auto-generate one. - metadata.needsDocumentation = false; + metadata.undocumented = false; // The class that contains this constructor const classDeclaration: AstDeclaration = astDeclaration.parent!; @@ -131,16 +132,43 @@ export class DocCommentEnhancer { ); } return; - } - - if (metadata.tsdocComment) { - // Require the summary to contain at least 10 non-spacing characters - metadata.needsDocumentation = !tsdoc.PlainTextEmitter.hasAnyTextContent( - metadata.tsdocComment.summarySection, - 10 - ); } else { - metadata.needsDocumentation = true; + // For non-constructor items, we will determine whether or not the item is documented as follows: + // 1. If it contains a summary section with at least 10 characters, then it is considered "documented". + // 2. If it contains an @inheritDoc tag, then it *may* be considered "documented", depending on whether or not + // the tag resolves to a "documented" API member. + // - Note: for external members, we cannot currently determine this, so we will consider the "documented" + // status to be unknown. + if (metadata.tsdocComment) { + if (tsdoc.PlainTextEmitter.hasAnyTextContent(metadata.tsdocComment.summarySection, 10)) { + // If the API item has a summary comment block (with at least 10 characters), mark it as "documented". + metadata.undocumented = false; + } else if (metadata.tsdocComment.inheritDocTag) { + if ( + this._refersToDeclarationInWorkingPackage( + metadata.tsdocComment.inheritDocTag.declarationReference + ) + ) { + // If the API item has an `@inheritDoc` comment that points to an API item in the working package, + // then the documentation contents should have already been copied from the target via `_applyInheritDoc`. + // The continued existence of the tag indicates that the declaration reference was invalid, and not + // documentation contents could be copied. + // An analyzer issue will have already been logged for this. + // We will treat such an API as "undocumented". + metadata.undocumented = true; + } else { + // If the API item has an `@inheritDoc` comment that points to an external API item, we cannot currently + // determine whether or not the target is "documented", so we cannot say definitively that this is "undocumented". + metadata.undocumented = false; + } + } else { + // If the API item has neither a summary comment block, nor an `@inheritDoc` comment, mark it as "undocumented". + metadata.undocumented = true; + } + } else { + // If there is no tsdoc comment at all, mark "undocumented". + metadata.undocumented = true; + } } } @@ -157,10 +185,7 @@ export class DocCommentEnhancer { // Is it referring to the working package? If not, we don't do any link validation, because // AstReferenceResolver doesn't support it yet (but ModelReferenceResolver does of course). // Tracked by: https://github.com/microsoft/rushstack/issues/1195 - if ( - node.codeDestination.packageName === undefined || - node.codeDestination.packageName === this._collector.workingPackage.name - ) { + if (this._refersToDeclarationInWorkingPackage(node.codeDestination)) { const referencedAstDeclaration: AstDeclaration | ResolverFailure = this._collector.astReferenceResolver.resolve(node.codeDestination); @@ -196,14 +221,8 @@ export class DocCommentEnhancer { return; } - // Is it referring to the working package? - if ( - !( - inheritDocTag.declarationReference.packageName === undefined || - inheritDocTag.declarationReference.packageName === this._collector.workingPackage.name - ) - ) { - // It's referencing an external package, so skip this inheritDoc tag, since AstReferenceResolver doesn't + if (!this._refersToDeclarationInWorkingPackage(inheritDocTag.declarationReference)) { + // The `@inheritDoc` tag is referencing an external package. Skip it, since AstReferenceResolver doesn't // support it yet. As a workaround, this tag will get handled later by api-documenter. // Tracked by: https://github.com/microsoft/rushstack/issues/1195 return; @@ -249,4 +268,16 @@ export class DocCommentEnhancer { targetDocComment.inheritDocTag = undefined; } + + /** + * Determines whether or not the provided declaration reference points to an item in the working package. + */ + private _refersToDeclarationInWorkingPackage( + declarationReference: tsdoc.DocDeclarationReference | undefined + ): boolean { + return ( + declarationReference?.packageName === undefined || + declarationReference.packageName === this._collector.workingPackage.name + ); + } } diff --git a/apps/api-extractor/src/enhancers/ValidationEnhancer.ts b/apps/api-extractor/src/enhancers/ValidationEnhancer.ts index 8795a4ec1ed..09d80f862cc 100644 --- a/apps/api-extractor/src/enhancers/ValidationEnhancer.ts +++ b/apps/api-extractor/src/enhancers/ValidationEnhancer.ts @@ -1,20 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; +import * as path from 'node:path'; + import * as ts from 'typescript'; -import { Collector } from '../collector/Collector'; +import { ReleaseTag } from '@microsoft/api-extractor-model'; + +import type { Collector } from '../collector/Collector'; import { AstSymbol } from '../analyzer/AstSymbol'; -import { AstDeclaration } from '../analyzer/AstDeclaration'; -import { ApiItemMetadata } from '../collector/ApiItemMetadata'; -import { SymbolMetadata } from '../collector/SymbolMetadata'; -import { CollectorEntity } from '../collector/CollectorEntity'; +import type { AstDeclaration } from '../analyzer/AstDeclaration'; +import type { ApiItemMetadata } from '../collector/ApiItemMetadata'; +import type { SymbolMetadata } from '../collector/SymbolMetadata'; +import type { CollectorEntity } from '../collector/CollectorEntity'; import { ExtractorMessageId } from '../api/ExtractorMessageId'; -import { ReleaseTag } from '@microsoft/api-extractor-model'; import { AstNamespaceImport } from '../analyzer/AstNamespaceImport'; -import { AstModuleExportInfo } from '../analyzer/AstModule'; -import { AstEntity } from '../analyzer/AstEntity'; +import type { IAstModuleExportInfo } from '../analyzer/AstModule'; +import type { AstEntity } from '../analyzer/AstEntity'; export class ValidationEnhancer { public static analyze(collector: Collector): void { @@ -47,7 +49,7 @@ export class ValidationEnhancer { // A namespace created using "import * as ___ from ___" const astNamespaceImport: AstNamespaceImport = entity.astEntity; - const astModuleExportInfo: AstModuleExportInfo = + const astModuleExportInfo: IAstModuleExportInfo = astNamespaceImport.fetchAstModuleExportInfo(collector); for (const namespaceMemberAstEntity of astModuleExportInfo.exportedLocalEntities.values()) { diff --git a/apps/api-extractor/src/generators/ApiModelGenerator.ts b/apps/api-extractor/src/generators/ApiModelGenerator.ts index 64c4fc56ceb..1ca3c340f9a 100644 --- a/apps/api-extractor/src/generators/ApiModelGenerator.ts +++ b/apps/api-extractor/src/generators/ApiModelGenerator.ts @@ -3,9 +3,11 @@ /* eslint-disable no-bitwise */ -import * as path from 'path'; +import * as path from 'node:path'; + import * as ts from 'typescript'; -import * as tsdoc from '@microsoft/tsdoc'; + +import type * as tsdoc from '@microsoft/tsdoc'; import { ApiModel, ApiClass, @@ -15,15 +17,15 @@ import { ApiNamespace, ApiInterface, ApiPropertySignature, - ApiItemContainerMixin, + type ApiItemContainerMixin, ReleaseTag, ApiProperty, ApiMethodSignature, - IApiParameterOptions, + type IApiParameterOptions, ApiEnum, ApiEnumMember, - IExcerptTokenRange, - IExcerptToken, + type IExcerptTokenRange, + type IExcerptToken, ApiConstructor, ApiConstructSignature, ApiFunction, @@ -31,23 +33,24 @@ import { ApiVariable, ApiTypeAlias, ApiCallSignature, - IApiTypeParameterOptions, + type IApiTypeParameterOptions, EnumMemberOrder } from '@microsoft/api-extractor-model'; import { Path } from '@rushstack/node-core-library'; -import { Collector } from '../collector/Collector'; -import { ISourceLocation } from '../collector/SourceMapper'; -import { AstDeclaration } from '../analyzer/AstDeclaration'; -import { ExcerptBuilder, IExcerptBuilderNodeToCapture } from './ExcerptBuilder'; +import type { Collector } from '../collector/Collector'; +import type { ISourceLocation } from '../collector/SourceMapper'; +import type { AstDeclaration } from '../analyzer/AstDeclaration'; +import { ExcerptBuilder, type IExcerptBuilderNodeToCapture } from './ExcerptBuilder'; import { AstSymbol } from '../analyzer/AstSymbol'; import { DeclarationReferenceGenerator } from './DeclarationReferenceGenerator'; -import { ApiItemMetadata } from '../collector/ApiItemMetadata'; -import { DeclarationMetadata } from '../collector/DeclarationMetadata'; +import type { ApiItemMetadata } from '../collector/ApiItemMetadata'; +import type { DeclarationMetadata } from '../collector/DeclarationMetadata'; import { AstNamespaceImport } from '../analyzer/AstNamespaceImport'; -import { AstEntity } from '../analyzer/AstEntity'; -import { AstModule } from '../analyzer/AstModule'; +import type { AstEntity } from '../analyzer/AstEntity'; +import type { AstModule } from '../analyzer/AstModule'; import { TypeScriptInternals } from '../analyzer/TypeScriptInternals'; +import type { ExtractorConfig } from '../api/ExtractorConfig'; interface IProcessAstEntityContext { name: string; @@ -55,15 +58,37 @@ interface IProcessAstEntityContext { parentApiItem: ApiItemContainerMixin; } +/** + * @beta + */ +export interface IApiModelGenerationOptions { + /** + * The release tags to trim. + */ + releaseTagsToTrim: Set; +} + export class ApiModelGenerator { private readonly _collector: Collector; private readonly _apiModel: ApiModel; private readonly _referenceGenerator: DeclarationReferenceGenerator; + private readonly _releaseTagsToTrim: Set | undefined; + + public readonly docModelEnabled: boolean; - public constructor(collector: Collector) { + public constructor(collector: Collector, extractorConfig: ExtractorConfig) { this._collector = collector; this._apiModel = new ApiModel(); this._referenceGenerator = new DeclarationReferenceGenerator(collector); + + const apiModelGenerationOptions: IApiModelGenerationOptions | undefined = + extractorConfig.docModelGenerationOptions; + if (apiModelGenerationOptions) { + this._releaseTagsToTrim = apiModelGenerationOptions.releaseTagsToTrim; + this.docModelEnabled = true; + } else { + this.docModelEnabled = false; + } } public get apiModel(): ApiModel { @@ -176,8 +201,8 @@ export class ApiModelGenerator { const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; - if (releaseTag === ReleaseTag.Internal || releaseTag === ReleaseTag.Alpha) { - return; // trim out items marked as "@internal" or "@alpha" + if (this._releaseTagsToTrim?.has(releaseTag)) { + return; } switch (astDeclaration.declaration.kind) { @@ -250,7 +275,14 @@ export class ApiModelGenerator { break; case ts.SyntaxKind.VariableDeclaration: - this._processApiVariable(astDeclaration, context); + // check for arrow functions in variable declaration + const functionDeclaration: ts.FunctionDeclaration | undefined = + this._tryFindFunctionDeclaration(astDeclaration); + if (functionDeclaration) { + this._processApiFunction(astDeclaration, context, functionDeclaration); + } else { + this._processApiVariable(astDeclaration, context); + } break; default: @@ -258,6 +290,13 @@ export class ApiModelGenerator { } } + private _tryFindFunctionDeclaration(astDeclaration: AstDeclaration): ts.FunctionDeclaration | undefined { + const children: readonly ts.Node[] = astDeclaration.declaration.getChildren( + astDeclaration.declaration.getSourceFile() + ); + return children.find(ts.isFunctionTypeNode) as ts.FunctionDeclaration | undefined; + } + private _processChildDeclarations(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { for (const childDeclaration of astDeclaration.children) { this._processDeclaration(childDeclaration, { @@ -544,7 +583,11 @@ export class ApiModelGenerator { } } - private _processApiFunction(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + private _processApiFunction( + astDeclaration: AstDeclaration, + context: IProcessAstEntityContext, + altFunctionDeclaration?: ts.FunctionDeclaration + ): void { const { name, isExported, parentApiItem } = context; const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration); @@ -554,7 +597,7 @@ export class ApiModelGenerator { if (apiFunction === undefined) { const functionDeclaration: ts.FunctionDeclaration = - astDeclaration.declaration as ts.FunctionDeclaration; + altFunctionDeclaration ?? (astDeclaration.declaration as ts.FunctionDeclaration); const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; diff --git a/apps/api-extractor/src/generators/ApiReportGenerator.ts b/apps/api-extractor/src/generators/ApiReportGenerator.ts index 6c99bdee3db..b42293d0084 100644 --- a/apps/api-extractor/src/generators/ApiReportGenerator.ts +++ b/apps/api-extractor/src/generators/ApiReportGenerator.ts @@ -2,24 +2,28 @@ // See LICENSE in the project root for license information. import * as ts from 'typescript'; -import { Text, InternalError } from '@rushstack/node-core-library'; + import { ReleaseTag } from '@microsoft/api-extractor-model'; +import { Text, InternalError } from '@rushstack/node-core-library'; import { Collector } from '../collector/Collector'; import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers'; import { Span } from '../analyzer/Span'; -import { CollectorEntity } from '../collector/CollectorEntity'; +import type { CollectorEntity } from '../collector/CollectorEntity'; import { AstDeclaration } from '../analyzer/AstDeclaration'; -import { ApiItemMetadata } from '../collector/ApiItemMetadata'; +import type { ApiItemMetadata } from '../collector/ApiItemMetadata'; import { AstImport } from '../analyzer/AstImport'; import { AstSymbol } from '../analyzer/AstSymbol'; -import { ExtractorMessage } from '../api/ExtractorMessage'; +import type { ExtractorMessage } from '../api/ExtractorMessage'; import { IndentedWriter } from './IndentedWriter'; import { DtsEmitHelpers } from './DtsEmitHelpers'; import { AstNamespaceImport } from '../analyzer/AstNamespaceImport'; -import { AstEntity } from '../analyzer/AstEntity'; -import { AstModuleExportInfo } from '../analyzer/AstModule'; +import type { AstEntity } from '../analyzer/AstEntity'; +import type { IAstModuleExportInfo } from '../analyzer/AstModule'; import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter'; +import { ExtractorMessageId } from '../api/ExtractorMessageId'; +import type { ApiReportVariant } from '../api/IConfigFile'; +import type { SymbolMetadata } from '../collector/SymbolMetadata'; export class ApiReportGenerator { private static _trimSpacesRegExp: RegExp = / +$/gm; @@ -41,13 +45,26 @@ export class ApiReportGenerator { return normalizedActual === normalizedExpected; } - public static generateReviewFileContent(collector: Collector): string { + /** + * Generates and returns the API report contents as a string. + * + * @param reportVariant - The release level with which the report is associated. + * Can also be viewed as the minimal release level of items that should be included in the report. + */ + public static generateReviewFileContent(collector: Collector, reportVariant: ApiReportVariant): string { const writer: IndentedWriter = new IndentedWriter(); writer.trimLeadingSpaces = true; + function capitalizeFirstLetter(input: string): string { + return input === '' ? '' : `${input[0].toLocaleUpperCase()}${input.slice(1)}`; + } + + // For backwards compatibility, don't emit "complete" in report text for untrimmed reports. + const releaseLevelPrefix: string = + reportVariant === 'complete' ? '' : `${capitalizeFirstLetter(reportVariant)} `; writer.writeLine( [ - `## API Report File for "${collector.workingPackage.name}"`, + `## ${releaseLevelPrefix}API Report File for "${collector.workingPackage.name}"`, ``, `> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).`, `` @@ -78,6 +95,13 @@ export class ApiReportGenerator { // Emit the regular declarations for (const entity of collector.entities) { const astEntity: AstEntity = entity.astEntity; + const symbolMetadata: SymbolMetadata | undefined = collector.tryFetchMetadataForAstEntity(astEntity); + const maxEffectiveReleaseTag: ReleaseTag = symbolMetadata?.maxEffectiveReleaseTag ?? ReleaseTag.None; + + if (!this._shouldIncludeReleaseTag(maxEffectiveReleaseTag, reportVariant)) { + continue; + } + if (entity.consumable || collector.extractorConfig.apiReportIncludeForgottenExports) { // First, collect the list of export names for this symbol. When reporting messages with // ExtractorMessage.properties.exportName, this will enable us to emit the warning comments alongside @@ -118,25 +142,27 @@ export class ApiReportGenerator { messagesToReport.push(message); } - writer.ensureSkippedLine(); - writer.write(ApiReportGenerator._getAedocSynopsis(collector, astDeclaration, messagesToReport)); + if (this._shouldIncludeDeclaration(collector, astDeclaration, reportVariant)) { + writer.ensureSkippedLine(); + writer.write(ApiReportGenerator._getAedocSynopsis(collector, astDeclaration, messagesToReport)); - const span: Span = new Span(astDeclaration.declaration); + const span: Span = new Span(astDeclaration.declaration); - const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration); - if (apiItemMetadata.isPreapproved) { - ApiReportGenerator._modifySpanForPreapproved(span); - } else { - ApiReportGenerator._modifySpan(collector, span, entity, astDeclaration, false); - } + const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration); + if (apiItemMetadata.isPreapproved) { + ApiReportGenerator._modifySpanForPreapproved(span); + } else { + ApiReportGenerator._modifySpan(collector, span, entity, astDeclaration, false, reportVariant); + } - span.writeModifiedText(writer); - writer.ensureNewLine(); + span.writeModifiedText(writer); + writer.ensureNewLine(); + } } } if (astEntity instanceof AstNamespaceImport) { - const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector); + const astModuleExportInfo: IAstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector); if (entity.nameForEmit === undefined) { // This should never happen @@ -254,11 +280,11 @@ export class ApiReportGenerator { span: Span, entity: CollectorEntity, astDeclaration: AstDeclaration, - insideTypeLiteral: boolean + insideTypeLiteral: boolean, + reportVariant: ApiReportVariant ): void { // Should we process this declaration at all? - // eslint-disable-next-line no-bitwise - if ((astDeclaration.modifierFlags & ts.ModifierFlags.Private) !== 0) { + if (!ApiReportGenerator._shouldIncludeDeclaration(collector, astDeclaration, reportVariant)) { span.modification.skipAll(); return; } @@ -276,6 +302,14 @@ export class ApiReportGenerator { break; case ts.SyntaxKind.ExportKeyword: + if (DtsEmitHelpers.isExportKeywordInNamespaceExportDeclaration(span.node)) { + // This is an export declaration inside a namespace - preserve the export keyword + break; + } + // Otherwise, delete the export keyword -- we will re-add it below + span.modification.skipAll(); + break; + case ts.SyntaxKind.DefaultKeyword: case ts.SyntaxKind.DeclareKeyword: // Delete any explicit "export" or "declare" keywords -- we will re-add them below @@ -384,7 +418,8 @@ export class ApiReportGenerator { childSpan, entity, childAstDeclaration, - insideTypeLiteral + insideTypeLiteral, + reportVariant ); } ); @@ -401,31 +436,88 @@ export class ApiReportGenerator { astDeclaration ); - if (sortChildren) { - span.modification.sortChildren = true; - child.modification.sortKey = Collector.getSortKeyIgnoringUnderscore( - childAstDeclaration.astSymbol.localName - ); - } + if (ApiReportGenerator._shouldIncludeDeclaration(collector, childAstDeclaration, reportVariant)) { + if (sortChildren) { + span.modification.sortChildren = true; + child.modification.sortKey = Collector.getSortKeyIgnoringUnderscore( + childAstDeclaration.astSymbol.localName + ); + } - if (!insideTypeLiteral) { - const messagesToReport: ExtractorMessage[] = - collector.messageRouter.fetchAssociatedMessagesForReviewFile(childAstDeclaration); - const aedocSynopsis: string = ApiReportGenerator._getAedocSynopsis( - collector, - childAstDeclaration, - messagesToReport - ); + if (!insideTypeLiteral) { + const messagesToReport: ExtractorMessage[] = + collector.messageRouter.fetchAssociatedMessagesForReviewFile(childAstDeclaration); - child.modification.prefix = aedocSynopsis + child.modification.prefix; + // NOTE: This generates ae-undocumented messages as a side effect + const aedocSynopsis: string = ApiReportGenerator._getAedocSynopsis( + collector, + childAstDeclaration, + messagesToReport + ); + + child.modification.prefix = aedocSynopsis + child.modification.prefix; + } } } - ApiReportGenerator._modifySpan(collector, child, entity, childAstDeclaration, insideTypeLiteral); + ApiReportGenerator._modifySpan( + collector, + child, + entity, + childAstDeclaration, + insideTypeLiteral, + reportVariant + ); } } } + private static _shouldIncludeDeclaration( + collector: Collector, + astDeclaration: AstDeclaration, + reportVariant: ApiReportVariant + ): boolean { + // Private declarations are not included in the API report + // eslint-disable-next-line no-bitwise + if ((astDeclaration.modifierFlags & ts.ModifierFlags.Private) !== 0) { + return false; + } + + const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration); + + return this._shouldIncludeReleaseTag(apiItemMetadata.effectiveReleaseTag, reportVariant); + } + + private static _shouldIncludeReleaseTag(releaseTag: ReleaseTag, reportVariant: ApiReportVariant): boolean { + switch (reportVariant) { + case 'complete': + return true; + case 'alpha': + return ( + releaseTag === ReleaseTag.Alpha || + releaseTag === ReleaseTag.Beta || + releaseTag === ReleaseTag.Public || + // NOTE: No specified release tag is implicitly treated as `@public`. + releaseTag === ReleaseTag.None + ); + case 'beta': + return ( + releaseTag === ReleaseTag.Beta || + releaseTag === ReleaseTag.Public || + // NOTE: No specified release tag is implicitly treated as `@public`. + releaseTag === ReleaseTag.None + ); + case 'public': + return ( + releaseTag === ReleaseTag.Public || + // NOTE: No specified release tag is implicitly treated as `@public`. + releaseTag === ReleaseTag.None + ); + default: + throw new Error(`Unrecognized release level: ${reportVariant}`); + } + } + /** * For declarations marked as `@preapproved`, this is used instead of _modifySpan(). */ @@ -494,36 +586,65 @@ export class ApiReportGenerator { if (!collector.isAncillaryDeclaration(astDeclaration)) { const footerParts: string[] = []; const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration); + + // 1. Release tag (if present) if (!apiItemMetadata.releaseTagSameAsParent) { if (apiItemMetadata.effectiveReleaseTag !== ReleaseTag.None) { footerParts.push(ReleaseTag.getTagName(apiItemMetadata.effectiveReleaseTag)); } } - if (apiItemMetadata.isSealed) { + // 2. Enumerate configured tags, reporting standard system tags first and then other configured tags. + // Note that the ordering we handle the standard tags is important for backwards compatibility. + // Also note that we had special mechanisms for checking whether or not an item is documented with these tags, + // so they are checked specially. + const { + '@sealed': reportSealedTag, + '@virtual': reportVirtualTag, + '@override': reportOverrideTag, + '@eventProperty': reportEventPropertyTag, + '@deprecated': reportDeprecatedTag, + ...otherTagsToReport + } = collector.extractorConfig.tagsToReport; + + // 2.a Check for standard tags and report those that are both configured and present in the metadata. + if (reportSealedTag && apiItemMetadata.isSealed) { footerParts.push('@sealed'); } - - if (apiItemMetadata.isVirtual) { + if (reportVirtualTag && apiItemMetadata.isVirtual) { footerParts.push('@virtual'); } - - if (apiItemMetadata.isOverride) { + if (reportOverrideTag && apiItemMetadata.isOverride) { footerParts.push('@override'); } - - if (apiItemMetadata.isEventProperty) { + if (reportEventPropertyTag && apiItemMetadata.isEventProperty) { footerParts.push('@eventProperty'); } + if (reportDeprecatedTag && apiItemMetadata.tsdocComment?.deprecatedBlock) { + footerParts.push('@deprecated'); + } - if (apiItemMetadata.tsdocComment) { - if (apiItemMetadata.tsdocComment.deprecatedBlock) { - footerParts.push('@deprecated'); + // 2.b Check for other configured tags and report those that are present in the tsdoc metadata. + for (const [tag, reportTag] of Object.entries(otherTagsToReport)) { + if (reportTag) { + // If the tag was not handled specially, check if it is present in the metadata. + if (apiItemMetadata.tsdocComment?.customBlocks.some((block) => block.blockTag.tagName === tag)) { + footerParts.push(tag); + } else if (apiItemMetadata.tsdocComment?.modifierTagSet.hasTagName(tag)) { + footerParts.push(tag); + } } } - if (apiItemMetadata.needsDocumentation) { + // 3. If the item is undocumented, append notice at the end of the list + if (apiItemMetadata.undocumented) { footerParts.push('(undocumented)'); + + collector.messageRouter.addAnalyzerIssue( + ExtractorMessageId.Undocumented, + `Missing documentation for "${astDeclaration.astSymbol.localName}".`, + astDeclaration + ); } if (footerParts.length > 0) { diff --git a/apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts b/apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts index 6a41534fec7..6f136fb9829 100644 --- a/apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts +++ b/apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts @@ -3,6 +3,7 @@ /* eslint-disable no-bitwise */ import * as ts from 'typescript'; + import { DeclarationReference, ModuleSource, @@ -10,11 +11,12 @@ import { Navigation, Meaning } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference'; -import { INodePackageJson, InternalError } from '@rushstack/node-core-library'; +import { type INodePackageJson, InternalError } from '@rushstack/node-core-library'; + import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers'; import { TypeScriptInternals } from '../analyzer/TypeScriptInternals'; -import { Collector } from '../collector/Collector'; -import { CollectorEntity } from '../collector/CollectorEntity'; +import type { Collector } from '../collector/Collector'; +import type { CollectorEntity } from '../collector/CollectorEntity'; import { AstNamespaceImport } from '../analyzer/AstNamespaceImport'; export class DeclarationReferenceGenerator { diff --git a/apps/api-extractor/src/generators/DtsEmitHelpers.ts b/apps/api-extractor/src/generators/DtsEmitHelpers.ts index 8095297b80d..08dee27b812 100644 --- a/apps/api-extractor/src/generators/DtsEmitHelpers.ts +++ b/apps/api-extractor/src/generators/DtsEmitHelpers.ts @@ -4,13 +4,15 @@ import * as ts from 'typescript'; import { InternalError } from '@rushstack/node-core-library'; -import { CollectorEntity } from '../collector/CollectorEntity'; + +import type { CollectorEntity } from '../collector/CollectorEntity'; import { AstImport, AstImportKind } from '../analyzer/AstImport'; import { AstDeclaration } from '../analyzer/AstDeclaration'; -import { Collector } from '../collector/Collector'; -import { Span } from '../analyzer/Span'; -import { IndentedWriter } from './IndentedWriter'; +import type { Collector } from '../collector/Collector'; +import type { Span } from '../analyzer/Span'; +import type { IndentedWriter } from './IndentedWriter'; import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter'; +import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers'; /** * Some common code shared between DtsRollupGenerator and ApiReportGenerator. @@ -170,4 +172,22 @@ export class DtsEmitHelpers { } } } + + /** + * Checks if an export keyword is part of an ExportDeclaration inside a namespace + * (e.g., "export { Foo, Bar };" inside "declare namespace SDK { ... }"). + * In that case, the export keyword must be preserved, otherwise the output is invalid TypeScript. + */ + public static isExportKeywordInNamespaceExportDeclaration(node: ts.Node): boolean { + if (node.parent && ts.isExportDeclaration(node.parent)) { + const moduleBlock: ts.ModuleBlock | undefined = TypeScriptHelpers.findFirstParent( + node, + ts.SyntaxKind.ModuleBlock + ); + if (moduleBlock) { + return true; + } + } + return false; + } } diff --git a/apps/api-extractor/src/generators/DtsRollupGenerator.ts b/apps/api-extractor/src/generators/DtsRollupGenerator.ts index b60623524d1..390dd93013c 100644 --- a/apps/api-extractor/src/generators/DtsRollupGenerator.ts +++ b/apps/api-extractor/src/generators/DtsRollupGenerator.ts @@ -1,28 +1,27 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -/* eslint-disable no-bitwise */ - import * as ts from 'typescript'; -import { FileSystem, NewlineKind, InternalError } from '@rushstack/node-core-library'; + import { ReleaseTag } from '@microsoft/api-extractor-model'; +import { FileSystem, type NewlineKind, InternalError } from '@rushstack/node-core-library'; -import { Collector } from '../collector/Collector'; +import type { Collector } from '../collector/Collector'; import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers'; -import { IndentDocCommentScope, Span, SpanModification } from '../analyzer/Span'; +import { IndentDocCommentScope, Span, type SpanModification } from '../analyzer/Span'; import { AstImport } from '../analyzer/AstImport'; -import { CollectorEntity } from '../collector/CollectorEntity'; +import type { CollectorEntity } from '../collector/CollectorEntity'; import { AstDeclaration } from '../analyzer/AstDeclaration'; -import { ApiItemMetadata } from '../collector/ApiItemMetadata'; +import type { ApiItemMetadata } from '../collector/ApiItemMetadata'; import { AstSymbol } from '../analyzer/AstSymbol'; -import { SymbolMetadata } from '../collector/SymbolMetadata'; +import type { SymbolMetadata } from '../collector/SymbolMetadata'; import { IndentedWriter } from './IndentedWriter'; import { DtsEmitHelpers } from './DtsEmitHelpers'; -import { DeclarationMetadata } from '../collector/DeclarationMetadata'; +import type { DeclarationMetadata } from '../collector/DeclarationMetadata'; import { AstNamespaceImport } from '../analyzer/AstNamespaceImport'; -import { AstModuleExportInfo } from '../analyzer/AstModule'; +import type { IAstModuleExportInfo } from '../analyzer/AstModule'; import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter'; -import { AstEntity } from '../analyzer/AstEntity'; +import type { AstEntity } from '../analyzer/AstEntity'; /** * Used with DtsRollupGenerator.writeTypingsFile() @@ -105,18 +104,12 @@ export class DtsRollupGenerator { // Emit the imports for (const entity of collector.entities) { if (entity.astEntity instanceof AstImport) { + // Note: it isn't valid to trim imports based on their release tags. + // E.g. class Foo (`@public`) extends interface Bar (`@beta`) from some external library. + // API-Extractor cannot trim `import { Bar } from "external-library"` when generating its public rollup, + // or the export of `Foo` would include a broken reference to `Bar`. const astImport: AstImport = entity.astEntity; - - // For example, if the imported API comes from an external package that supports AEDoc, - // and it was marked as `@internal`, then don't emit it. - const symbolMetadata: SymbolMetadata | undefined = collector.tryFetchMetadataForAstEntity(astImport); - const maxEffectiveReleaseTag: ReleaseTag = symbolMetadata - ? symbolMetadata.maxEffectiveReleaseTag - : ReleaseTag.None; - - if (this._shouldIncludeReleaseTag(maxEffectiveReleaseTag, dtsKind)) { - DtsEmitHelpers.emitImport(writer, entity, astImport); - } + DtsEmitHelpers.emitImport(writer, entity, astImport); } } writer.ensureSkippedLine(); @@ -159,7 +152,7 @@ export class DtsRollupGenerator { } if (astEntity instanceof AstNamespaceImport) { - const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector); + const astModuleExportInfo: IAstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector); if (entity.nameForEmit === undefined) { // This should never happen @@ -209,6 +202,17 @@ export class DtsRollupGenerator { ); } + // If the entity's declaration won't be included, then neither should the namespace export it + // This fixes the issue encountered here: https://github.com/microsoft/rushstack/issues/2791 + const exportedSymbolMetadata: SymbolMetadata | undefined = + collector.tryFetchMetadataForAstEntity(exportedEntity); + const exportedMaxEffectiveReleaseTag: ReleaseTag = exportedSymbolMetadata + ? exportedSymbolMetadata.maxEffectiveReleaseTag + : ReleaseTag.None; + if (!this._shouldIncludeReleaseTag(exportedMaxEffectiveReleaseTag, dtsKind)) { + continue; + } + if (collectorEntity.nameForEmit === exportedName) { exportClauses.push(collectorEntity.nameForEmit); } else { @@ -266,6 +270,14 @@ export class DtsRollupGenerator { break; case ts.SyntaxKind.ExportKeyword: + if (DtsEmitHelpers.isExportKeywordInNamespaceExportDeclaration(span.node)) { + // This is an export declaration inside a namespace - preserve the export keyword + break; + } + // Otherwise, delete the export keyword -- we will re-add it below + span.modification.skipAll(); + break; + case ts.SyntaxKind.DefaultKeyword: case ts.SyntaxKind.DeclareKeyword: // Delete any explicit "export" or "declare" keywords -- we will re-add them below diff --git a/apps/api-extractor/src/generators/ExcerptBuilder.ts b/apps/api-extractor/src/generators/ExcerptBuilder.ts index 40e4a9c83e3..8b10b4674d1 100644 --- a/apps/api-extractor/src/generators/ExcerptBuilder.ts +++ b/apps/api-extractor/src/generators/ExcerptBuilder.ts @@ -2,12 +2,17 @@ // See LICENSE in the project root for license information. import * as ts from 'typescript'; -import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference'; -import { ExcerptTokenKind, IExcerptToken, IExcerptTokenRange } from '@microsoft/api-extractor-model'; + +import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference'; +import { + ExcerptTokenKind, + type IExcerptToken, + type IExcerptTokenRange +} from '@microsoft/api-extractor-model'; import { Span } from '../analyzer/Span'; -import { DeclarationReferenceGenerator } from './DeclarationReferenceGenerator'; -import { AstDeclaration } from '../analyzer/AstDeclaration'; +import type { DeclarationReferenceGenerator } from './DeclarationReferenceGenerator'; +import type { AstDeclaration } from '../analyzer/AstDeclaration'; /** * Used to provide ExcerptBuilder with a list of nodes whose token range we want to capture. diff --git a/apps/api-extractor/src/generators/IndentedWriter.ts b/apps/api-extractor/src/generators/IndentedWriter.ts index d2e60bff6bc..84c2a91e35b 100644 --- a/apps/api-extractor/src/generators/IndentedWriter.ts +++ b/apps/api-extractor/src/generators/IndentedWriter.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { StringBuilder, IStringBuilder } from '@rushstack/node-core-library'; +import { StringBuilder, type IStringBuilder } from '@rushstack/node-core-library'; /** * A utility for writing indented text. diff --git a/apps/api-extractor/src/index.ts b/apps/api-extractor/src/index.ts index cd65453daaa..657d112b6d9 100644 --- a/apps/api-extractor/src/index.ts +++ b/apps/api-extractor/src/index.ts @@ -11,27 +11,31 @@ export { ConsoleMessageId } from './api/ConsoleMessageId'; -export { CompilerState, ICompilerStateCreateOptions } from './api/CompilerState'; +export { CompilerState, type ICompilerStateCreateOptions } from './api/CompilerState'; -export { Extractor, IExtractorInvokeOptions, ExtractorResult } from './api/Extractor'; +export { Extractor, type IExtractorInvokeOptions, ExtractorResult } from './api/Extractor'; export { - IExtractorConfigPrepareOptions, - IExtractorConfigLoadForFolderOptions, + type IExtractorConfigApiReport, + type IExtractorConfigPrepareOptions, + type IExtractorConfigLoadForFolderOptions, ExtractorConfig } from './api/ExtractorConfig'; +export type { IApiModelGenerationOptions } from './generators/ApiModelGenerator'; + export { ExtractorLogLevel } from './api/ExtractorLogLevel'; export { ExtractorMessage, - IExtractorMessageProperties, + type IExtractorMessageProperties, ExtractorMessageCategory } from './api/ExtractorMessage'; export { ExtractorMessageId } from './api/ExtractorMessageId'; -export { +export type { + ApiReportVariant, IConfigCompiler, IConfigApiReport, IConfigDocModel, @@ -40,5 +44,6 @@ export { IConfigMessageReportingRule, IConfigMessageReportingTable, IExtractorMessagesConfig, - IConfigFile + IConfigFile, + ReleaseTagForTrim } from './api/IConfigFile'; diff --git a/apps/api-extractor/src/schemas/api-extractor-defaults.json b/apps/api-extractor/src/schemas/api-extractor-defaults.json index d3a949e4c3f..bb6b6157aaf 100644 --- a/apps/api-extractor/src/schemas/api-extractor-defaults.json +++ b/apps/api-extractor/src/schemas/api-extractor-defaults.json @@ -30,7 +30,6 @@ "dtsRollup": { // ("enabled" is required) - "untrimmedFilePath": "/dist/.d.ts", "alphaTrimmedFilePath": "", "betaTrimmedFilePath": "", @@ -69,6 +68,9 @@ "logLevel": "warning", "addToApiReportFile": true }, + "ae-undocumented": { + "logLevel": "none" + }, "ae-unresolved-inheritdoc-reference": { "logLevel": "warning", "addToApiReportFile": true diff --git a/apps/api-extractor/src/schemas/api-extractor-template.json b/apps/api-extractor/src/schemas/api-extractor-template.json index c5b47c880aa..4e1f7be9e12 100644 --- a/apps/api-extractor/src/schemas/api-extractor-template.json +++ b/apps/api-extractor/src/schemas/api-extractor-template.json @@ -53,12 +53,19 @@ * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly - * imports library2. To avoid this, we can specify: + * imports library2. To avoid this, we might specify: * * "bundledPackages": [ "library2" ], * * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been * local files for library1. + * + * The "bundledPackages" elements may specify glob patterns using minimatch syntax. To ensure deterministic + * output, globs are expanded by matching explicitly declared top-level dependencies only. For example, + * the pattern below will NOT match "@my-company/example" unless it appears in a field such as "dependencies" + * or "devDependencies" of the project's package.json file: + * + * "bundledPackages": [ "@my-company/*" ], */ "bundledPackages": [], @@ -71,14 +78,6 @@ */ // "newlineKind": "crlf", - /** - * Set to true when invoking API Extractor's test harness. When `testMode` is true, the `toolVersion` field in the - * .api.json file is assigned an empty string to prevent spurious diffs in output files tracked for tests. - * - * DEFAULT VALUE: "false" - */ - // "testMode": false, - /** * Specifies how API Extractor sorts members of an enum when generating the .api.json file. By default, the output * files will be sorted alphabetically, which is "by-name". To keep the ordering in the source code, specify @@ -88,6 +87,14 @@ */ // "enumMemberOrder": "by-name", + /** + * Set to true when invoking API Extractor's test harness. When `testMode` is true, the `toolVersion` field in the + * .api.json file is assigned an empty string to prevent spurious diffs in output files tracked for tests. + * + * DEFAULT VALUE: "false" + */ + // "testMode": false, + /** * Determines how the TypeScript compiler engine will be invoked by API Extractor. */ @@ -138,15 +145,31 @@ "enabled": true /** - * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce - * a full file path. + * The base filename for the API report files, to be combined with "reportFolder" or "reportTempFolder" + * to produce the full file path. The "reportFileName" should not include any path separators such as + * "\" or "/". The "reportFileName" should not include a file extension, since API Extractor will automatically + * append an appropriate file extension such as ".api.md". If the "reportVariants" setting is used, then the + * file extension includes the variant name, for example "my-report.public.api.md" or "my-report.beta.api.md". + * The "complete" variant always uses the simple extension "my-report.api.md". * - * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". + * Previous versions of API Extractor required "reportFileName" to include the ".api.md" extension explicitly; + * for backwards compatibility, that is still accepted but will be discarded before applying the above rules. * * SUPPORTED TOKENS: , - * DEFAULT VALUE: ".api.md" + * DEFAULT VALUE: "" */ - // "reportFileName": ".api.md", + // "reportFileName": "", + + /** + * To support different approval requirements for different API levels, multiple "variants" of the API report can + * be generated. The "reportVariants" setting specifies a list of variants to be generated. If omitted, + * by default only the "complete" variant will be generated, which includes all @internal, @alpha, @beta, + * and @public items. Other possible variants are "alpha" (@alpha + @beta + @public), "beta" (@beta + @public), + * and "public" (@public only). + * + * DEFAULT VALUE: [ "complete" ] + */ + // "reportVariants": ["public", "beta"], /** * Specifies the folder where the API report file is written. The file name portion is determined by @@ -159,9 +182,9 @@ * prepend a folder token such as "". * * SUPPORTED TOKENS: , , - * DEFAULT VALUE: "/temp/" + * DEFAULT VALUE: "/etc/" */ - // "reportFolder": "/temp/", + // "reportFolder": "/etc/", /** * Specifies the folder where the temporary report file is written. The file name portion is determined by @@ -226,7 +249,7 @@ * item's file path is "api/ExtractorConfig.ts", the full URL file path would be * "https://github.com/microsoft/rushstack/tree/main/apps/api-extractor/api/ExtractorConfig.js". * - * Can be omitted if you don't need source code links in your API documentation reference. + * This setting can be omitted if you don't need source code links in your API documentation reference. * * SUPPORTED TOKENS: none * DEFAULT VALUE: "" @@ -261,6 +284,8 @@ * Specifies the output path for a .d.ts rollup file to be generated with trimming for an "alpha" release. * This file will include only declarations that are marked as "@public", "@beta", or "@alpha". * + * If the path is an empty string, then this file will not be written. + * * The path is resolved relative to the folder of the config file that contains the setting; to change this, * prepend a folder token such as "". * @@ -273,6 +298,8 @@ * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. * This file will include only declarations that are marked as "@public" or "@beta". * + * If the path is an empty string, then this file will not be written. + * * The path is resolved relative to the folder of the config file that contains the setting; to change this, * prepend a folder token such as "". * diff --git a/apps/api-extractor/src/schemas/api-extractor.schema.json b/apps/api-extractor/src/schemas/api-extractor.schema.json index 2d64af8b313..20773498c59 100644 --- a/apps/api-extractor/src/schemas/api-extractor.schema.json +++ b/apps/api-extractor/src/schemas/api-extractor.schema.json @@ -24,13 +24,20 @@ }, "bundledPackages": { - "description": "A list of NPM package names whose exports should be treated as part of this package.", + "description": "A list of NPM package names whose exports should be treated as part of this package. Also supports glob patterns.", "type": "array", "items": { "type": "string" } }, + "newlineKind": { + "description": "Specifies what type of newlines API Extractor should use when writing output files. By default, the output files will be written with Windows-style newlines. To use POSIX-style newlines, specify \"lf\" instead. To use the OS's default newline kind, specify \"os\".", + "type": "string", + "enum": ["crlf", "lf", "os"], + "default": "crlf" + }, + "enumMemberOrder": { "description": "Specifies how API Extractor sorts the members of an enum when generating the .api.json doc model. \n 'by-name': sort the items according to the enum member name \n 'preserve': keep the original order that items appear in the source code", "type": "string", @@ -38,6 +45,11 @@ "default": "by-name" }, + "testMode": { + "description": "Set to true invoking API Extractor's test harness. When \"testMode\" is true, the \"toolVersion\" field in the .api.json file is assigned an empty string to prevent spurious diffs in output files tracked for tests.", + "type": "boolean" + }, + "compiler": { "description": "Determines how the TypeScript compiler engine will be invoked by API Extractor.", "type": "object", @@ -68,8 +80,17 @@ }, "reportFileName": { - "description": "The filename for the API report files. It will be combined with \"reportFolder\" or \"reportTempFolder\" to produce a full file path. The file extension should be \".api.md\", and the string should not contain a path separator such as \"\\\" or \"/\".", - "type": "string" + "description": "The base filename for the API report files, to be combined with \"reportFolder\" or \"reportTempFolder\" to produce the full file path. The \"reportFileName\" should not include any path separators such as \"\\\" or \"/\". The \"reportFileName\" should not include a file extension, since API Extractor will automatically append an appropriate file extension such as \".api.md\". If the \"reportVariants\" setting is used, then the file extension includes the variant name, for example \"my-report.public.api.md\" or \"my-report.beta.api.md\". The \"complete\" variant always uses the simple extension \"my-report.api.md\".\n\nPrevious versions of API Extractor required \"reportFileName\" to include the \".api.md\" extension explicitly; for backwards compatibility, that is still accepted but will be discarded before applying the above rules.", + "type": ["string"] + }, + + "reportVariants": { + "description": "To support different approval requirements for different API levels, multiple \"variants\" of the API report can be generated. The \"reportVariants\" setting specifies a list of variants to be generated. If omitted, by default only the \"complete\" variant will be generated, which includes all @internal, @alpha, @beta, and @public items. Other possible variants are \"alpha\" (@alpha + @beta + @public), \"beta\" (@beta + @public), and \"public\" (@public only).", + "type": "array", + "items": { + "type": "string", + "enum": ["public", "beta", "alpha", "complete"] + } }, "reportFolder": { @@ -85,6 +106,17 @@ "includeForgottenExports": { "description": "Whether \"forgotten exports\" should be included in the API report file. Forgotten exports are declarations flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to learn more.", "type": "boolean" + }, + + "tagsToReport": { + "description": "Specifies a list of TSDoc tags that should be reported in the API report file for items whose documentation contains them. This can be used to include standard TSDoc tags or custom ones. Specified tag names must begin with \"@\". By default, the following tags are reported: [@sealed, @virtual, @override, @eventProperty, @deprecated]. Tags will appear in the order they are specified in this list. Note that an item's release tag will always reported; this behavior cannot be overridden.", + "type": "object", + "patternProperties": { + "^@[^\\s]*$": { + "type": "boolean" + } + }, + "additionalProperties": false } }, "required": ["enabled"], @@ -110,6 +142,14 @@ "projectFolderUrl": { "description": "The base URL where the project's source code can be viewed on a website such as GitHub or Azure DevOps. This URL path corresponds to the `` path on disk. This URL is concatenated with the file paths serialized to the doc model to produce URL file paths to individual API items. For example, if the `projectFolderUrl` is \"https://github.com/microsoft/rushstack/tree/main/apps/api-extractor\" and an API item's file path is \"api/ExtractorConfig.ts\", the full URL file path would be \"https://github.com/microsoft/rushstack/tree/main/apps/api-extractor/api/ExtractorConfig.js\". Can be omitted if you don't need source code links in your API documentation reference.", "type": "string" + }, + "releaseTagsToTrim": { + "description": "Specifies a list of release tags that will be trimmed from the doc model. The default value is `[\"@internal\"]`.", + "type": "array", + "items": { + "enum": ["@internal", "@alpha", "@beta", "@public"] + }, + "uniqueItems": true } }, "required": ["enabled"], @@ -165,13 +205,6 @@ "additionalProperties": false }, - "newlineKind": { - "description": "Specifies what type of newlines API Extractor should use when writing output files. By default, the output files will be written with Windows-style newlines. To use POSIX-style newlines, specify \"lf\" instead. To use the OS's default newline kind, specify \"os\".", - "type": "string", - "enum": ["crlf", "lf", "os"], - "default": "crlf" - }, - "messages": { "description": "Configures how API Extractor reports error and warning messages produced during analysis.", "type": "object", @@ -190,11 +223,6 @@ } }, "additionalProperties": false - }, - - "testMode": { - "description": "Set to true invoking API Extractor's test harness. When \"testMode\" is true, the \"toolVersion\" field in the .api.json file is assigned an empty string to prevent spurious diffs in output files tracked for tests.", - "type": "boolean" } }, "required": ["mainEntryPointFilePath"], diff --git a/apps/api-extractor/src/start.ts b/apps/api-extractor/src/start.ts index f2ae9e9c45e..2274fea6b22 100644 --- a/apps/api-extractor/src/start.ts +++ b/apps/api-extractor/src/start.ts @@ -1,20 +1,23 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as os from 'os'; -import colors from 'colors'; +import * as os from 'node:os'; + +import { Colorize } from '@rushstack/terminal'; import { ApiExtractorCommandLine } from './cli/ApiExtractorCommandLine'; import { Extractor } from './api/Extractor'; console.log( os.EOL + - colors.bold(`api-extractor ${Extractor.version} ` + colors.cyan(' - https://api-extractor.com/') + os.EOL) + Colorize.bold( + `api-extractor ${Extractor.version} ` + Colorize.cyan(' - https://api-extractor.com/') + os.EOL + ) ); const parser: ApiExtractorCommandLine = new ApiExtractorCommandLine(); -parser.execute().catch((error) => { - console.error(colors.red(`An unexpected error occurred: ${error}`)); +parser.executeAsync().catch((error) => { + console.error(Colorize.red(`An unexpected error occurred: ${error}`)); process.exit(1); }); diff --git a/apps/api-extractor/tsconfig.json b/apps/api-extractor/tsconfig.json index fbc2f5c0a6c..1a33d17b873 100644 --- a/apps/api-extractor/tsconfig.json +++ b/apps/api-extractor/tsconfig.json @@ -1,7 +1,3 @@ { - "extends": "./node_modules/@rushstack/heft-node-rig/profiles/default/tsconfig-base.json", - - "compilerOptions": { - "types": ["heft-jest", "node"] - } + "extends": "./node_modules/decoupled-local-node-rig/profiles/default/tsconfig-base.json" } diff --git a/apps/cpu-profile-summarizer/.npmignore b/apps/cpu-profile-summarizer/.npmignore new file mode 100644 index 00000000000..bc349f9a4be --- /dev/null +++ b/apps/cpu-profile-summarizer/.npmignore @@ -0,0 +1,32 @@ +# THIS IS A STANDARD TEMPLATE FOR .npmignore FILES IN THIS REPO. + +# Ignore all files by default, to avoid accidentally publishing unintended files. +* + +# Use negative patterns to bring back the specific things we want to publish. +!/bin/** +!/lib/** +!/lib-*/** +!/dist/** + +!CHANGELOG.md +!CHANGELOG.json +!heft-plugin.json +!rush-plugin-manifest.json +!ThirdPartyNotice.txt + +# Ignore certain patterns that should not get published. +/dist/*.stats.* +/lib/**/test/ +/lib-*/**/test/ +*.test.js + +# NOTE: These don't need to be specified, because NPM includes them automatically. +# +# package.json +# README.md +# LICENSE + +# --------------------------------------------------------------------------- +# DO NOT MODIFY ABOVE THIS LINE! Add any project-specific overrides below. +# --------------------------------------------------------------------------- diff --git a/apps/cpu-profile-summarizer/CHANGELOG.json b/apps/cpu-profile-summarizer/CHANGELOG.json new file mode 100644 index 00000000000..7d4d7e94024 --- /dev/null +++ b/apps/cpu-profile-summarizer/CHANGELOG.json @@ -0,0 +1,653 @@ +{ + "name": "@rushstack/cpu-profile-summarizer", + "entries": [ + { + "version": "0.1.39", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.39", + "date": "Thu, 08 Jan 2026 01:12:30 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.6.10`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.10`" + } + ] + } + }, + { + "version": "0.1.38", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.38", + "date": "Wed, 07 Jan 2026 01:12:24 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.7`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.6.9`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.9`" + } + ] + } + }, + { + "version": "0.1.37", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.37", + "date": "Mon, 05 Jan 2026 16:12:49 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.6`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.6.8`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.8`" + } + ] + } + }, + { + "version": "0.1.36", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.36", + "date": "Sat, 06 Dec 2025 01:12:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.5`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.6.7`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.7`" + } + ] + } + }, + { + "version": "0.1.35", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.35", + "date": "Fri, 21 Nov 2025 16:13:56 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.4`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.6.6`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.6`" + } + ] + } + }, + { + "version": "0.1.34", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.34", + "date": "Wed, 12 Nov 2025 01:12:56 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.6.5`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.5`" + } + ] + } + }, + { + "version": "0.1.33", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.33", + "date": "Tue, 04 Nov 2025 08:15:14 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.6.4`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.4`" + } + ] + } + }, + { + "version": "0.1.32", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.32", + "date": "Fri, 24 Oct 2025 00:13:38 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.3`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.6.3`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.3`" + } + ] + } + }, + { + "version": "0.1.31", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.31", + "date": "Wed, 22 Oct 2025 00:57:54 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.2`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.6.2`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.2`" + } + ] + } + }, + { + "version": "0.1.30", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.30", + "date": "Wed, 08 Oct 2025 00:13:29 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.1`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.6.1`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.1`" + } + ] + } + }, + { + "version": "0.1.29", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.29", + "date": "Fri, 03 Oct 2025 20:10:00 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.0`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.6.0`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.1.0`" + } + ] + } + }, + { + "version": "0.1.28", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.28", + "date": "Tue, 30 Sep 2025 23:57:45 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.5`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.30`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `1.0.0`" + } + ] + } + }, + { + "version": "0.1.27", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.27", + "date": "Tue, 30 Sep 2025 20:33:51 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.4`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.29`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.75.0`" + } + ] + } + }, + { + "version": "0.1.26", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.26", + "date": "Fri, 12 Sep 2025 15:13:07 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.28`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.74.5`" + } + ] + } + }, + { + "version": "0.1.25", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.25", + "date": "Thu, 11 Sep 2025 00:22:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.3`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.27`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.74.4`" + } + ] + } + }, + { + "version": "0.1.24", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.24", + "date": "Tue, 19 Aug 2025 20:45:02 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.26`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.74.3`" + } + ] + } + }, + { + "version": "0.1.23", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.23", + "date": "Fri, 01 Aug 2025 00:12:48 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.25`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.74.2`" + } + ] + } + }, + { + "version": "0.1.22", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.22", + "date": "Wed, 23 Jul 2025 20:55:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.2`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.24`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.74.1`" + } + ] + } + }, + { + "version": "0.1.21", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.21", + "date": "Sat, 21 Jun 2025 00:13:15 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.23`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.74.0`" + } + ] + } + }, + { + "version": "0.1.20", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.20", + "date": "Tue, 13 May 2025 02:09:20 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.22`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.73.6`" + } + ] + } + }, + { + "version": "0.1.19", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.19", + "date": "Thu, 01 May 2025 15:11:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.21`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.73.5`" + } + ] + } + }, + { + "version": "0.1.18", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.18", + "date": "Thu, 01 May 2025 00:11:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.1`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.20`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.73.4`" + } + ] + } + }, + { + "version": "0.1.17", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.17", + "date": "Fri, 25 Apr 2025 00:11:32 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.19`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.73.3`" + } + ] + } + }, + { + "version": "0.1.16", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.16", + "date": "Mon, 21 Apr 2025 22:24:25 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.0`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.18`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.73.2`" + } + ] + } + }, + { + "version": "0.1.15", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.15", + "date": "Thu, 17 Apr 2025 00:11:21 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.17`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.73.1`" + } + ] + } + }, + { + "version": "0.1.14", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.14", + "date": "Tue, 15 Apr 2025 15:11:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.16`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.73.0`" + } + ] + } + }, + { + "version": "0.1.13", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.13", + "date": "Wed, 09 Apr 2025 00:11:03 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.15`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.72.0`" + } + ] + } + }, + { + "version": "0.1.12", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.12", + "date": "Fri, 04 Apr 2025 18:34:35 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.14`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.71.2`" + } + ] + } + }, + { + "version": "0.1.11", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.11", + "date": "Tue, 25 Mar 2025 15:11:15 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.7`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.13`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.71.1`" + } + ] + } + }, + { + "version": "0.1.10", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.10", + "date": "Wed, 12 Mar 2025 22:41:36 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.12`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.71.0`" + } + ] + } + }, + { + "version": "0.1.9", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.9", + "date": "Wed, 12 Mar 2025 00:11:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.11`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.70.1`" + } + ] + } + }, + { + "version": "0.1.8", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.8", + "date": "Tue, 11 Mar 2025 02:12:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.6`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.10`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.70.0`" + } + ] + } + }, + { + "version": "0.1.7", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.7", + "date": "Tue, 11 Mar 2025 00:11:25 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.9`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.69.3`" + } + ] + } + }, + { + "version": "0.1.6", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.6", + "date": "Sat, 01 Mar 2025 05:00:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.8`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.69.2`" + } + ] + } + }, + { + "version": "0.1.5", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.5", + "date": "Thu, 27 Feb 2025 01:10:39 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.7`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.69.1`" + } + ] + } + }, + { + "version": "0.1.4", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.4", + "date": "Wed, 26 Feb 2025 16:11:11 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.6`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.69.0`" + } + ] + } + }, + { + "version": "0.1.3", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.3", + "date": "Sat, 22 Feb 2025 01:11:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.5`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.18`" + } + ] + } + }, + { + "version": "0.1.2", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.2", + "date": "Wed, 19 Feb 2025 18:53:48 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.4`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.17`" + } + ] + } + }, + { + "version": "0.1.1", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.1", + "date": "Wed, 12 Feb 2025 01:10:52 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.5`" + }, + { + "comment": "Updating dependency \"@rushstack/worker-pool\" to `0.5.3`" + }, + { + "comment": "Updating dependency \"@rushstack/heft\" to `0.68.16`" + } + ] + } + }, + { + "version": "0.1.0", + "tag": "@rushstack/cpu-profile-summarizer_v0.1.0", + "date": "Sat, 08 Feb 2025 01:10:27 GMT", + "comments": { + "minor": [ + { + "comment": "Add new command line tool for summarizing data across a collection of .cpuprofile files." + } + ] + } + } + ] +} diff --git a/apps/cpu-profile-summarizer/CHANGELOG.md b/apps/cpu-profile-summarizer/CHANGELOG.md new file mode 100644 index 00000000000..bd243bd817d --- /dev/null +++ b/apps/cpu-profile-summarizer/CHANGELOG.md @@ -0,0 +1,206 @@ +# Change Log - @rushstack/cpu-profile-summarizer + +This log was last generated on Thu, 08 Jan 2026 01:12:30 GMT and should not be manually modified. + +## 0.1.39 +Thu, 08 Jan 2026 01:12:30 GMT + +_Version update only_ + +## 0.1.38 +Wed, 07 Jan 2026 01:12:24 GMT + +_Version update only_ + +## 0.1.37 +Mon, 05 Jan 2026 16:12:49 GMT + +_Version update only_ + +## 0.1.36 +Sat, 06 Dec 2025 01:12:28 GMT + +_Version update only_ + +## 0.1.35 +Fri, 21 Nov 2025 16:13:56 GMT + +_Version update only_ + +## 0.1.34 +Wed, 12 Nov 2025 01:12:56 GMT + +_Version update only_ + +## 0.1.33 +Tue, 04 Nov 2025 08:15:14 GMT + +_Version update only_ + +## 0.1.32 +Fri, 24 Oct 2025 00:13:38 GMT + +_Version update only_ + +## 0.1.31 +Wed, 22 Oct 2025 00:57:54 GMT + +_Version update only_ + +## 0.1.30 +Wed, 08 Oct 2025 00:13:29 GMT + +_Version update only_ + +## 0.1.29 +Fri, 03 Oct 2025 20:10:00 GMT + +_Version update only_ + +## 0.1.28 +Tue, 30 Sep 2025 23:57:45 GMT + +_Version update only_ + +## 0.1.27 +Tue, 30 Sep 2025 20:33:51 GMT + +_Version update only_ + +## 0.1.26 +Fri, 12 Sep 2025 15:13:07 GMT + +_Version update only_ + +## 0.1.25 +Thu, 11 Sep 2025 00:22:31 GMT + +_Version update only_ + +## 0.1.24 +Tue, 19 Aug 2025 20:45:02 GMT + +_Version update only_ + +## 0.1.23 +Fri, 01 Aug 2025 00:12:48 GMT + +_Version update only_ + +## 0.1.22 +Wed, 23 Jul 2025 20:55:57 GMT + +_Version update only_ + +## 0.1.21 +Sat, 21 Jun 2025 00:13:15 GMT + +_Version update only_ + +## 0.1.20 +Tue, 13 May 2025 02:09:20 GMT + +_Version update only_ + +## 0.1.19 +Thu, 01 May 2025 15:11:33 GMT + +_Version update only_ + +## 0.1.18 +Thu, 01 May 2025 00:11:12 GMT + +_Version update only_ + +## 0.1.17 +Fri, 25 Apr 2025 00:11:32 GMT + +_Version update only_ + +## 0.1.16 +Mon, 21 Apr 2025 22:24:25 GMT + +_Version update only_ + +## 0.1.15 +Thu, 17 Apr 2025 00:11:21 GMT + +_Version update only_ + +## 0.1.14 +Tue, 15 Apr 2025 15:11:57 GMT + +_Version update only_ + +## 0.1.13 +Wed, 09 Apr 2025 00:11:03 GMT + +_Version update only_ + +## 0.1.12 +Fri, 04 Apr 2025 18:34:35 GMT + +_Version update only_ + +## 0.1.11 +Tue, 25 Mar 2025 15:11:15 GMT + +_Version update only_ + +## 0.1.10 +Wed, 12 Mar 2025 22:41:36 GMT + +_Version update only_ + +## 0.1.9 +Wed, 12 Mar 2025 00:11:31 GMT + +_Version update only_ + +## 0.1.8 +Tue, 11 Mar 2025 02:12:33 GMT + +_Version update only_ + +## 0.1.7 +Tue, 11 Mar 2025 00:11:25 GMT + +_Version update only_ + +## 0.1.6 +Sat, 01 Mar 2025 05:00:09 GMT + +_Version update only_ + +## 0.1.5 +Thu, 27 Feb 2025 01:10:39 GMT + +_Version update only_ + +## 0.1.4 +Wed, 26 Feb 2025 16:11:11 GMT + +_Version update only_ + +## 0.1.3 +Sat, 22 Feb 2025 01:11:12 GMT + +_Version update only_ + +## 0.1.2 +Wed, 19 Feb 2025 18:53:48 GMT + +_Version update only_ + +## 0.1.1 +Wed, 12 Feb 2025 01:10:52 GMT + +_Version update only_ + +## 0.1.0 +Sat, 08 Feb 2025 01:10:27 GMT + +### Minor changes + +- Add new command line tool for summarizing data across a collection of .cpuprofile files. + diff --git a/apps/cpu-profile-summarizer/LICENSE b/apps/cpu-profile-summarizer/LICENSE new file mode 100644 index 00000000000..f354f2bbba9 --- /dev/null +++ b/apps/cpu-profile-summarizer/LICENSE @@ -0,0 +1,24 @@ +@rushstack/cpu-profile-summarizer + +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/apps/cpu-profile-summarizer/README.md b/apps/cpu-profile-summarizer/README.md new file mode 100644 index 00000000000..b7c59218377 --- /dev/null +++ b/apps/cpu-profile-summarizer/README.md @@ -0,0 +1,26 @@ +# @rushstack/cpu-profile-summarizer + +> 🚨 _EARLY PREVIEW RELEASE_ 🚨 +> +> Not all features are implemented yet. To provide suggestions, please +> [create a GitHub issue](https://github.com/microsoft/rushstack/issues/new/choose). +> If you have questions, see the [Rush Stack Help page](https://rushstack.io/pages/help/support/) +> for support resources. + +The `cpu-profile-summarizer` command line tool helps you: + +- Collate self/total CPU usage statistics for an entire monorepo worth of V8 .cpuprofile files + +## Usage + +It's recommended to install this package globally: + +``` +# Install the NPM package +npm install -g @rushstack/cpu-profile-summarizer + +# Process a folder of cpuprofile files into a summary tsv file +cpu-profile-summarizer --input FOLDER --output FILE.tsv +``` + +The output file is in the tab-separated values (tsv) format. \ No newline at end of file diff --git a/apps/cpu-profile-summarizer/bin/cpu-profile-aggregator b/apps/cpu-profile-summarizer/bin/cpu-profile-aggregator new file mode 100644 index 00000000000..aee68e80224 --- /dev/null +++ b/apps/cpu-profile-summarizer/bin/cpu-profile-aggregator @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../lib/start.js'); diff --git a/apps/cpu-profile-summarizer/config/jest.config.json b/apps/cpu-profile-summarizer/config/jest.config.json new file mode 100644 index 00000000000..d1749681d90 --- /dev/null +++ b/apps/cpu-profile-summarizer/config/jest.config.json @@ -0,0 +1,3 @@ +{ + "extends": "local-node-rig/profiles/default/config/jest.config.json" +} diff --git a/apps/cpu-profile-summarizer/config/rig.json b/apps/cpu-profile-summarizer/config/rig.json new file mode 100644 index 00000000000..165ffb001f5 --- /dev/null +++ b/apps/cpu-profile-summarizer/config/rig.json @@ -0,0 +1,7 @@ +{ + // The "rig.json" file directs tools to look for their config files in an external package. + // Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + + "rigPackageName": "local-node-rig" +} diff --git a/apps/cpu-profile-summarizer/eslint.config.js b/apps/cpu-profile-summarizer/eslint.config.js new file mode 100644 index 00000000000..ceb5a1bee40 --- /dev/null +++ b/apps/cpu-profile-summarizer/eslint.config.js @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +const nodeTrustedToolProfile = require('local-node-rig/profiles/default/includes/eslint/flat/profile/node-trusted-tool'); +const friendlyLocalsMixin = require('local-node-rig/profiles/default/includes/eslint/flat/mixins/friendly-locals'); + +module.exports = [ + ...nodeTrustedToolProfile, + ...friendlyLocalsMixin, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + tsconfigRootDir: __dirname + } + }, + rules: { + 'no-console': 'off' + } + } +]; diff --git a/apps/cpu-profile-summarizer/package.json b/apps/cpu-profile-summarizer/package.json new file mode 100644 index 00000000000..1c84e8a5707 --- /dev/null +++ b/apps/cpu-profile-summarizer/package.json @@ -0,0 +1,29 @@ +{ + "name": "@rushstack/cpu-profile-summarizer", + "version": "0.1.39", + "description": "CLI tool for running analytics on multiple V8 .cpuprofile files", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/rushstack.git", + "directory": "apps/cpu-profile-summarizer" + }, + "bin": { + "cpu-profile-summarizer": "./bin/cpu-profile-summarizer" + }, + "license": "MIT", + "scripts": { + "start": "node lib/start", + "build": "heft build --clean", + "_phase:build": "heft run --only build -- --clean", + "_phase:test": "heft run --only test -- --clean" + }, + "dependencies": { + "@rushstack/ts-command-line": "workspace:*", + "@rushstack/worker-pool": "workspace:*" + }, + "devDependencies": { + "@rushstack/heft": "workspace:*", + "eslint": "~9.37.0", + "local-node-rig": "workspace:*" + } +} diff --git a/apps/cpu-profile-summarizer/src/protocol.ts b/apps/cpu-profile-summarizer/src/protocol.ts new file mode 100644 index 00000000000..eddd992e4ae --- /dev/null +++ b/apps/cpu-profile-summarizer/src/protocol.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { IProfileSummary } from './types'; + +/** + * A message sent to a worker to process a file (or shutdown). + */ +export type IMessageToWorker = string | false; + +/** + * A message sent from a worker to the main thread on success. + */ +export interface IWorkerSuccessMessage { + type: 'success'; + /** + * The file requested to be processed. + */ + file: string; + /** + * The summary of the profile data. + */ + data: IProfileSummary; +} + +/** + * A message sent from a worker to the main thread on error. + */ +export interface IWorkerErrorMessage { + type: 'error'; + /** + * The file requested to be processed. + */ + file: string; + /** + * The error stack trace or message. + */ + data: string; +} + +/** + * A message sent from a worker to the main thread. + */ +export type IMessageFromWorker = IWorkerSuccessMessage | IWorkerErrorMessage; diff --git a/apps/cpu-profile-summarizer/src/start.ts b/apps/cpu-profile-summarizer/src/start.ts new file mode 100644 index 00000000000..8360ef05e9f --- /dev/null +++ b/apps/cpu-profile-summarizer/src/start.ts @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { once } from 'node:events'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import type { Worker } from 'node:worker_threads'; + +import { + type CommandLineStringListParameter, + type IRequiredCommandLineStringParameter, + CommandLineParser +} from '@rushstack/ts-command-line'; +import { WorkerPool } from '@rushstack/worker-pool'; + +import type { IMessageFromWorker } from './protocol'; +import type { INodeSummary, IProfileSummary } from './types'; + +/** + * Merges summarized information from multiple profiles into a single collection. + * @param accumulator - The collection to merge the nodes into + * @param values - The nodes to merge + */ +function mergeProfileSummaries( + accumulator: Map, + values: Iterable<[string, INodeSummary]> +): void { + for (const [nodeId, node] of values) { + const existing: INodeSummary | undefined = accumulator.get(nodeId); + if (!existing) { + accumulator.set(nodeId, node); + } else { + existing.selfTime += node.selfTime; + existing.totalTime += node.totalTime; + } + } +} + +/** + * Scans a directory and its subdirectories for CPU profiles. + * @param baseDir - The directory to recursively search for CPU profiles + * @returns All .cpuprofile files found in the directory and its subdirectories + */ +function findProfiles(baseDir: string): string[] { + baseDir = path.resolve(baseDir); + + const files: string[] = []; + const directories: string[] = [baseDir]; + + for (const dir of directories) { + const entries: fs.Dirent[] = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isFile() && entry.name.endsWith('.cpuprofile')) { + files.push(`${dir}/${entry.name}`); + } else if (entry.isDirectory()) { + directories.push(`${dir}/${entry.name}`); + } + } + } + + return files; +} + +/** + * Processes a set of CPU profiles and aggregates the results. + * Uses a worker pool. + * @param profiles - The set of .cpuprofile files to process + * @returns A summary of the profiles + */ +async function processProfilesAsync(profiles: Set): Promise { + const maxWorkers: number = Math.min(profiles.size, os.availableParallelism()); + console.log(`Processing ${profiles.size} profiles using ${maxWorkers} workers...`); + const workerPool: WorkerPool = new WorkerPool({ + id: 'cpu-profile-summarizer', + maxWorkers, + workerScriptPath: path.resolve(__dirname, 'worker.js') + }); + + const summary: IProfileSummary = new Map(); + + let processed: number = 0; + await Promise.all( + Array.from(profiles, async (profile: string) => { + const worker: Worker = await workerPool.checkoutWorkerAsync(true); + const responsePromise: Promise = once(worker, 'message'); + worker.postMessage(profile); + const { 0: messageFromWorker } = await responsePromise; + if (messageFromWorker.type === 'error') { + console.error(`Error processing ${profile}: ${messageFromWorker.data}`); + } else { + ++processed; + console.log(`Processed ${profile} (${processed}/${profiles.size})`); + mergeProfileSummaries(summary, messageFromWorker.data); + } + workerPool.checkinWorker(worker); + }) + ); + + await workerPool.finishAsync(); + + return summary; +} + +function writeSummaryToTsv(tsvPath: string, summary: IProfileSummary): void { + const dir: string = path.dirname(tsvPath); + fs.mkdirSync(dir, { recursive: true }); + + let tsv: string = `Self Time (seconds)\tTotal Time (seconds)\tFunction Name\tURL\tLine\tColumn`; + for (const { selfTime, totalTime, functionName, url, lineNumber, columnNumber } of summary.values()) { + const selfSeconds: string = (selfTime / 1e6).toFixed(3); + const totalSeconds: string = (totalTime / 1e6).toFixed(3); + + tsv += `\n${selfSeconds}\t${totalSeconds}\t${functionName}\t${url}\t${lineNumber}\t${columnNumber}`; + } + + fs.writeFileSync(tsvPath, tsv, 'utf8'); + console.log(`Wrote summary to ${tsvPath}`); +} + +class CpuProfileSummarizerCommandLineParser extends CommandLineParser { + private readonly _inputParameter: CommandLineStringListParameter; + private readonly _outputParameter: IRequiredCommandLineStringParameter; + + public constructor() { + super({ + toolFilename: 'cpu-profile-summarizer', + toolDescription: + 'This tool summarizes the contents of multiple V8 .cpuprofile reports. ' + + 'For example, those generated by running `node --cpu-prof`.' + }); + + this._inputParameter = this.defineStringListParameter({ + parameterLongName: '--input', + parameterShortName: '-i', + description: 'The directory containing .cpuprofile files to summarize', + argumentName: 'DIR', + required: true + }); + + this._outputParameter = this.defineStringParameter({ + parameterLongName: '--output', + parameterShortName: '-o', + description: 'The output file to write the summary to', + argumentName: 'TSV_FILE', + required: true + }); + } + + protected override async onExecuteAsync(): Promise { + const input: readonly string[] = this._inputParameter.values; + const output: string = this._outputParameter.value; + + if (input.length === 0) { + throw new Error('No input directories provided'); + } + + const allProfiles: Set = new Set(); + for (const dir of input) { + const resolvedDir: string = path.resolve(dir); + console.log(`Collating CPU profiles from ${resolvedDir}...`); + const profiles: string[] = findProfiles(resolvedDir); + console.log(`Found ${profiles.length} profiles`); + for (const profile of profiles) { + allProfiles.add(profile); + } + } + + if (allProfiles.size === 0) { + throw new Error(`No profiles found`); + } + + const summary: IProfileSummary = await processProfilesAsync(allProfiles); + + writeSummaryToTsv(output, summary); + } +} + +process.exitCode = 1; +const parser: CpuProfileSummarizerCommandLineParser = new CpuProfileSummarizerCommandLineParser(); + +parser + .executeAsync() + .then((success: boolean) => { + if (success) { + process.exitCode = 0; + } + }) + .catch((error: Error) => { + console.error(error); + }); diff --git a/apps/cpu-profile-summarizer/src/types.ts b/apps/cpu-profile-summarizer/src/types.ts new file mode 100644 index 00000000000..34e54cbc498 --- /dev/null +++ b/apps/cpu-profile-summarizer/src/types.ts @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * Content of a V8 CPU Profile file + */ +export interface ICpuProfile { + nodes: INode[]; + /** + * Start time in microseconds. Offset is arbitrary. + */ + startTime: number; + /** + * End time in microseconds. Only relevant compared to `startTime`. + */ + endTime: number; + /** + * The identifier of the active node at each sample. + */ + samples: number[]; + /** + * The time deltas between samples, in microseconds. + */ + timeDeltas: number[]; +} + +/** + * A single stack frame in a CPU profile. + */ +export interface INode { + /** + * Identifier of the node. + * Referenced in the `children` field of other nodes and in the `samples` field of the profile. + */ + id: number; + /** + * The call frame of the function that was executing when the profile was taken. + */ + callFrame: ICallFrame; + /** + * The number of samples where this node was on top of the stack. + */ + hitCount: number; + /** + * The child nodes. + */ + children?: number[]; + /** + * Optional information about time spent on particular lines. + */ + positionTicks?: IPositionTick[]; +} + +/** + * The call frame of a profiler tick + */ +export interface ICallFrame { + /** + * The name of the function being executed, if any + */ + functionName: string; + /** + * An identifier for the script containing the function. + * Mostly relevant for new Function() and similar. + */ + scriptId: string; + /** + * The URL of the script being executed. + */ + url: string; + /** + * The line number in the script where the function is defined. + */ + lineNumber: number; + /** + * The column number in the line where the function is defined. + */ + columnNumber: number; +} + +/** + * Summarized information about a node in the CPU profile. + * Caller/callee information is discarded for brevity. + */ +export interface INodeSummary { + /** + * The name of the function being executed, if any + */ + functionName: string; + /** + * The URL of the script being executed + */ + url: string; + /** + * The line number in the script where the function is defined. + */ + lineNumber: number; + /** + * The column number in the line where the function is defined. + */ + columnNumber: number; + + /** + * Time spent while this function was the top of the stack, in microseconds. + */ + selfTime: number; + /** + * Time spent while this function was on the stack, in microseconds. + */ + totalTime: number; +} + +/** + * A collection of summarized information about nodes in a CPU profile. + * The keys contain the function name, url, line number, and column number of the node. + */ +export type IProfileSummary = Map; + +/** + * Information about a sample that is tied to a specific line within a function + */ +export interface IPositionTick { + /** + * The line number where the tick was recorded, within the script containing the executing function. + */ + line: number; + /** + * The number of samples where this line was active. + */ + ticks: number; +} diff --git a/apps/cpu-profile-summarizer/src/worker.ts b/apps/cpu-profile-summarizer/src/worker.ts new file mode 100644 index 00000000000..65b84db5a66 --- /dev/null +++ b/apps/cpu-profile-summarizer/src/worker.ts @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import fs from 'node:fs'; +import worker_threads from 'node:worker_threads'; + +import type { ICallFrame, ICpuProfile, INodeSummary, IProfileSummary } from './types'; +import type { IMessageToWorker } from './protocol'; + +interface ILocalTimeInfo { + self: number; + contributors: Set | undefined; +} + +/** + * Computes an identifier to use for summarizing call frames. + * @param callFrame - The call frame to compute the ID for + * @returns A portable string identifying the call frame + */ +function computeCallFrameId(callFrame: ICallFrame): string { + const { url, lineNumber, columnNumber, functionName } = callFrame; + return `${url}\0${lineNumber}\0${columnNumber}\0${functionName}`; +} + +/** + * Adds the contents of a .cpuprofile file to a summary. + * @param filePath - The path to the .cpuprofile file to read + * @param accumulator - The summary to add the profile to + */ +function addFileToSummary(filePath: string, accumulator: IProfileSummary): void { + const profile: ICpuProfile = JSON.parse(fs.readFileSync(filePath, 'utf8')); + addProfileToSummary(profile, accumulator); +} + +/** + * Adds a CPU profile to a summary. + * @param profile - The profile to add + * @param accumulator - The summary to add the profile to + * @returns + */ +function addProfileToSummary(profile: ICpuProfile, accumulator: IProfileSummary): IProfileSummary { + const { nodes, samples, timeDeltas, startTime, endTime }: ICpuProfile = profile; + + const localTimes: ILocalTimeInfo[] = []; + const nodeIdToIndex: Map = new Map(); + + function getIndexFromNodeId(id: number): number { + let index: number | undefined = nodeIdToIndex.get(id); + if (index === undefined) { + index = nodeIdToIndex.size; + nodeIdToIndex.set(id, index); + } + return index; + } + + for (let i: number = 0; i < nodes.length; i++) { + localTimes.push({ + self: 0, + contributors: undefined + }); + + const { id } = nodes[i]; + // Ensure that the mapping entry has been created. + getIndexFromNodeId(id); + } + + const duration: number = endTime - startTime; + let lastNodeTime: number = duration - timeDeltas[0]; + for (let i: number = 0; i < timeDeltas.length - 1; i++) { + const sampleDuration: number = timeDeltas[i + 1]; + const localTime: ILocalTimeInfo = localTimes[getIndexFromNodeId(samples[i])]; + localTime.self += sampleDuration; + lastNodeTime -= sampleDuration; + } + + localTimes[getIndexFromNodeId(samples[samples.length - 1])].self += lastNodeTime; + + // Have to pick the maximum totalTime for a given frame, + const nodesByFrame: Map> = new Map(); + + for (let i: number = 0; i < nodes.length; i++) { + const { callFrame } = nodes[i]; + const frameId: string = computeCallFrameId(callFrame); + + const nodesForFrame: Set | undefined = nodesByFrame.get(frameId); + if (nodesForFrame) { + nodesForFrame.add(i); + } else { + nodesByFrame.set(frameId, new Set([i])); + } + } + + for (const [frameId, contributors] of nodesByFrame) { + // To compute the total time spent in a node, we need to sum the self time of all contributors. + // We can't simply add up total times because a frame can recurse. + let selfTime: number = 0; + let totalTime: number = 0; + + let selfIndex: number | undefined; + for (const contributor of contributors) { + if (selfIndex === undefined) { + // The first contributor to a frame will always be itself. + selfIndex = contributor; + } + + const localTime: ILocalTimeInfo = localTimes[contributor]; + selfTime += localTime.self; + } + + const queue: Set = new Set(contributors); + for (const nodeIndex of queue) { + totalTime += localTimes[nodeIndex].self; + const { children } = nodes[nodeIndex]; + if (children) { + for (const childId of children) { + const childIndex: number = getIndexFromNodeId(childId); + queue.add(childIndex); + } + } + } + + const frame: INodeSummary | undefined = accumulator.get(frameId); + if (!frame) { + if (selfIndex === undefined) { + throw new Error('selfIndex should not be undefined'); + } + + const { + callFrame: { functionName, url, lineNumber, columnNumber } + } = nodes[selfIndex]; + + accumulator.set(frameId, { + functionName, + url, + lineNumber, + columnNumber, + + selfTime, + totalTime + }); + } else { + frame.selfTime += selfTime; + frame.totalTime += totalTime; + } + } + + return accumulator; +} + +const { parentPort } = worker_threads; +if (parentPort) { + const messageHandler = (message: IMessageToWorker): void => { + if (message === false) { + // Shutdown signal. + parentPort.removeListener('message', messageHandler); + parentPort.close(); + return; + } + + try { + const summary: IProfileSummary = new Map(); + addFileToSummary(message, summary); + parentPort.postMessage({ file: message, data: summary }); + } catch (error) { + parentPort.postMessage({ + file: message, + data: error.stack || error.message + }); + } + }; + + parentPort.on('message', messageHandler); +} diff --git a/apps/cpu-profile-summarizer/tsconfig.json b/apps/cpu-profile-summarizer/tsconfig.json new file mode 100644 index 00000000000..dac21d04081 --- /dev/null +++ b/apps/cpu-profile-summarizer/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/local-node-rig/profiles/default/tsconfig-base.json" +} diff --git a/apps/heft/.eslintrc.js b/apps/heft/.eslintrc.js deleted file mode 100644 index 4c934799d67..00000000000 --- a/apps/heft/.eslintrc.js +++ /dev/null @@ -1,10 +0,0 @@ -// This is a workaround for https://github.com/eslint/eslint/issues/3458 -require('@rushstack/eslint-config/patch/modern-module-resolution'); - -module.exports = { - extends: [ - '@rushstack/eslint-config/profile/node-trusted-tool', - '@rushstack/eslint-config/mixins/friendly-locals' - ], - parserOptions: { tsconfigRootDir: __dirname } -}; diff --git a/apps/heft/.npmignore b/apps/heft/.npmignore index 26245a35adf..3b7d5ed9526 100644 --- a/apps/heft/.npmignore +++ b/apps/heft/.npmignore @@ -8,6 +8,11 @@ !/lib/** !/lib-*/** !/dist/** + +!CHANGELOG.md +!CHANGELOG.json +!heft-plugin.json +!rush-plugin-manifest.json !ThirdPartyNotice.txt # Ignore certain patterns that should not get published. @@ -19,14 +24,13 @@ # NOTE: These don't need to be specified, because NPM includes them automatically. # # package.json -# README (and its variants) -# CHANGELOG (and its variants) -# LICENSE / LICENCE +# README.md +# LICENSE -#-------------------------------------------- -# DO NOT MODIFY THE TEMPLATE ABOVE THIS LINE -#-------------------------------------------- +# --------------------------------------------------------------------------- +# DO NOT MODIFY ABOVE THIS LINE! Add any project-specific overrides below. +# --------------------------------------------------------------------------- -# (Add your project-specific overrides here) !/includes/** !UPGRADING.md + diff --git a/apps/heft/CHANGELOG.json b/apps/heft/CHANGELOG.json index 70a61578e79..ae011ea42b0 100644 --- a/apps/heft/CHANGELOG.json +++ b/apps/heft/CHANGELOG.json @@ -1,6 +1,2658 @@ { "name": "@rushstack/heft", "entries": [ + { + "version": "1.1.10", + "tag": "@rushstack/heft_v1.1.10", + "date": "Thu, 08 Jan 2026 01:12:30 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.55.5`" + } + ] + } + }, + { + "version": "1.1.9", + "tag": "@rushstack/heft_v1.1.9", + "date": "Wed, 07 Jan 2026 01:12:24 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.19.7`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.5.7`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.21.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.7`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.55.4`" + } + ] + } + }, + { + "version": "1.1.8", + "tag": "@rushstack/heft_v1.1.8", + "date": "Mon, 05 Jan 2026 16:12:49 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.19.6`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.5.6`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.20.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.6`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.55.3`" + } + ] + } + }, + { + "version": "1.1.7", + "tag": "@rushstack/heft_v1.1.7", + "date": "Sat, 06 Dec 2025 01:12:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.19.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.19.1`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.5.5`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.5`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.5`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.55.2`" + } + ] + } + }, + { + "version": "1.1.6", + "tag": "@rushstack/heft_v1.1.6", + "date": "Fri, 21 Nov 2025 16:13:56 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.19.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.19.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.5.4`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.4`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.55.1`" + } + ] + } + }, + { + "version": "1.1.5", + "tag": "@rushstack/heft_v1.1.5", + "date": "Wed, 12 Nov 2025 01:12:56 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.55.0`" + } + ] + } + }, + { + "version": "1.1.4", + "tag": "@rushstack/heft_v1.1.4", + "date": "Tue, 04 Nov 2025 08:15:14 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.54.0`" + } + ] + } + }, + { + "version": "1.1.3", + "tag": "@rushstack/heft_v1.1.3", + "date": "Fri, 24 Oct 2025 00:13:38 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.19.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.18.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.5.3`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.3`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.53.3`" + } + ] + } + }, + { + "version": "1.1.2", + "tag": "@rushstack/heft_v1.1.2", + "date": "Wed, 22 Oct 2025 00:57:54 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.19.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.17.1`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.5.2`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.2`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.53.2`" + } + ] + } + }, + { + "version": "1.1.1", + "tag": "@rushstack/heft_v1.1.1", + "date": "Wed, 08 Oct 2025 00:13:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.19.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.17.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.5.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.1`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.53.1`" + } + ] + } + }, + { + "version": "1.1.0", + "tag": "@rushstack/heft_v1.1.0", + "date": "Fri, 03 Oct 2025 20:09:59 GMT", + "comments": { + "minor": [ + { + "comment": "Normalize import of builtin modules to use the `node:` protocol." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.19.0`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.16.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.5.0`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.6.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.19.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.1.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.53.0`" + } + ] + } + }, + { + "version": "1.0.0", + "tag": "@rushstack/heft_v1.0.0", + "date": "Tue, 30 Sep 2025 23:57:45 GMT", + "comments": { + "major": [ + { + "comment": "Release Heft version 1.0.0" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.18.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.15.1`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.4.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.18.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.5`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.15`" + } + ] + } + }, + { + "version": "0.75.0", + "tag": "@rushstack/heft_v0.75.0", + "date": "Tue, 30 Sep 2025 20:33:51 GMT", + "comments": { + "minor": [ + { + "comment": "Enhance logging in watch mode by allowing plugins to report detailed reasons for requesting rerun, e.g. specific changed files." + }, + { + "comment": "(BREAKING CHANGE) Make the `taskStart`/`taskFinish`/`phaseStart`/`phaseFinish` hooks synchronous to signify that they are not intended to be used for expensive work." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.18.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.15.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.4.0`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.17.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.4`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.14`" + } + ] + } + }, + { + "version": "0.74.5", + "tag": "@rushstack/heft_v0.74.5", + "date": "Fri, 12 Sep 2025 15:13:07 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.13`" + } + ] + } + }, + { + "version": "0.74.4", + "tag": "@rushstack/heft_v0.74.4", + "date": "Thu, 11 Sep 2025 00:22:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.18.4`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.3.2`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.16.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.3`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.12`" + } + ] + } + }, + { + "version": "0.74.3", + "tag": "@rushstack/heft_v0.74.3", + "date": "Tue, 19 Aug 2025 20:45:02 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.11`" + } + ] + } + }, + { + "version": "0.74.2", + "tag": "@rushstack/heft_v0.74.2", + "date": "Fri, 01 Aug 2025 00:12:48 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.10`" + } + ] + } + }, + { + "version": "0.74.1", + "tag": "@rushstack/heft_v0.74.1", + "date": "Wed, 23 Jul 2025 20:55:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.18.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.14.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.3.1`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.15.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.2`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.9`" + } + ] + } + }, + { + "version": "0.74.0", + "tag": "@rushstack/heft_v0.74.0", + "date": "Sat, 21 Jun 2025 00:13:15 GMT", + "comments": { + "minor": [ + { + "comment": "Added support for task and phase lifecycle events, `taskStart`, `taskFinish`, `phaseStart`, `phaseFinish`." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.3.0`" + } + ] + } + }, + { + "version": "0.73.6", + "tag": "@rushstack/heft_v0.73.6", + "date": "Tue, 13 May 2025 02:09:20 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.8`" + } + ] + } + }, + { + "version": "0.73.5", + "tag": "@rushstack/heft_v0.73.5", + "date": "Thu, 01 May 2025 15:11:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.7`" + } + ] + } + }, + { + "version": "0.73.4", + "tag": "@rushstack/heft_v0.73.4", + "date": "Thu, 01 May 2025 00:11:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.18.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.13.1`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.41`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.15.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.1`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.6`" + } + ] + } + }, + { + "version": "0.73.3", + "tag": "@rushstack/heft_v0.73.3", + "date": "Fri, 25 Apr 2025 00:11:32 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.18.1`" + } + ] + } + }, + { + "version": "0.73.2", + "tag": "@rushstack/heft_v0.73.2", + "date": "Mon, 21 Apr 2025 22:24:25 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `5.0.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.5`" + } + ] + } + }, + { + "version": "0.73.1", + "tag": "@rushstack/heft_v0.73.1", + "date": "Thu, 17 Apr 2025 00:11:21 GMT", + "comments": { + "patch": [ + { + "comment": "Update documentation for `extends`" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.18.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.4`" + } + ] + } + }, + { + "version": "0.73.0", + "tag": "@rushstack/heft_v0.73.0", + "date": "Tue, 15 Apr 2025 15:11:57 GMT", + "comments": { + "minor": [ + { + "comment": "Add `globAsync` to task run options." + } + ] + } + }, + { + "version": "0.72.0", + "tag": "@rushstack/heft_v0.72.0", + "date": "Wed, 09 Apr 2025 00:11:02 GMT", + "comments": { + "minor": [ + { + "comment": "Add a method `tryLoadProjectConfigurationFileAsync(options, terminal)` to `HeftConfiguration`." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.17.0`" + } + ] + } + }, + { + "version": "0.71.2", + "tag": "@rushstack/heft_v0.71.2", + "date": "Fri, 04 Apr 2025 18:34:35 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.3`" + } + ] + } + }, + { + "version": "0.71.1", + "tag": "@rushstack/heft_v0.71.1", + "date": "Tue, 25 Mar 2025 15:11:15 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.16.8`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.13.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.40`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.15.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.7`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.2`" + } + ] + } + }, + { + "version": "0.71.0", + "tag": "@rushstack/heft_v0.71.0", + "date": "Wed, 12 Mar 2025 22:41:36 GMT", + "comments": { + "minor": [ + { + "comment": "Add a `numberOfCores` property to `HeftConfiguration`." + } + ] + } + }, + { + "version": "0.70.1", + "tag": "@rushstack/heft_v0.70.1", + "date": "Wed, 12 Mar 2025 00:11:31 GMT", + "comments": { + "patch": [ + { + "comment": "Revert `useNodeJSResolver: true` to deal with plugins that have an `exports` field that doesn't contain `./package.json`." + } + ] + } + }, + { + "version": "0.70.0", + "tag": "@rushstack/heft_v0.70.0", + "date": "Tue, 11 Mar 2025 02:12:33 GMT", + "comments": { + "minor": [ + { + "comment": "Use `useNodeJSResolver: true` in `Import.resolvePackage` calls." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.16.7`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.12.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.39`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.15.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.6`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.1`" + } + ] + } + }, + { + "version": "0.69.3", + "tag": "@rushstack/heft_v0.69.3", + "date": "Tue, 11 Mar 2025 00:11:25 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.52.0`" + } + ] + } + }, + { + "version": "0.69.2", + "tag": "@rushstack/heft_v0.69.2", + "date": "Sat, 01 Mar 2025 05:00:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.51.1`" + } + ] + } + }, + { + "version": "0.69.1", + "tag": "@rushstack/heft_v0.69.1", + "date": "Thu, 27 Feb 2025 01:10:39 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.51.0`" + } + ] + } + }, + { + "version": "0.69.0", + "tag": "@rushstack/heft_v0.69.0", + "date": "Wed, 26 Feb 2025 16:11:11 GMT", + "comments": { + "minor": [ + { + "comment": "Expose `watchFs` on the incremental run options for tasks to give more flexibility when having Heft perform file watching than only invoking globs directly." + } + ] + } + }, + { + "version": "0.68.18", + "tag": "@rushstack/heft_v0.68.18", + "date": "Sat, 22 Feb 2025 01:11:11 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.50.1`" + } + ] + } + }, + { + "version": "0.68.17", + "tag": "@rushstack/heft_v0.68.17", + "date": "Wed, 19 Feb 2025 18:53:48 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.16.6`" + } + ] + } + }, + { + "version": "0.68.16", + "tag": "@rushstack/heft_v0.68.16", + "date": "Wed, 12 Feb 2025 01:10:52 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.16.5`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.38`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.15.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.5`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.50.0`" + } + ] + } + }, + { + "version": "0.68.15", + "tag": "@rushstack/heft_v0.68.15", + "date": "Thu, 30 Jan 2025 16:10:36 GMT", + "comments": { + "patch": [ + { + "comment": "Prefer `os.availableParallelism()` to `os.cpus().length`." + } + ] + } + }, + { + "version": "0.68.14", + "tag": "@rushstack/heft_v0.68.14", + "date": "Thu, 30 Jan 2025 01:11:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.16.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.11.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.37`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.6`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.4`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.49.2`" + } + ] + } + }, + { + "version": "0.68.13", + "tag": "@rushstack/heft_v0.68.13", + "date": "Thu, 09 Jan 2025 01:10:10 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.16.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.10.2`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.36`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.5`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.3`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.49.1`" + } + ] + } + }, + { + "version": "0.68.12", + "tag": "@rushstack/heft_v0.68.12", + "date": "Tue, 07 Jan 2025 22:17:32 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.49.0`" + } + ] + } + }, + { + "version": "0.68.11", + "tag": "@rushstack/heft_v0.68.11", + "date": "Sat, 14 Dec 2024 01:11:07 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.16.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.10.1`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.35`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.2`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.48.1`" + } + ] + } + }, + { + "version": "0.68.10", + "tag": "@rushstack/heft_v0.68.10", + "date": "Mon, 09 Dec 2024 20:31:43 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.16.1`" + } + ] + } + }, + { + "version": "0.68.9", + "tag": "@rushstack/heft_v0.68.9", + "date": "Tue, 03 Dec 2024 16:11:07 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.16.0`" + } + ] + } + }, + { + "version": "0.68.8", + "tag": "@rushstack/heft_v0.68.8", + "date": "Sat, 23 Nov 2024 01:18:55 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.48.0`" + } + ] + } + }, + { + "version": "0.68.7", + "tag": "@rushstack/heft_v0.68.7", + "date": "Fri, 22 Nov 2024 01:10:43 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.15.9`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.10.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.34`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.1`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.47.12`" + } + ] + } + }, + { + "version": "0.68.6", + "tag": "@rushstack/heft_v0.68.6", + "date": "Thu, 24 Oct 2024 00:15:47 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.15.8`" + } + ] + } + }, + { + "version": "0.68.5", + "tag": "@rushstack/heft_v0.68.5", + "date": "Mon, 21 Oct 2024 18:50:09 GMT", + "comments": { + "patch": [ + { + "comment": "Remove usage of true-case-path in favor of manually adjusting the drive letter casing to avoid confusing file system tracing tools with unnecessary directory enumerations." + } + ] + } + }, + { + "version": "0.68.4", + "tag": "@rushstack/heft_v0.68.4", + "date": "Thu, 17 Oct 2024 08:35:06 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.23.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.47.11`" + } + ] + } + }, + { + "version": "0.68.3", + "tag": "@rushstack/heft_v0.68.3", + "date": "Tue, 15 Oct 2024 00:12:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.47.10`" + } + ] + } + }, + { + "version": "0.68.2", + "tag": "@rushstack/heft_v0.68.2", + "date": "Wed, 02 Oct 2024 00:11:19 GMT", + "comments": { + "patch": [ + { + "comment": "Ensure `configHash` for file copy incremental cache file is portable." + } + ] + } + }, + { + "version": "0.68.1", + "tag": "@rushstack/heft_v0.68.1", + "date": "Tue, 01 Oct 2024 00:11:28 GMT", + "comments": { + "patch": [ + { + "comment": "Include all previous `inputFileVersions` in incremental copy files cache file during watch mode. Fix incorrect serialization of cache file for file copy." + } + ] + } + }, + { + "version": "0.68.0", + "tag": "@rushstack/heft_v0.68.0", + "date": "Mon, 30 Sep 2024 15:12:19 GMT", + "comments": { + "minor": [ + { + "comment": "Update file copy logic to use an incremental cache file in the temp directory for the current task to avoid unnecessary file writes." + } + ] + } + }, + { + "version": "0.67.2", + "tag": "@rushstack/heft_v0.67.2", + "date": "Fri, 13 Sep 2024 00:11:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.15.7`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.9.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.33`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.8`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.47.9`" + } + ] + } + }, + { + "version": "0.67.1", + "tag": "@rushstack/heft_v0.67.1", + "date": "Tue, 10 Sep 2024 20:08:11 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.15.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.8.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.32`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.7`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.47.8`" + } + ] + } + }, + { + "version": "0.67.0", + "tag": "@rushstack/heft_v0.67.0", + "date": "Wed, 21 Aug 2024 05:43:04 GMT", + "comments": { + "minor": [ + { + "comment": "Add a `slashNormalizedBuildFolderPath` property to `HeftConfiguration`." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.15.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.7.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.31`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.14.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.6`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.47.7`" + } + ] + } + }, + { + "version": "0.66.26", + "tag": "@rushstack/heft_v0.66.26", + "date": "Mon, 12 Aug 2024 22:16:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.15.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.6.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.30`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.13.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.5`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.47.6`" + } + ] + } + }, + { + "version": "0.66.25", + "tag": "@rushstack/heft_v0.66.25", + "date": "Fri, 02 Aug 2024 17:26:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.4`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.47.5`" + } + ] + } + }, + { + "version": "0.66.24", + "tag": "@rushstack/heft_v0.66.24", + "date": "Sat, 27 Jul 2024 00:10:27 GMT", + "comments": { + "patch": [ + { + "comment": "Include CHANGELOG.md in published releases again" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.15.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.5.1`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.29`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.5.3`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.13.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.3`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.47.4`" + } + ] + } + }, + { + "version": "0.66.23", + "tag": "@rushstack/heft_v0.66.23", + "date": "Wed, 24 Jul 2024 00:12:14 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.47.3`" + } + ] + } + }, + { + "version": "0.66.22", + "tag": "@rushstack/heft_v0.66.22", + "date": "Wed, 17 Jul 2024 06:55:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.15.2`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.28`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.13.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.2`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.47.2`" + } + ] + } + }, + { + "version": "0.66.21", + "tag": "@rushstack/heft_v0.66.21", + "date": "Wed, 17 Jul 2024 00:11:19 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.27`" + } + ] + } + }, + { + "version": "0.66.20", + "tag": "@rushstack/heft_v0.66.20", + "date": "Tue, 16 Jul 2024 00:36:21 GMT", + "comments": { + "patch": [ + { + "comment": "Update schemas/templates/heft.json to reflect new settings" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.15.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.5.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.26`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.13.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.1`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.47.1`" + } + ] + } + }, + { + "version": "0.66.19", + "tag": "@rushstack/heft_v0.66.19", + "date": "Thu, 27 Jun 2024 21:01:36 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.15.0`" + } + ] + } + }, + { + "version": "0.66.18", + "tag": "@rushstack/heft_v0.66.18", + "date": "Mon, 03 Jun 2024 23:43:15 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.47.0`" + } + ] + } + }, + { + "version": "0.66.17", + "tag": "@rushstack/heft_v0.66.17", + "date": "Thu, 30 May 2024 00:13:05 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.25`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.4.1`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.25`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.13.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.22.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.46.2`" + } + ] + } + }, + { + "version": "0.66.16", + "tag": "@rushstack/heft_v0.66.16", + "date": "Wed, 29 May 2024 02:03:50 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.24`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.4.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.24`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.12.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.5`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.46.1`" + } + ] + } + }, + { + "version": "0.66.15", + "tag": "@rushstack/heft_v0.66.15", + "date": "Wed, 29 May 2024 00:10:52 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.46.0`" + } + ] + } + }, + { + "version": "0.66.14", + "tag": "@rushstack/heft_v0.66.14", + "date": "Tue, 28 May 2024 15:10:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.23`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.3.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.23`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.12.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.4`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.45.1`" + } + ] + } + }, + { + "version": "0.66.13", + "tag": "@rushstack/heft_v0.66.13", + "date": "Tue, 28 May 2024 00:09:47 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.22`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.2.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.22`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.12.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.3`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.45.0`" + } + ] + } + }, + { + "version": "0.66.12", + "tag": "@rushstack/heft_v0.66.12", + "date": "Sat, 25 May 2024 04:54:07 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.21`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.1.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.21`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.12.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.2`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.44.1`" + } + ] + } + }, + { + "version": "0.66.11", + "tag": "@rushstack/heft_v0.66.11", + "date": "Fri, 24 May 2024 00:15:08 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.44.0`" + } + ] + } + }, + { + "version": "0.66.10", + "tag": "@rushstack/heft_v0.66.10", + "date": "Thu, 23 May 2024 02:26:56 GMT", + "comments": { + "patch": [ + { + "comment": "Update schema definitions to conform to strict schema-type validation." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.20`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `5.0.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.20`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.11.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.1`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.43.8`" + } + ] + } + }, + { + "version": "0.66.9", + "tag": "@rushstack/heft_v0.66.9", + "date": "Thu, 16 May 2024 15:10:22 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.21.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.43.7`" + } + ] + } + }, + { + "version": "0.66.8", + "tag": "@rushstack/heft_v0.66.8", + "date": "Wed, 15 May 2024 23:42:58 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.19`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.19`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.11.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.20.1`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.43.6`" + } + ] + } + }, + { + "version": "0.66.7", + "tag": "@rushstack/heft_v0.66.7", + "date": "Wed, 15 May 2024 06:04:17 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.18`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.3.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.18`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.10.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.20.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.43.5`" + } + ] + } + }, + { + "version": "0.66.6", + "tag": "@rushstack/heft_v0.66.6", + "date": "Fri, 10 May 2024 05:33:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.17`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.2.1`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.17`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.10.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.5`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.43.4`" + } + ] + } + }, + { + "version": "0.66.5", + "tag": "@rushstack/heft_v0.66.5", + "date": "Wed, 08 May 2024 22:23:50 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.4`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.43.3`" + } + ] + } + }, + { + "version": "0.66.4", + "tag": "@rushstack/heft_v0.66.4", + "date": "Mon, 06 May 2024 15:11:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.16`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.2.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.16`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.10.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.3`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.43.2`" + } + ] + } + }, + { + "version": "0.66.3", + "tag": "@rushstack/heft_v0.66.3", + "date": "Wed, 10 Apr 2024 15:10:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.15`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.1.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.15`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.10.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.2`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.43.1`" + } + ] + } + }, + { + "version": "0.66.2", + "tag": "@rushstack/heft_v0.66.2", + "date": "Tue, 19 Mar 2024 15:10:18 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.43.0`" + } + ] + } + }, + { + "version": "0.66.1", + "tag": "@rushstack/heft_v0.66.1", + "date": "Fri, 15 Mar 2024 00:12:40 GMT", + "comments": { + "patch": [ + { + "comment": "Fix internal error when run 'heft clean'" + } + ] + } + }, + { + "version": "0.66.0", + "tag": "@rushstack/heft_v0.66.0", + "date": "Tue, 05 Mar 2024 01:19:24 GMT", + "comments": { + "minor": [ + { + "comment": "Add new metrics value `bootDurationMs` to track the boot overhead of Heft before the action starts executing the subtasks. Update the start time used to compute `taskTotalExecutionMs` to be the beginning of operation graph execution. Fix the value of `taskTotalExecutionMs` field to be in milliseconds instead of seconds. Add new metrics value `totalUptimeMs` to track how long watch mode sessions are kept alive." + } + ] + } + }, + { + "version": "0.65.10", + "tag": "@rushstack/heft_v0.65.10", + "date": "Sun, 03 Mar 2024 20:58:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.1`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.42.3`" + } + ] + } + }, + { + "version": "0.65.9", + "tag": "@rushstack/heft_v0.65.9", + "date": "Sat, 02 Mar 2024 02:22:23 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.19.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.42.2`" + } + ] + } + }, + { + "version": "0.65.8", + "tag": "@rushstack/heft_v0.65.8", + "date": "Fri, 01 Mar 2024 01:10:08 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.18.1`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.42.1`" + } + ] + } + }, + { + "version": "0.65.7", + "tag": "@rushstack/heft_v0.65.7", + "date": "Thu, 29 Feb 2024 07:11:45 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.42.0`" + } + ] + } + }, + { + "version": "0.65.6", + "tag": "@rushstack/heft_v0.65.6", + "date": "Wed, 28 Feb 2024 16:09:27 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.18.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.41.1`" + } + ] + } + }, + { + "version": "0.65.5", + "tag": "@rushstack/heft_v0.65.5", + "date": "Sat, 24 Feb 2024 23:02:51 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.14`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.14`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.10.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.4`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.41.0`" + } + ] + } + }, + { + "version": "0.65.4", + "tag": "@rushstack/heft_v0.65.4", + "date": "Thu, 22 Feb 2024 01:36:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.13`" + } + ] + } + }, + { + "version": "0.65.3", + "tag": "@rushstack/heft_v0.65.3", + "date": "Wed, 21 Feb 2024 21:45:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.13`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.0.2`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.12`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.9.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.3`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.40.6`" + } + ] + } + }, + { + "version": "0.65.2", + "tag": "@rushstack/heft_v0.65.2", + "date": "Wed, 21 Feb 2024 08:55:47 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.40.5`" + } + ] + } + }, + { + "version": "0.65.1", + "tag": "@rushstack/heft_v0.65.1", + "date": "Tue, 20 Feb 2024 21:45:10 GMT", + "comments": { + "patch": [ + { + "comment": "Fix a recent regression causing `Error: Cannot find module 'colors/safe'` (GitHub #4525)" + }, + { + "comment": "Remove a no longer needed dependency on the `chokidar` package" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.12`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.0.1`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.11`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.8.1`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.40.4`" + } + ] + } + }, + { + "version": "0.65.0", + "tag": "@rushstack/heft_v0.65.0", + "date": "Tue, 20 Feb 2024 16:10:52 GMT", + "comments": { + "minor": [ + { + "comment": "Add a built-in `set-environment-variables-plugin` task plugin to set environment variables." + } + ] + } + }, + { + "version": "0.64.8", + "tag": "@rushstack/heft_v0.64.8", + "date": "Mon, 19 Feb 2024 21:54:26 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.11`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `4.0.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.10`" + }, + { + "comment": "Updating dependency \"@rushstack/terminal\" to `0.8.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.40.3`" + } + ] + } + }, + { + "version": "0.64.7", + "tag": "@rushstack/heft_v0.64.7", + "date": "Sat, 17 Feb 2024 06:24:34 GMT", + "comments": { + "patch": [ + { + "comment": "Fix broken link to API documentation" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.10`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.66.1`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.9`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.5.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.2`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.40.2`" + } + ] + } + }, + { + "version": "0.64.6", + "tag": "@rushstack/heft_v0.64.6", + "date": "Thu, 08 Feb 2024 01:09:21 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.9`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.66.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.8`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.40.1`" + } + ] + } + }, + { + "version": "0.64.5", + "tag": "@rushstack/heft_v0.64.5", + "date": "Wed, 07 Feb 2024 01:11:18 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.40.0`" + } + ] + } + }, + { + "version": "0.64.4", + "tag": "@rushstack/heft_v0.64.4", + "date": "Mon, 05 Feb 2024 23:46:52 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.8`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.65.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.7`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.39.5`" + } + ] + } + }, + { + "version": "0.64.3", + "tag": "@rushstack/heft_v0.64.3", + "date": "Thu, 25 Jan 2024 01:09:30 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.7`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.64.2`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.6`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.39.4`" + } + ] + } + }, + { + "version": "0.64.2", + "tag": "@rushstack/heft_v0.64.2", + "date": "Tue, 23 Jan 2024 20:12:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.64.1`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.5`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.39.3`" + } + ] + } + }, + { + "version": "0.64.1", + "tag": "@rushstack/heft_v0.64.1", + "date": "Tue, 23 Jan 2024 16:15:05 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.64.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.4`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.39.2`" + } + ] + } + }, + { + "version": "0.64.0", + "tag": "@rushstack/heft_v0.64.0", + "date": "Tue, 16 Jan 2024 18:30:10 GMT", + "comments": { + "minor": [ + { + "comment": "Add support for TypeScript 5.3" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.3`" + } + ] + } + }, + { + "version": "0.63.6", + "tag": "@rushstack/heft_v0.63.6", + "date": "Wed, 03 Jan 2024 00:31:18 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.63.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.2`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.39.1`" + } + ] + } + }, + { + "version": "0.63.5", + "tag": "@rushstack/heft_v0.63.5", + "date": "Wed, 20 Dec 2023 01:09:45 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.39.0`" + } + ] + } + }, + { + "version": "0.63.4", + "tag": "@rushstack/heft_v0.63.4", + "date": "Thu, 07 Dec 2023 03:44:13 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.62.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.1`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.38.5`" + } + ] + } + }, + { + "version": "0.63.3", + "tag": "@rushstack/heft_v0.63.3", + "date": "Tue, 05 Dec 2023 01:10:16 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.38.4`" + } + ] + } + }, + { + "version": "0.63.2", + "tag": "@rushstack/heft_v0.63.2", + "date": "Fri, 10 Nov 2023 18:02:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.38.3`" + } + ] + } + }, + { + "version": "0.63.1", + "tag": "@rushstack/heft_v0.63.1", + "date": "Wed, 01 Nov 2023 23:11:35 GMT", + "comments": { + "patch": [ + { + "comment": "Fix line endings in published package." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.1`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.38.2`" + } + ] + } + }, + { + "version": "0.63.0", + "tag": "@rushstack/heft_v0.63.0", + "date": "Mon, 30 Oct 2023 23:36:37 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue with parsing of the \"--debug\" and \"--unmanaged\" flags for Heft" + } + ], + "minor": [ + { + "comment": "[BREAKING CHANGE] Remove \"heft run\" short-parameters for \"--to\" (\"-t\"), \"--to-except\" (\"-T\"), and \"--only\" (\"-o\")." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.38.1`" + } + ] + } + }, + { + "version": "0.62.3", + "tag": "@rushstack/heft_v0.62.3", + "date": "Sun, 01 Oct 2023 02:56:29 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.38.0`" + } + ] + } + }, + { + "version": "0.62.2", + "tag": "@rushstack/heft_v0.62.2", + "date": "Sat, 30 Sep 2023 00:20:51 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.37.3`" + } + ] + } + }, + { + "version": "0.62.1", + "tag": "@rushstack/heft_v0.62.1", + "date": "Thu, 28 Sep 2023 20:53:17 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.61.0`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.2.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.37.2`" + } + ] + } + }, + { + "version": "0.62.0", + "tag": "@rushstack/heft_v0.62.0", + "date": "Wed, 27 Sep 2023 00:21:38 GMT", + "comments": { + "minor": [ + { + "comment": "(BREAKING API CHANGE) Remove the deprecated `cancellationToken` property of `IHeftTaskRunHookOptions`. Use `abortSignal` on that object instead." + } + ] + } + }, + { + "version": "0.61.3", + "tag": "@rushstack/heft_v0.61.3", + "date": "Tue, 26 Sep 2023 21:02:30 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where `heft clean` would crash with `ERR_ILLEGAL_CONSTRUCTOR`." + } + ] + } + }, + { + "version": "0.61.2", + "tag": "@rushstack/heft_v0.61.2", + "date": "Tue, 26 Sep 2023 09:30:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update type-only imports to include the type modifier." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.60.1`" + }, + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.1.2`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.5.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.16.1`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.37.1`" + } + ] + } + }, + { + "version": "0.61.1", + "tag": "@rushstack/heft_v0.61.1", + "date": "Mon, 25 Sep 2023 23:38:27 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.1.1`" + } + ] + } + }, + { + "version": "0.61.0", + "tag": "@rushstack/heft_v0.61.0", + "date": "Fri, 22 Sep 2023 00:05:50 GMT", + "comments": { + "minor": [ + { + "comment": "(BREAKING CHANGE): Rename task temp folder from \".\" to \"/\" to simplify caching phase outputs." + } + ] + } + }, + { + "version": "0.60.0", + "tag": "@rushstack/heft_v0.60.0", + "date": "Tue, 19 Sep 2023 15:21:51 GMT", + "comments": { + "minor": [ + { + "comment": "Allow Heft to communicate via IPC with a host process when running in watch mode. The host controls scheduling of incremental re-runs." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/operation-graph\" to `0.1.0`" + } + ] + } + }, + { + "version": "0.59.0", + "tag": "@rushstack/heft_v0.59.0", + "date": "Fri, 15 Sep 2023 00:36:58 GMT", + "comments": { + "minor": [ + { + "comment": "Update @types/node from 14 to 18" + } + ], + "patch": [ + { + "comment": "Migrate plugin name collision detection to the InternalHeftSession instance to allow multiple Heft sessions in the same process." + } + ], + "none": [ + { + "comment": "Avoid mutating config files after reading." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.14.0`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.60.0`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.5.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.16.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.37.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.4`" + } + ] + } + }, + { + "version": "0.58.2", + "tag": "@rushstack/heft_v0.58.2", + "date": "Tue, 08 Aug 2023 07:10:39 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.13.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.7`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.4.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.15.2`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.36.4`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.3`" + } + ] + } + }, + { + "version": "0.58.1", + "tag": "@rushstack/heft_v0.58.1", + "date": "Sat, 29 Jul 2023 00:22:50 GMT", + "comments": { + "patch": [ + { + "comment": "Fix the `toolFinish` lifecycle hook so that it is invoked after the `recordMetrics` hook, rather than before. Ensure that the `toolFinish` lifecycle hook is invoked if the user performs a graceful shutdown of Heft (e.g. via Ctrl+C)." + } + ] + } + }, + { + "version": "0.58.0", + "tag": "@rushstack/heft_v0.58.0", + "date": "Thu, 20 Jul 2023 20:47:28 GMT", + "comments": { + "minor": [ + { + "comment": "BREAKING CHANGE: Update the heft.json \"cleanFiles\" property and the delete-files-plugin to delete the contents of folders specified by \"sourcePath\" instead of deleting the folders themselves. To delete the folders, use the \"includeGlobs\" property to specify the folder to delete." + } + ] + } + }, + { + "version": "0.57.1", + "tag": "@rushstack/heft_v0.57.1", + "date": "Wed, 19 Jul 2023 00:20:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.13.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.6`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.36.3`" + } + ] + } + }, + { + "version": "0.57.0", + "tag": "@rushstack/heft_v0.57.0", + "date": "Thu, 13 Jul 2023 00:22:37 GMT", + "comments": { + "minor": [ + { + "comment": "Support `--clean` in watch mode. Cleaning in watch mode is now performed only during the first-pass of lifecycle or phase operations. Once the clean has been completed, `--clean` will be ignored until the command is restarted" + } + ] + } + }, + { + "version": "0.56.3", + "tag": "@rushstack/heft_v0.56.3", + "date": "Wed, 12 Jul 2023 15:20:39 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.36.2`" + } + ] + } + }, + { + "version": "0.56.2", + "tag": "@rushstack/heft_v0.56.2", + "date": "Fri, 07 Jul 2023 00:19:32 GMT", + "comments": { + "patch": [ + { + "comment": "Revise README.md and UPGRADING.md documentation" + } + ] + } + }, + { + "version": "0.56.1", + "tag": "@rushstack/heft_v0.56.1", + "date": "Thu, 06 Jul 2023 00:16:19 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.13.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.5`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.36.1`" + } + ] + } + }, + { + "version": "0.56.0", + "tag": "@rushstack/heft_v0.56.0", + "date": "Mon, 19 Jun 2023 22:40:21 GMT", + "comments": { + "minor": [ + { + "comment": "Use the `IRigConfig` interface in the `HeftConfiguration` object insteacd of the `RigConfig` class." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.13.0`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.4.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.36.0`" + } + ] + } + }, + { + "version": "0.55.2", + "tag": "@rushstack/heft_v0.55.2", + "date": "Thu, 15 Jun 2023 00:21:01 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.12.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.4`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.21`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.15.1`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.35.4`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.2`" + } + ] + } + }, + { + "version": "0.55.1", + "tag": "@rushstack/heft_v0.55.1", + "date": "Wed, 14 Jun 2023 00:19:41 GMT", + "comments": { + "patch": [ + { + "comment": "Add MockScopedLogger to help plugin authors with unit testing." + } + ] + } + }, + { + "version": "0.55.0", + "tag": "@rushstack/heft_v0.55.0", + "date": "Tue, 13 Jun 2023 15:17:20 GMT", + "comments": { + "minor": [ + { + "comment": "Remove the deprecated `cacheFolderPath` property from the session object." + } + ] + } + }, + { + "version": "0.54.0", + "tag": "@rushstack/heft_v0.54.0", + "date": "Tue, 13 Jun 2023 01:49:01 GMT", + "comments": { + "minor": [ + { + "comment": "Add plugin support for parameter short-names." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.15.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.35.3`" + } + ] + } + }, + { + "version": "0.53.1", + "tag": "@rushstack/heft_v0.53.1", + "date": "Fri, 09 Jun 2023 18:05:34 GMT", + "comments": { + "patch": [ + { + "comment": "Revise CHANGELOG.md to more clearly identify the breaking changes" + } + ] + } + }, + { + "version": "0.53.0", + "tag": "@rushstack/heft_v0.53.0", + "date": "Fri, 09 Jun 2023 00:19:49 GMT", + "comments": { + "patch": [ + { + "comment": "Update UPGRADING.md with new JSON schema URLs" + } + ], + "minor": [ + { + "comment": "(BREAKING CHANGE) Remove \"taskEvents\" heft.json configuration option, and replace it with directly referencing the included plugins. Please read https://github.com/microsoft/rushstack/blob/main/apps/heft/UPGRADING.md" + } + ] + } + }, + { + "version": "0.52.2", + "tag": "@rushstack/heft_v0.52.2", + "date": "Thu, 08 Jun 2023 15:21:17 GMT", + "comments": { + "patch": [ + { + "comment": "Provide a useful error message when encountering legacy Heft configurations" + } + ] + } + }, + { + "version": "0.52.1", + "tag": "@rushstack/heft_v0.52.1", + "date": "Thu, 08 Jun 2023 00:20:02 GMT", + "comments": { + "patch": [ + { + "comment": "Remove the concept of the cache folder, since it mostly just causes bugs." + } + ] + } + }, + { + "version": "0.52.0", + "tag": "@rushstack/heft_v0.52.0", + "date": "Wed, 07 Jun 2023 22:45:16 GMT", + "comments": { + "minor": [ + { + "comment": "Add a new API IHeftTaskSession.parsedCommandLine for accessing the invoked command name" + }, + { + "comment": "(BREAKING CHANGE) The built-in task NodeServicePlugin now supports the \"--serve\" mode with semantics similar to heft-webpack5-plugin. Please read https://github.com/microsoft/rushstack/blob/main/apps/heft/UPGRADING.md" + } + ], + "patch": [ + { + "comment": "Add action aliases support. Action aliases can be used to create custom \"heft \" commands which call existing Heft commands with optional default arguments." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/heft-config-file\" to `0.12.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.3`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.20`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.14.0`" + }, + { + "comment": "Updating dependency \"@microsoft/api-extractor\" to `7.35.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.1`" + } + ] + } + }, { "version": "0.51.0", "tag": "@rushstack/heft_v0.51.0", @@ -8,7 +2660,7 @@ "comments": { "minor": [ { - "comment": "Overhaul to support splitting single-project builds into more phases than \"build\" and \"test\", to align with Rush phased commands. See UPGRADING.md for details." + "comment": "(BREAKING CHANGE) Overhaul to support splitting single-project builds into more phases than \"build\" and \"test\", to align with Rush phased commands. Please read https://github.com/microsoft/rushstack/blob/main/apps/heft/UPGRADING.md" } ] } diff --git a/apps/heft/CHANGELOG.md b/apps/heft/CHANGELOG.md index eb77511aced..ec4a3cae715 100644 --- a/apps/heft/CHANGELOG.md +++ b/apps/heft/CHANGELOG.md @@ -1,13 +1,826 @@ # Change Log - @rushstack/heft -This log was last generated on Fri, 02 Jun 2023 02:01:12 GMT and should not be manually modified. +This log was last generated on Thu, 08 Jan 2026 01:12:30 GMT and should not be manually modified. + +## 1.1.10 +Thu, 08 Jan 2026 01:12:30 GMT + +_Version update only_ + +## 1.1.9 +Wed, 07 Jan 2026 01:12:24 GMT + +_Version update only_ + +## 1.1.8 +Mon, 05 Jan 2026 16:12:49 GMT + +_Version update only_ + +## 1.1.7 +Sat, 06 Dec 2025 01:12:28 GMT + +_Version update only_ + +## 1.1.6 +Fri, 21 Nov 2025 16:13:56 GMT + +_Version update only_ + +## 1.1.5 +Wed, 12 Nov 2025 01:12:56 GMT + +_Version update only_ + +## 1.1.4 +Tue, 04 Nov 2025 08:15:14 GMT + +_Version update only_ + +## 1.1.3 +Fri, 24 Oct 2025 00:13:38 GMT + +_Version update only_ + +## 1.1.2 +Wed, 22 Oct 2025 00:57:54 GMT + +_Version update only_ + +## 1.1.1 +Wed, 08 Oct 2025 00:13:28 GMT + +_Version update only_ + +## 1.1.0 +Fri, 03 Oct 2025 20:09:59 GMT + +### Minor changes + +- Normalize import of builtin modules to use the `node:` protocol. + +## 1.0.0 +Tue, 30 Sep 2025 23:57:45 GMT + +### Breaking changes + +- Release Heft version 1.0.0 + +## 0.75.0 +Tue, 30 Sep 2025 20:33:51 GMT + +### Minor changes + +- Enhance logging in watch mode by allowing plugins to report detailed reasons for requesting rerun, e.g. specific changed files. +- (BREAKING CHANGE) Make the `taskStart`/`taskFinish`/`phaseStart`/`phaseFinish` hooks synchronous to signify that they are not intended to be used for expensive work. + +## 0.74.5 +Fri, 12 Sep 2025 15:13:07 GMT + +_Version update only_ + +## 0.74.4 +Thu, 11 Sep 2025 00:22:31 GMT + +_Version update only_ + +## 0.74.3 +Tue, 19 Aug 2025 20:45:02 GMT + +_Version update only_ + +## 0.74.2 +Fri, 01 Aug 2025 00:12:48 GMT + +_Version update only_ + +## 0.74.1 +Wed, 23 Jul 2025 20:55:57 GMT + +_Version update only_ + +## 0.74.0 +Sat, 21 Jun 2025 00:13:15 GMT + +### Minor changes + +- Added support for task and phase lifecycle events, `taskStart`, `taskFinish`, `phaseStart`, `phaseFinish`. + +## 0.73.6 +Tue, 13 May 2025 02:09:20 GMT + +_Version update only_ + +## 0.73.5 +Thu, 01 May 2025 15:11:33 GMT + +_Version update only_ + +## 0.73.4 +Thu, 01 May 2025 00:11:12 GMT + +_Version update only_ + +## 0.73.3 +Fri, 25 Apr 2025 00:11:32 GMT + +_Version update only_ + +## 0.73.2 +Mon, 21 Apr 2025 22:24:25 GMT + +_Version update only_ + +## 0.73.1 +Thu, 17 Apr 2025 00:11:21 GMT + +### Patches + +- Update documentation for `extends` + +## 0.73.0 +Tue, 15 Apr 2025 15:11:57 GMT + +### Minor changes + +- Add `globAsync` to task run options. + +## 0.72.0 +Wed, 09 Apr 2025 00:11:02 GMT + +### Minor changes + +- Add a method `tryLoadProjectConfigurationFileAsync(options, terminal)` to `HeftConfiguration`. + +## 0.71.2 +Fri, 04 Apr 2025 18:34:35 GMT + +_Version update only_ + +## 0.71.1 +Tue, 25 Mar 2025 15:11:15 GMT + +_Version update only_ + +## 0.71.0 +Wed, 12 Mar 2025 22:41:36 GMT + +### Minor changes + +- Add a `numberOfCores` property to `HeftConfiguration`. + +## 0.70.1 +Wed, 12 Mar 2025 00:11:31 GMT + +### Patches + +- Revert `useNodeJSResolver: true` to deal with plugins that have an `exports` field that doesn't contain `./package.json`. + +## 0.70.0 +Tue, 11 Mar 2025 02:12:33 GMT + +### Minor changes + +- Use `useNodeJSResolver: true` in `Import.resolvePackage` calls. + +## 0.69.3 +Tue, 11 Mar 2025 00:11:25 GMT + +_Version update only_ + +## 0.69.2 +Sat, 01 Mar 2025 05:00:09 GMT + +_Version update only_ + +## 0.69.1 +Thu, 27 Feb 2025 01:10:39 GMT + +_Version update only_ + +## 0.69.0 +Wed, 26 Feb 2025 16:11:11 GMT + +### Minor changes + +- Expose `watchFs` on the incremental run options for tasks to give more flexibility when having Heft perform file watching than only invoking globs directly. + +## 0.68.18 +Sat, 22 Feb 2025 01:11:11 GMT + +_Version update only_ + +## 0.68.17 +Wed, 19 Feb 2025 18:53:48 GMT + +_Version update only_ + +## 0.68.16 +Wed, 12 Feb 2025 01:10:52 GMT + +_Version update only_ + +## 0.68.15 +Thu, 30 Jan 2025 16:10:36 GMT + +### Patches + +- Prefer `os.availableParallelism()` to `os.cpus().length`. + +## 0.68.14 +Thu, 30 Jan 2025 01:11:42 GMT + +_Version update only_ + +## 0.68.13 +Thu, 09 Jan 2025 01:10:10 GMT + +_Version update only_ + +## 0.68.12 +Tue, 07 Jan 2025 22:17:32 GMT + +_Version update only_ + +## 0.68.11 +Sat, 14 Dec 2024 01:11:07 GMT + +_Version update only_ + +## 0.68.10 +Mon, 09 Dec 2024 20:31:43 GMT + +_Version update only_ + +## 0.68.9 +Tue, 03 Dec 2024 16:11:07 GMT + +_Version update only_ + +## 0.68.8 +Sat, 23 Nov 2024 01:18:55 GMT + +_Version update only_ + +## 0.68.7 +Fri, 22 Nov 2024 01:10:43 GMT + +_Version update only_ + +## 0.68.6 +Thu, 24 Oct 2024 00:15:47 GMT + +_Version update only_ + +## 0.68.5 +Mon, 21 Oct 2024 18:50:09 GMT + +### Patches + +- Remove usage of true-case-path in favor of manually adjusting the drive letter casing to avoid confusing file system tracing tools with unnecessary directory enumerations. + +## 0.68.4 +Thu, 17 Oct 2024 08:35:06 GMT + +_Version update only_ + +## 0.68.3 +Tue, 15 Oct 2024 00:12:31 GMT + +_Version update only_ + +## 0.68.2 +Wed, 02 Oct 2024 00:11:19 GMT + +### Patches + +- Ensure `configHash` for file copy incremental cache file is portable. + +## 0.68.1 +Tue, 01 Oct 2024 00:11:28 GMT + +### Patches + +- Include all previous `inputFileVersions` in incremental copy files cache file during watch mode. Fix incorrect serialization of cache file for file copy. + +## 0.68.0 +Mon, 30 Sep 2024 15:12:19 GMT + +### Minor changes + +- Update file copy logic to use an incremental cache file in the temp directory for the current task to avoid unnecessary file writes. + +## 0.67.2 +Fri, 13 Sep 2024 00:11:42 GMT + +_Version update only_ + +## 0.67.1 +Tue, 10 Sep 2024 20:08:11 GMT + +_Version update only_ + +## 0.67.0 +Wed, 21 Aug 2024 05:43:04 GMT + +### Minor changes + +- Add a `slashNormalizedBuildFolderPath` property to `HeftConfiguration`. + +## 0.66.26 +Mon, 12 Aug 2024 22:16:04 GMT + +_Version update only_ + +## 0.66.25 +Fri, 02 Aug 2024 17:26:42 GMT + +_Version update only_ + +## 0.66.24 +Sat, 27 Jul 2024 00:10:27 GMT + +### Patches + +- Include CHANGELOG.md in published releases again + +## 0.66.23 +Wed, 24 Jul 2024 00:12:14 GMT + +_Version update only_ + +## 0.66.22 +Wed, 17 Jul 2024 06:55:09 GMT + +_Version update only_ + +## 0.66.21 +Wed, 17 Jul 2024 00:11:19 GMT + +_Version update only_ + +## 0.66.20 +Tue, 16 Jul 2024 00:36:21 GMT + +### Patches + +- Update schemas/templates/heft.json to reflect new settings + +## 0.66.19 +Thu, 27 Jun 2024 21:01:36 GMT + +_Version update only_ + +## 0.66.18 +Mon, 03 Jun 2024 23:43:15 GMT + +_Version update only_ + +## 0.66.17 +Thu, 30 May 2024 00:13:05 GMT + +_Version update only_ + +## 0.66.16 +Wed, 29 May 2024 02:03:50 GMT + +_Version update only_ + +## 0.66.15 +Wed, 29 May 2024 00:10:52 GMT + +_Version update only_ + +## 0.66.14 +Tue, 28 May 2024 15:10:09 GMT + +_Version update only_ + +## 0.66.13 +Tue, 28 May 2024 00:09:47 GMT + +_Version update only_ + +## 0.66.12 +Sat, 25 May 2024 04:54:07 GMT + +_Version update only_ + +## 0.66.11 +Fri, 24 May 2024 00:15:08 GMT + +_Version update only_ + +## 0.66.10 +Thu, 23 May 2024 02:26:56 GMT + +### Patches + +- Update schema definitions to conform to strict schema-type validation. + +## 0.66.9 +Thu, 16 May 2024 15:10:22 GMT + +_Version update only_ + +## 0.66.8 +Wed, 15 May 2024 23:42:58 GMT + +_Version update only_ + +## 0.66.7 +Wed, 15 May 2024 06:04:17 GMT + +_Version update only_ + +## 0.66.6 +Fri, 10 May 2024 05:33:33 GMT + +_Version update only_ + +## 0.66.5 +Wed, 08 May 2024 22:23:50 GMT + +_Version update only_ + +## 0.66.4 +Mon, 06 May 2024 15:11:04 GMT + +_Version update only_ + +## 0.66.3 +Wed, 10 Apr 2024 15:10:09 GMT + +_Version update only_ + +## 0.66.2 +Tue, 19 Mar 2024 15:10:18 GMT + +_Version update only_ + +## 0.66.1 +Fri, 15 Mar 2024 00:12:40 GMT + +### Patches + +- Fix internal error when run 'heft clean' + +## 0.66.0 +Tue, 05 Mar 2024 01:19:24 GMT + +### Minor changes + +- Add new metrics value `bootDurationMs` to track the boot overhead of Heft before the action starts executing the subtasks. Update the start time used to compute `taskTotalExecutionMs` to be the beginning of operation graph execution. Fix the value of `taskTotalExecutionMs` field to be in milliseconds instead of seconds. Add new metrics value `totalUptimeMs` to track how long watch mode sessions are kept alive. + +## 0.65.10 +Sun, 03 Mar 2024 20:58:12 GMT + +_Version update only_ + +## 0.65.9 +Sat, 02 Mar 2024 02:22:23 GMT + +_Version update only_ + +## 0.65.8 +Fri, 01 Mar 2024 01:10:08 GMT + +_Version update only_ + +## 0.65.7 +Thu, 29 Feb 2024 07:11:45 GMT + +_Version update only_ + +## 0.65.6 +Wed, 28 Feb 2024 16:09:27 GMT + +_Version update only_ + +## 0.65.5 +Sat, 24 Feb 2024 23:02:51 GMT + +_Version update only_ + +## 0.65.4 +Thu, 22 Feb 2024 01:36:09 GMT + +_Version update only_ + +## 0.65.3 +Wed, 21 Feb 2024 21:45:28 GMT + +_Version update only_ + +## 0.65.2 +Wed, 21 Feb 2024 08:55:47 GMT + +_Version update only_ + +## 0.65.1 +Tue, 20 Feb 2024 21:45:10 GMT + +### Patches + +- Fix a recent regression causing `Error: Cannot find module 'colors/safe'` (GitHub #4525) +- Remove a no longer needed dependency on the `chokidar` package + +## 0.65.0 +Tue, 20 Feb 2024 16:10:52 GMT + +### Minor changes + +- Add a built-in `set-environment-variables-plugin` task plugin to set environment variables. + +## 0.64.8 +Mon, 19 Feb 2024 21:54:26 GMT + +_Version update only_ + +## 0.64.7 +Sat, 17 Feb 2024 06:24:34 GMT + +### Patches + +- Fix broken link to API documentation + +## 0.64.6 +Thu, 08 Feb 2024 01:09:21 GMT + +_Version update only_ + +## 0.64.5 +Wed, 07 Feb 2024 01:11:18 GMT + +_Version update only_ + +## 0.64.4 +Mon, 05 Feb 2024 23:46:52 GMT + +_Version update only_ + +## 0.64.3 +Thu, 25 Jan 2024 01:09:30 GMT + +_Version update only_ + +## 0.64.2 +Tue, 23 Jan 2024 20:12:57 GMT + +_Version update only_ + +## 0.64.1 +Tue, 23 Jan 2024 16:15:05 GMT + +_Version update only_ + +## 0.64.0 +Tue, 16 Jan 2024 18:30:10 GMT + +### Minor changes + +- Add support for TypeScript 5.3 + +## 0.63.6 +Wed, 03 Jan 2024 00:31:18 GMT + +_Version update only_ + +## 0.63.5 +Wed, 20 Dec 2023 01:09:45 GMT + +_Version update only_ + +## 0.63.4 +Thu, 07 Dec 2023 03:44:13 GMT + +_Version update only_ + +## 0.63.3 +Tue, 05 Dec 2023 01:10:16 GMT + +_Version update only_ + +## 0.63.2 +Fri, 10 Nov 2023 18:02:04 GMT + +_Version update only_ + +## 0.63.1 +Wed, 01 Nov 2023 23:11:35 GMT + +### Patches + +- Fix line endings in published package. + +## 0.63.0 +Mon, 30 Oct 2023 23:36:37 GMT + +### Minor changes + +- [BREAKING CHANGE] Remove "heft run" short-parameters for "--to" ("-t"), "--to-except" ("-T"), and "--only" ("-o"). + +### Patches + +- Fix an issue with parsing of the "--debug" and "--unmanaged" flags for Heft + +## 0.62.3 +Sun, 01 Oct 2023 02:56:29 GMT + +_Version update only_ + +## 0.62.2 +Sat, 30 Sep 2023 00:20:51 GMT + +_Version update only_ + +## 0.62.1 +Thu, 28 Sep 2023 20:53:17 GMT + +_Version update only_ + +## 0.62.0 +Wed, 27 Sep 2023 00:21:38 GMT + +### Minor changes + +- (BREAKING API CHANGE) Remove the deprecated `cancellationToken` property of `IHeftTaskRunHookOptions`. Use `abortSignal` on that object instead. + +## 0.61.3 +Tue, 26 Sep 2023 21:02:30 GMT + +### Patches + +- Fix an issue where `heft clean` would crash with `ERR_ILLEGAL_CONSTRUCTOR`. + +## 0.61.2 +Tue, 26 Sep 2023 09:30:33 GMT + +### Patches + +- Update type-only imports to include the type modifier. + +## 0.61.1 +Mon, 25 Sep 2023 23:38:27 GMT + +_Version update only_ + +## 0.61.0 +Fri, 22 Sep 2023 00:05:50 GMT + +### Minor changes + +- (BREAKING CHANGE): Rename task temp folder from "." to "/" to simplify caching phase outputs. + +## 0.60.0 +Tue, 19 Sep 2023 15:21:51 GMT + +### Minor changes + +- Allow Heft to communicate via IPC with a host process when running in watch mode. The host controls scheduling of incremental re-runs. + +## 0.59.0 +Fri, 15 Sep 2023 00:36:58 GMT + +### Minor changes + +- Update @types/node from 14 to 18 + +### Patches + +- Migrate plugin name collision detection to the InternalHeftSession instance to allow multiple Heft sessions in the same process. + +## 0.58.2 +Tue, 08 Aug 2023 07:10:39 GMT + +_Version update only_ + +## 0.58.1 +Sat, 29 Jul 2023 00:22:50 GMT + +### Patches + +- Fix the `toolFinish` lifecycle hook so that it is invoked after the `recordMetrics` hook, rather than before. Ensure that the `toolFinish` lifecycle hook is invoked if the user performs a graceful shutdown of Heft (e.g. via Ctrl+C). + +## 0.58.0 +Thu, 20 Jul 2023 20:47:28 GMT + +### Minor changes + +- BREAKING CHANGE: Update the heft.json "cleanFiles" property and the delete-files-plugin to delete the contents of folders specified by "sourcePath" instead of deleting the folders themselves. To delete the folders, use the "includeGlobs" property to specify the folder to delete. + +## 0.57.1 +Wed, 19 Jul 2023 00:20:31 GMT + +_Version update only_ + +## 0.57.0 +Thu, 13 Jul 2023 00:22:37 GMT + +### Minor changes + +- Support `--clean` in watch mode. Cleaning in watch mode is now performed only during the first-pass of lifecycle or phase operations. Once the clean has been completed, `--clean` will be ignored until the command is restarted + +## 0.56.3 +Wed, 12 Jul 2023 15:20:39 GMT + +_Version update only_ + +## 0.56.2 +Fri, 07 Jul 2023 00:19:32 GMT + +### Patches + +- Revise README.md and UPGRADING.md documentation + +## 0.56.1 +Thu, 06 Jul 2023 00:16:19 GMT + +_Version update only_ + +## 0.56.0 +Mon, 19 Jun 2023 22:40:21 GMT + +### Minor changes + +- Use the `IRigConfig` interface in the `HeftConfiguration` object insteacd of the `RigConfig` class. + +## 0.55.2 +Thu, 15 Jun 2023 00:21:01 GMT + +_Version update only_ + +## 0.55.1 +Wed, 14 Jun 2023 00:19:41 GMT + +### Patches + +- Add MockScopedLogger to help plugin authors with unit testing. + +## 0.55.0 +Tue, 13 Jun 2023 15:17:20 GMT + +### Minor changes + +- Remove the deprecated `cacheFolderPath` property from the session object. + +## 0.54.0 +Tue, 13 Jun 2023 01:49:01 GMT + +### Minor changes + +- Add plugin support for parameter short-names. + +## 0.53.1 +Fri, 09 Jun 2023 18:05:34 GMT + +### Patches + +- Revise CHANGELOG.md to more clearly identify the breaking changes + +## 0.53.0 +Fri, 09 Jun 2023 00:19:49 GMT + +### Minor changes + +- (BREAKING CHANGE) Remove "taskEvents" heft.json configuration option, and replace it with directly referencing the included plugins. Please read https://github.com/microsoft/rushstack/blob/main/apps/heft/UPGRADING.md + +### Patches + +- Update UPGRADING.md with new JSON schema URLs + +## 0.52.2 +Thu, 08 Jun 2023 15:21:17 GMT + +### Patches + +- Provide a useful error message when encountering legacy Heft configurations + +## 0.52.1 +Thu, 08 Jun 2023 00:20:02 GMT + +### Patches + +- Remove the concept of the cache folder, since it mostly just causes bugs. + +## 0.52.0 +Wed, 07 Jun 2023 22:45:16 GMT + +### Minor changes + +- Add a new API IHeftTaskSession.parsedCommandLine for accessing the invoked command name +- (BREAKING CHANGE) The built-in task NodeServicePlugin now supports the "--serve" mode with semantics similar to heft-webpack5-plugin. Please read https://github.com/microsoft/rushstack/blob/main/apps/heft/UPGRADING.md + +### Patches + +- Add action aliases support. Action aliases can be used to create custom "heft " commands which call existing Heft commands with optional default arguments. ## 0.51.0 Fri, 02 Jun 2023 02:01:12 GMT ### Minor changes -- Overhaul to support splitting single-project builds into more phases than "build" and "test", to align with Rush phased commands. See UPGRADING.md for details. +- (BREAKING CHANGE) Overhaul to support splitting single-project builds into more phases than "build" and "test", to align with Rush phased commands. Please read https://github.com/microsoft/rushstack/blob/main/apps/heft/UPGRADING.md ## 0.50.7 Mon, 29 May 2023 15:21:15 GMT diff --git a/apps/heft/README.md b/apps/heft/README.md index a652807e8c3..fd224141575 100644 --- a/apps/heft/README.md +++ b/apps/heft/README.md @@ -8,21 +8,21 @@

- - - + + + Heft is a config-driven toolchain that invokes other popular tools such as TypeScript, ESLint, Jest, Webpack, and API Extractor. You can use it to build web applications, Node.js services, command-line tools, libraries, and more. Heft builds all your JavaScript projects the same way: A way that works. -Heft is typically launched by the `"build"` action from a **package.json** file. It's designed for use in -a monorepo with potentially hundreds of projects, where the [Rush](https://rushjs.io/) orchestrator invokes -a `"build"` action separately in each project folder. In this situation, everything must execute as fast as possible. +Heft is typically launched by **package.json** commands such as `"npm run build"` or `"npm run test"`. It's designed +for use in a monorepo with potentially hundreds of projects, where the [Rush](https://rushjs.io/) orchestrator invokes +these commands separately in each project folder. In this situation, everything must execute as fast as possible. Special purpose scripts become a headache to maintain, so it's better to replace them with a reusable engine that's driven by config files. In a large repo, you'll want to minimize duplication of these config files across projects. Ultimately, you'll want to define a small set of stereotypical project types -(["rigs"](https://rushstack.io/pages/heft/rig_packages/)) that you will maintain, then discourage projects from +(["rigs"](https://rushstack.io/pages/heft/rig_packages/)) to officially support, then discourage projects from overriding the rig configuration. Being consistent ensures that any person can easily contribute to any project. Heft is a ready-made implementation of all these concepts. @@ -30,38 +30,39 @@ You don't need a monorepo to use Heft, however. It also works well for small sta similar systems, Heft has some unique design goals: - **Scalable**: Heft interfaces with the [Rush Stack](https://rushstack.io/) family of tools, which are tailored - for large monorepos with many people and projects. Heft doesn't require Rush, though. + for large monorepos with many people and projects. Heft doesn't require Rush, though. -- **Optimized**: Heft tracks fine-grained performance metrics at each step. Although Heft is still in its - early stages, the TypeScript plugin already implements sophisticated optimizations such as: filesystem caching, - incremental compilation, symlinking of cache files to reduce copy times, hosting the compiler in a separate - worker process, and a unified compiler pass for Jest and Webpack. +- **Optimized**: Heft tracks fine-grained performance metrics at each step. The TypeScript plugin implements + sophisticated optimizations such as: filesystem caching, incremental compilation, simultaneous multi-target emit, + and a unified compiler pass for Jest/Webpack/ESLint. JSON config files and plugin manifests enable fast + querying of metadata without evaluating potentially inefficient script code. - **Complete**: Rush Stack aspires to establish a fully worked out solution for building typical TypeScript projects. Unopinionated task abstractions often work against this goal: It is expensive to optimize and support - (and document!) every possible cocktail of tech choices. The best optimizations and integrations - make lots of assumptions about how tasks will interact. Heft is opinionated. Our aim is to agree on a recommended - toolkit that works well for a broad range of scenarios, then work together on the deep investments that will - make that a great experience. + (and document!) every possible cocktail of tech choices. The best optimizations and integrations + make deep assumptions about how tasks will interact. Although the Heft engine itself is very flexible, + our philosophy is to agree on a standard approach that covers a broad range of scenarios, then invest in + making the best possible experience for that approach. - **Extensible**: Most projects require at least a few specialized tasks such as preprocessors, postprocessors, - or loaders. Heft is composed of plugins using the [tapable](https://www.npmjs.com/package/tapable) - hook system (familiar from Webpack). It's easy to write your own plugins. Compared to loose architectures - such as Grunt or Gulp, Heft ships a predefined arrangement of "stages" that custom tasks hook into. Having - a standardized starting point makes it easier to get technical support for customized rigs. + or loaders. Heft is organized around plugins using the [tapable](https://www.npmjs.com/package/tapable) + hook system (familiar from Webpack). Strongly typed APIs make it easy to write your own plugins. Compared to + loose architectures such as Grunt or Gulp, Heft's plugin-system is organized around explicit easy-to-read + config files. Customizations generally will extend a standard rig rather than starting from scratch. - **Familiar**: Like Rush, Heft is a regular Node.js application -- developers don't need to install native - prerequisites such as Python, MSYS2, or the .NET Framework. Heft's source code is easy to understand and debug - because it's 100% TypeScript, the same programming language as your web projects. Developing for native targets + prerequisites such as Python, MSYS2, or the .NET Framework. Heft's source code is easy to understand and debug + because it's 100% TypeScript, the same programming language as your web projects. Developing for native targets is still possible, of course. -- **Professional**: The Rush Stack projects are developed by and for engineers who ship major commercial services. - Each feature is designed, discussed in the open, and thoughtfully code reviewed. Despite being a free community - collaboration, this software is developed with the mindset that we'll be depending on it for many years to come. +- **Professional**: The Rush Stack projects are developed by and for engineers who ship large scale commercial + apps. Each feature is designed, discussed in the open, and thoughtfully code reviewed. Breaking changes + require us to migrate thousands of our own projects, so upgrades are relatively painless compared to typical + Node.js tooling. - - - + + + Heft has not yet reached its 1.0 milestone, however the following tasks are already available: @@ -84,6 +85,6 @@ the Rush Stack website. - [UPGRADING.md]( https://github.com/microsoft/rushstack/blob/main/apps/heft/UPGRADING.md) - Instructions for migrating existing projects to use a newer version of Heft -- [API Reference](https://rushstack.io/pages/api/heft/) +- [API Reference](https://api.rushstack.io/pages/heft/) Heft is part of the [Rush Stack](https://rushstack.io/) family of projects. diff --git a/apps/heft/UPGRADING.md b/apps/heft/UPGRADING.md index 0e3048b7e89..fa6a9bc7af3 100644 --- a/apps/heft/UPGRADING.md +++ b/apps/heft/UPGRADING.md @@ -1,347 +1,59 @@ # Upgrade notes for @rushstack/heft -### Heft 0.51.0 - -Multi-phase Heft is a complete re-write of the `@rushstack/heft` project with the intention of being more closely compatible with multi-phase Rush builds. In addition, this update brings greater customizability and improved parallel process handling to Heft. - -Some key areas that were improved with the updated version of Heft include: -- Developer-defined order of execution for Heft plugins and Heft events -- Partial execution of Heft actions via scoping parameters like `--to` or `--only` -- A simplified plugin API for developers making Heft plugins -- Explicit definition of Heft plugins via "heft-plugin.json" -- Native support for defining multiple plugins within a single plugin package -- Improved handling of plugin parameters -- Native support for incremental watch-mode in Heft actions -- Reduced overhead and performance improvements -- Much more! - -#### Heft Tasks -Heft tasks are the smallest unit of work specified in "heft.json". Tasks can either implement _a single plugin_, or _a single Heft event_. Heft tasks may take dependencies on other tasks within the same phase, and all task dependencies must complete execution before dependent tasks can execute. - -Heft events are essentially built-in plugins that can be used to provide the implementation of a Heft task. Available Heft events include: -- `copyFiles` -- `deleteFiles` -- `runScript` -- `nodeService` - -#### Heft Phases -Heft phases are a collection of tasks that will run when executing a phase. Phases act as a logical collection of tasks that would reasonably (but not necessarily) map to a Rush phase. Heft phases may take dependencies on other phases, and when executing multiple phases, all selected phases must complete execution before dependent phases can execute. - -#### Heft Actions -Using similar expansion logic to Rush, execution of a selection of Heft phases can be done through the use of the `heft run` action. This action executes a set of selected phases in order of phase dependency. If the selected phases are not dependencies, they will be executed in parallel. Selection parameters include: -- `--only` - Execute the specified phase -- `--to` - Execute the specified phase and all its dependencies - -Additionally, task- and phase-specific parameters may be provided to the `heft run` action by appending `-- ` to the command. For example, `heft run --only build -- --clean` will run only the `build` phase and will run a clean before executing the phase. - -In addition, Heft will generate actions for each phase specified in the "heft.json" configuration. These actions are executed by running `heft ` and run Heft to the specified phase, including all phase dependencies. As such, these inferred Heft actions are equivalent to running `heft run --to `, and are intended as a CLI shorthand. - -#### Watch Mode -Watch mode is now a first-class feature in Heft. Watch mode actions are created for all Heft actions. For example, to run "build" and "test" phases in watch mode, either of the commands `heft test-watch` or `heft run-watch --to test`. When running in watch mode, Heft prefers the `runIncremental` hook to the `run` hook (see [Heft Task Plugins](#heft-task-plugins)). +### Heft 0.53.0 +The `taskEvent` configuration option in heft.json has been removed, and use of any `taskEvent`-based functionality is now accomplished by referencing the plugins directly within the `@rushstack/heft` package. -#### Heft Plugins -##### Heft Lifecycle Plugins -Heft lifecycle plugins provide the implementation for certain lifecycle-related hooks. These plugins will be used across all Heft phases, and as such should be rarely used outside of a few specific cases (such as for metrics reporting). Heft lifecycle plugins provide an `apply` method, and here plugins can hook into the following Tapable hooks: -- `toolStart` - Used to provide plugin-related functionality at the start of Heft execution -- `toolFinish` - Used to provide plugin-related functionality at the end of Heft execution, after all tasks are finished -- `recordMetrics` - Used to provide metrics information about the Heft run to the plugin after all tasks are finished +Plugin name mappings for previously-existing task events are: +- `copyFiles` -> `copy-files-plugin` +- `deleteFiles` -> `delete-files-plugin` +- `runScript` -> `run-script-plugin` +- `nodeService` -> `node-service-plugin` -##### Heft Task Plugins -Heft task plugins provide the implementation for Heft tasks. Heft plugins provide an `apply` method, and here plugins can hook into the following Tapable hooks: -- `registerFileOperations` - Invoked exactly once before the first time a plugin runs. Allows a plugin to register copy or delete operations using the same options as the `copyFiles` and `deleteFiles` Heft events (this hook is how those events are implemented). -- `run` - Used to provide plugin-related task functionality -- `runIncremental` - Used to provide plugin-related task functionality when in watch mode. If no `runIncremental` implementation is provided for a task, Heft will fall back to using the `run` hook as usual. The options structure includes two functions used to support watch operations: - - `requestRun()` - This function asks the Heft runtime to schedule a new run of the plugin's owning task, potentially cancelling the current build. - - `watchGlobAsync(patterns, options)` - This function is provided for convenience for the common case of monitoring a glob for changes. It returns a `Map` that enumerates the list of files (or folders) selected by the glob and whether or not they have changed since the previous invocation. It will automatically invoke the `requestRun()` callback if it detects changes to files or directory listings that might impact the output of the glob. - -##### Heft Cross-Plugin Interaction -Heft plugins can use the `requestAccessToPluginByName` API to access the requested plugin accessors. Accessors are objects provided by plugins for external use and are the ideal place to share plugin-specific information or hooks used to provide additional plugin functionality. - -Access requests are fulfilled at the beginning of phase execution, prior to `clean` hook execution. If the requested plugin does not provide an accessor, an error will be thrown noting the plugin with the missing accessor. However, if the plugin requested is not present at all, the access request will silently fail. This is done to allow for non-required integrations with external plugins. For this reason, it is important to implement cross-plugin interaction in such a way as to expect this case and to handle it gracefully, or to throw a helpful error. - -Plugins available for access are restricted based on scope. For lifecycle plugins, you may request access to any other lifecycle plugin added to the Heft configuration. For task plugins, you may request access to any other task plugin residing within the same phase in the Heft configuration. - -#### heft.json -The "heft.json" file is where phases and tasks are defined. Since contains the relationships between the phases and tasks, it defines the order of operations for the execution of a Heft action. - -##### Lifecycle Plugin Specification -Lifecycle plugins are specified in the top-level `heftPlugins` array. Plugins can be referenced by providing a package name and a plugin name. Optionally, if a package contains only a single plugin, a plugin can be referenced by providing only the package name and Heft will resolve to the only exported plugin. Lifecycle plugins can also be provided options to modify the default behavior. -```json +Example diff of a heft.json file that uses the `copyFiles` task event: +```diff { - "$schema": "https://developer.microsoft.com/json-schemas/heft/heft.schema.json", - "extends": "base-project/config/heft.json", - - "heftPlugins": [ - { - "packageName": "@rushstack/heft-metrics-reporter", - "options": { - "disableMetrics": true - } - }, - { - "packageName": "@rushstack/heft-initialization-plugin", - "pluginName": "my-lifecycle-plugin" - } - ] -} -``` - -##### Phase, Task, and Task Plugin Specification -All phases are defined within the top-level `phasesByName` property. Each phase may specify `phaseDependencies` to define the order of phase execution when running a selection of Heft phases. Phases may also provide the `cleanFiles` option, which accepts an array of deletion operations to perform when running with the `--clean` flag. - -Within the phase specification, `tasksByName` defines all tasks that run while executing a phase. Each task may specify `taskDependencies` to define the order of task execution. All tasks defined in `taskDependencies` must exist within the same phase. For CLI-availability reasons, phase names, task names, plugin names, and parameter scopes, must be `kebab-cased`. - -The following is an example "heft.json" file defining both a "build" and a "test" phase: -```json -{ - "$schema": "https://developer.microsoft.com/json-schemas/heft/heft.schema.json", - "extends": "base-project/config/heft.json", - - // "heftPlugins" can be used alongside "phasesByName" - "heftPlugins": [ - { - "packageName": "@rushstack/heft-metrics-reporter" - } - ], - - // "phasesByName" defines all phases, and each phase defines tasks to be run "phasesByName": { "build": { - "phaseDescription": "Transpile and run a linter against build output", - "cleanFiles": [ - { - "sourcePath": "temp-build-output" - } - ], - // "tasksByName" defines all tasks within a phase - "tasksByName": { - "typescript": { - "taskPlugin": { - "pluginPackage": "@rushstack/heft-typescript-plugin" - } - }, - "lint": { - "taskDependencies": [ "typescript" ], - "taskPlugin": { - "pluginPackage": "@rushstack/heft-lint-plugin", - "pluginName": "eslint" - } - }, - "copy-assets": { - "taskEvent": { - "eventKind": "copyFiles", + "tasksbyName": { + "perform-copy": { +- "taskEvent": { +- "eventKind": "copyFiles", ++ "taskPlugin": { ++ "pluginPackage": "@rushstack/heft", ++ "pluginName": "copy-files-plugin", "options": { - "copyOperations": [ - { - "sourceFolder": "src/assets", - "destinationFolders": [ "dist/assets" ] - } - ] + ... } } } } - }, - - "test": { - "phaseDependencies": [ "build" ], - "phaseDescription": "Run Jest tests, if provided.", - "tasksByName": { - "jest": { - "taskPlugin": { - "pluginPackage": "@rushstack/heft-jest-plugin" - } - } - } } } } ``` -##### Property Inheritance in "heft.json" -Previously, common properties between a "heft.json" file its extended base file would merge arrays and overwrite objects. Now, both arrays and objects will merge, allowing for simplified use of the "heft.json" file when customizing extended base configurations. +### Heft 0.52.0 -Additionally, we now provide merge behavior overrides to allow modifying extended configurations more dynamically. This is done by using inline markup properties that define inheritance behavior. For example, assume that we are extending a file with a previously defined "property1" value that is a keyed object, and a "property2" value that is an array object: -```json -{ - "$schema": "...", - "$extends": "...", - - "$property1.inheritanceType": "override | merge", - "property1": { - "$subProperty1.inheritanceType": "override | merge", - "subProperty1": { ... }, - "$subProperty2.inheritanceType": "override | append", - "subProperty2": [ ... ] - }, - - "$property2.inheritanceType": "override | append", - "property2": [ ... ] -} -``` -Once an object is set to a `inheritanceType` of override, all sub-property `inheritanceType` values will be ignored, since the top-most object already overrides all sub-properties. -One thing to note is that different mergeBehavior verbs are used for the merging of keyed objects and arrays. This is to make it explicit that arrays will be appended as-is, and no additional processing (eg. deduping if the array is intended to be a set) is done during merge. If such behavior is required, it can be done on the implementation side. Deduping arrays within the @rushstack/heft-config-file package doesn't quite make sense, since deduping arrays of non-primitive objects is not easily defined. - -##### Example "heft.json" Comparison -###### "heft.json" in `@rushstack/heft@0.49.0-rc.1` -```json -{ - "$schema": "https://developer.microsoft.com/json-schemas/heft/heft.schema.json", - - "phasesByName": { - "build": { - "cleanFiles": [ - { "sourcePath": "dist" }, - { "sourcePath": "lib" } - ], - "tasksByName": { - "typescript": { - "taskPlugin": { - "pluginPackage": "@rushstack/heft-typescript-plugin" - } - }, - "lint": { - "taskDependencies": ["typescript"], - "taskPlugin": { - "pluginPackage": "@rushstack/heft-lint-plugin" - } - }, - "api-extractor": { - "taskDependencies": ["typescript"], - "taskPlugin": { - "pluginPackage": "@rushstack/heft-api-extractor-plugin" - } - } - } - }, - - "test": { - "phaseDependencies": ["build"], - "tasksByName": { - "jest": { - "taskPlugin": { - "pluginPackage": "@rushstack/heft-jest-plugin" - } - } - } - } - } -} -``` -###### "heft.json" in `@rushstack/heft@0.48.8` -```json -{ - "$schema": "https://developer.microsoft.com/json-schemas/heft/heft.schema.json", +The `nodeService` built-in plugin now supports the `--serve` parameter, to be consistent with the `@rushstack/heft-webpack5-plugin` dev server. - "eventActions": [ - { - "actionKind": "deleteGlobs", - "heftEvent": "clean", - "actionId": "defaultClean", - "globsToDelete": ["dist", "lib", "lib-commonjs", "temp"] - } - ], +Old behavior: +- `nodeService` was always enabled, but would have no effect unless Heft was in watch mode (`heft start`) +- If `config/node-service.json` was omitted, the plugin would silently be disabled - "heftPlugins": [ - { "plugin": "@rushstack/heft-jest-plugin" } - ] -} -``` -*NOTE: This "heft.json" file is simple due to the implicitly included plugins, which must now be added by developers or consumed via a rig.* +New behavior: +- `nodeService` is always loaded by `@rushstack/heft-node-rig` but for a custom `heft.json` you need to load it manually +- `nodeService` has no effect unless you specify `--serve`, for example: `heft build-watch --serve` +- If `--serve` is specified and `config/node-service.json` is omitted, then Heft fails with a hard error -#### heft-plugin.json -The new heft-plugin.json file is a new, required manifest file specified at the root of all Heft plugin packages. This file is used for multiple purposes, including the definition of all contained lifecycle or task plugins, the definition of all plugin-specific CLI parameters, and providing an optional schema file to validate plugin options that can be passed via "heft.json". +### Heft 0.51.0 -The following is an example "heft-plugin.json" file defining a lifecycle plugin and a task plugin: -```json -{ - "$schema": "https://developer.microsoft.com/json-schemas/heft/heft-plugin.schema.json", +⭐ This release included significant breaking changes. ⭐ - "lifecyclePlugins": [ - { - "pluginName": "my-lifecycle-plugin", - "entryPoint": "./lib/MyLifecyclePlugin.js", - "optionsSchema": "./lib/schemas/mylifecycleplugin.schema.json", - "parameterScope": "my-lifecycle", - "parameters": [ - { - "parameterKind": "string", - "longName": "--my-string", - "description": "…", - "argumentName": "ARG_NAME", - "required": false - } - ] - } - ], +For details, please see our two blog posts: - "taskPlugins": [ - { - "pluginName": "my-task-plugin", - "entryPoint": "./lib/MyTaskPlugin.js", - "optionsSchema": "./lib/schemas/mytaskplugin.schema.json", - "parameterScope": "my-task", - "parameters": [ - { - "parameterKind": "string", - "longName": "--my-other-string", - "description": "…", - "argumentName": "ARG_NAME", - "required": false - } - ] - } - ] -} -``` +- [What's New in Heft 0.51](https://rushstack.io/blog/2023/06/15/heft-whats-new/) -##### Defining Plugin CLI Parameters -Defining CLI parameters is now only possible via "heft-plugin.json", and defined parameters can be consumed in plugins via the `HeftTaskSession.parameters` API. Additionally, all plugin parameters for the selected Heft phases are now discoverable on the CLI when using the `--help` argument (ex. `heft test --help` or `heft run --to test -- --help`). - -These parameters can be automatically "de-duped" on the CLI using an optionally-provided `parameterScope`. By default, parameters defined in "heft-plugin.json" will be available on the CLI using `--` and `--:`. When multiple plugins provide the same parameter, only the latter parameter will be available on the CLI in order to "de-dupe" conflicting parameters. For example, if PluginA with parameter scope "PluginA" defines `--parameter`, and PluginB with parameter scope "PluginB" also defines `--parameter`, the parameters will _only_ be available as `--PluginA:parameter` and `--PluginB:parameter`. - -#### Updating "heft.json" -In updating to the new version of Heft, "heft.json" files will need to be updated to define the flow of your Heft run. This is a big change in behavior since legacy Heft defined a strict set of hooks, any of which could be tied into by any plugin. When converting to the new "heft.json" format, special care should be paid to the order-of-operations. - -An important note on upgrading to the new version of Heft is that legacy Heft included a few plugins by default which have since been externalized. Due to this change, these default plugins need to be manually included in your Heft project. These plugins include: -- `@rushstack/heft-typescript-plugin` -- `@rushstack/heft-lint-plugin` -- `@rushstack/heft-api-extractor-plugin` - -To simplify upgrading to the new version of Heft, usage of rigs is encouraged since rigs help centralize changes to Heft configurations in one location. The above plugins are included in the Rushstack-provided `@rushstack/heft-node-rig` and `@rushstack/heft-web-rig` packages. - -#### Updating Heft Plugins -In updating to the new version of Heft, plugins will also need to be updated for compatibility. Some of the more notable API changes include: -- "heft.json" format completely changed. See above for more information on "heft.json" -- "heft-plugin.json" manifest file must accompany any plugin package. If no "heft-plugin.json" file is found, Heft will throw an error. See above for more information on "heft-plugin.json" -- Plugin classes must have parameterless constructors, and must be the default export of the file pointed to by the `entryPoint` property in "heft-plugin.json" -- Schema files for options provided in "heft.json" can now be specified using the `optionsSchema` property in "heft-plugin.json" and they will be validated by Heft -- Parameters are now defined in "heft-plugin.json" and are consumed in the plugin via the `IHeftTaskSession.parameters` or `IHeftLifecycleSession.parameters` property. *NOTE: Other than the default Heft-included parameters, only parameters defined by the calling plugin are accessible* -- Plugins can no longer define their own actions. If a plugin deserves its own action, a dedicated phase should be added to the consumers "heft.json" -- The `runScript` Heft event has been modified to only accept a `runAsync` method, and the properties have been updated to reflect what is available to normal Heft task plugins -- Path-related variables have been renamed to clarify they are paths (ex. `HeftConfiguration.buildFolder` is now `HeftConfiguration.buildFolderPath`) -- The `runIncremental` hook can now be utilized to add ensure that watch mode rebuilds occur in proper dependency order -- The `clean` hook was removed in favor of the `cleanFiles` option in "heft.json" in order to make it obvious what files are being cleaned and when -- The `folderNameForTests` and `extensionForTests` properties have been removed and should instead be addressed via the `testMatch` property in `jest.config.json` - -#### Testing on Your Own Project -The new version of Heft and all related plugins are available in the following packages: -- `@rushstack/heft@0.51.0` -- `@rushstack/heft-typescript-plugin@0.1.0` -- `@rushstack/heft-lint-plugin@0.1.0` -- `@rushstack/heft-api-extractor-plugin@0.1.0` -- `@rushstack/heft-jest-plugin@0.6.0` -- `@rushstack/heft-sass-plugin@0.11.0` -- `@rushstack/heft-storybook-plugin@0.3.0` -- `@rushstack/heft-webpack4-plugin@0.6.0` -- `@rushstack/heft-webpack5-plugin@0.7.0` -- `@rushstack/heft-dev-cert-plugin@0.3.0` - -Additionally, Rushstack-provided rigs have been updated to be compatible with the new version of Heft: -- `@rushstack/heft-node-rig@1.14.0` -- `@rushstack/heft-web-rig@0.16.0` - -If you have any issues with the prerelease packages or the new changes to Heft, please [file an issue](https://github.com/microsoft/rushstack/issues/new?assignees=&labels=&template=heft.md&title=%5Bheft%2Frc%2f0%5D+). +- [Heft 0.51 Migration Guide](https://rushstack.io/blog/2023/06/16/heft-migration-guide/) ### Heft 0.35.0 diff --git a/apps/heft/config/heft.json b/apps/heft/config/heft.json new file mode 100644 index 00000000000..8d1359f022f --- /dev/null +++ b/apps/heft/config/heft.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + "extends": "decoupled-local-node-rig/profiles/default/config/heft.json", + + "phasesByName": { + "build": { + "tasksByName": { + "copy-json-schemas": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft", + "pluginName": "copy-files-plugin", + "options": { + "copyOperations": [ + { + "sourcePath": "src/schemas", + "destinationFolders": ["temp/json-schemas/heft/v1"], + "fileExtensions": [".schema.json"], + "hardlink": true + } + ] + } + } + } + } + } + } +} diff --git a/apps/heft/config/jest.config.json b/apps/heft/config/jest.config.json index 4bb17bde3ee..7c0f9ccc9d6 100644 --- a/apps/heft/config/jest.config.json +++ b/apps/heft/config/jest.config.json @@ -1,3 +1,3 @@ { - "extends": "@rushstack/heft-node-rig/profiles/default/config/jest.config.json" + "extends": "decoupled-local-node-rig/profiles/default/config/jest.config.json" } diff --git a/apps/heft/config/rig.json b/apps/heft/config/rig.json index 6ac88a96368..cc98dea43dd 100644 --- a/apps/heft/config/rig.json +++ b/apps/heft/config/rig.json @@ -3,5 +3,5 @@ // Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@rushstack/heft-node-rig" + "rigPackageName": "decoupled-local-node-rig" } diff --git a/apps/heft/eslint.config.js b/apps/heft/eslint.config.js new file mode 100644 index 00000000000..e54effd122a --- /dev/null +++ b/apps/heft/eslint.config.js @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +const nodeTrustedToolProfile = require('decoupled-local-node-rig/profiles/default/includes/eslint/flat/profile/node-trusted-tool'); +const friendlyLocalsMixin = require('decoupled-local-node-rig/profiles/default/includes/eslint/flat/mixins/friendly-locals'); + +module.exports = [ + ...nodeTrustedToolProfile, + ...friendlyLocalsMixin, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + tsconfigRootDir: __dirname + } + } + } +]; diff --git a/apps/heft/heft-plugin.json b/apps/heft/heft-plugin.json new file mode 100644 index 00000000000..d7c161a404f --- /dev/null +++ b/apps/heft/heft-plugin.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-plugin.schema.json", + + "lifecyclePlugins": [], + + "taskPlugins": [ + { + "pluginName": "copy-files-plugin", + "entryPoint": "./lib/plugins/CopyFilesPlugin", + "optionsSchema": "./lib/schemas/copy-files-options.schema.json" + }, + { + "pluginName": "delete-files-plugin", + "entryPoint": "./lib/plugins/DeleteFilesPlugin", + "optionsSchema": "./lib/schemas/delete-files-options.schema.json" + }, + { + "pluginName": "node-service-plugin", + "entryPoint": "./lib/plugins/NodeServicePlugin", + "parameterScope": "node-service", + "parameters": [ + { + "longName": "--serve", + "parameterKind": "flag", + "description": "Start a local web server for testing purposes. This parameter is only available when running in watch mode." + } + ] + }, + { + "pluginName": "run-script-plugin", + "entryPoint": "./lib/plugins/RunScriptPlugin", + "optionsSchema": "./lib/schemas/run-script-options.schema.json" + }, + { + "entryPoint": "./lib/plugins/SetEnvironmentVariablesPlugin", + "pluginName": "set-environment-variables-plugin", + "optionsSchema": "./lib/schemas/set-environment-variables-plugin.schema.json" + } + ] +} diff --git a/apps/heft/package.json b/apps/heft/package.json index 1563419e831..b2a46f16a19 100644 --- a/apps/heft/package.json +++ b/apps/heft/package.json @@ -1,6 +1,6 @@ { "name": "@rushstack/heft", - "version": "0.51.0", + "version": "1.1.10", "description": "Build all your JavaScript projects the same way: A way that works.", "keywords": [ "toolchain", @@ -29,36 +29,29 @@ "license": "MIT", "scripts": { "build": "heft build --clean", - "start": "heft test --clean --watch", - "_phase:build": "heft build --clean", - "_phase:test": "heft test --no-build" + "start": "heft build-watch --clean", + "_phase:build": "heft run --only build -- --clean", + "_phase:test": "heft run --only test -- --clean" }, "dependencies": { "@rushstack/heft-config-file": "workspace:*", "@rushstack/node-core-library": "workspace:*", + "@rushstack/operation-graph": "workspace:*", "@rushstack/rig-package": "workspace:*", + "@rushstack/terminal": "workspace:*", "@rushstack/ts-command-line": "workspace:*", "@types/tapable": "1.0.6", - "argparse": "~1.0.9", - "chokidar": "~3.4.0", - "fast-glob": "~3.2.4", + "fast-glob": "~3.3.1", "git-repo-info": "~2.1.0", "ignore": "~5.1.6", "tapable": "1.1.3", - "true-case-path": "~2.2.1", "watchpack": "2.4.0" }, "devDependencies": { "@microsoft/api-extractor": "workspace:*", - "@nodelib/fs.scandir": "2.1.5", - "@nodelib/fs.stat": "2.0.5", - "@rushstack/eslint-config": "workspace:*", - "@rushstack/heft": "0.50.6", - "@rushstack/heft-node-rig": "1.13.0", - "@types/argparse": "1.0.38", - "@types/heft-jest": "1.0.1", - "@types/node": "14.18.36", + "@rushstack/heft": "1.1.7", "@types/watchpack": "2.4.0", - "typescript": "~5.0.4" + "decoupled-local-node-rig": "workspace:*", + "eslint": "~9.37.0" } } diff --git a/apps/heft/src/cli/HeftActionRunner.ts b/apps/heft/src/cli/HeftActionRunner.ts index 167ee869c1e..b9d5c7f7f26 100644 --- a/apps/heft/src/cli/HeftActionRunner.ts +++ b/apps/heft/src/cli/HeftActionRunner.ts @@ -1,18 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { performance } from 'perf_hooks'; -import { createInterface, type Interface as ReadlineInterface } from 'readline'; -import os from 'os'; +import { performance } from 'node:perf_hooks'; +import { createInterface, type Interface as ReadlineInterface } from 'node:readline'; +import os from 'node:os'; +import { AlreadyReportedError, InternalError, type IPackageJson } from '@rushstack/node-core-library'; +import { Colorize, ConsoleTerminalProvider, type ITerminal } from '@rushstack/terminal'; import { - AlreadyReportedError, - Colors, - ConsoleTerminalProvider, - InternalError, - type ITerminal, - type IPackageJson -} from '@rushstack/node-core-library'; + type IOperationExecutionOptions, + type IWatchLoopState, + Operation, + OperationExecutionManager, + OperationGroupRecord, + type OperationRequestRunCallback, + OperationStatus, + WatchLoop +} from '@rushstack/operation-graph'; import type { CommandLineFlagParameter, CommandLineParameterProvider, @@ -24,26 +28,42 @@ import type { HeftConfiguration } from '../configuration/HeftConfiguration'; import type { LoggingManager } from '../pluginFramework/logging/LoggingManager'; import type { MetricsCollector } from '../metrics/MetricsCollector'; import { HeftParameterManager } from '../pluginFramework/HeftParameterManager'; -import { - OperationExecutionManager, - type IOperationExecutionOptions -} from '../operations/OperationExecutionManager'; -import { Operation } from '../operations/Operation'; import { TaskOperationRunner } from '../operations/runners/TaskOperationRunner'; import { PhaseOperationRunner } from '../operations/runners/PhaseOperationRunner'; -import { LifecycleOperationRunner } from '../operations/runners/LifecycleOperationRunner'; -import type { HeftPhase } from '../pluginFramework/HeftPhase'; -import type { IHeftAction, IHeftActionOptions } from '../cli/actions/IHeftAction'; -import type { HeftTask } from '../pluginFramework/HeftTask'; -import type { LifecycleOperationRunnerType } from '../operations/runners/LifecycleOperationRunner'; -import { CancellationToken, CancellationTokenSource } from '../pluginFramework/CancellationToken'; +import type { IHeftPhase, HeftPhase } from '../pluginFramework/HeftPhase'; +import type { IHeftAction, IHeftActionOptions } from './actions/IHeftAction'; +import type { + IHeftLifecycleCleanHookOptions, + IHeftLifecycleSession, + IHeftLifecycleToolFinishHookOptions, + IHeftLifecycleToolStartHookOptions +} from '../pluginFramework/HeftLifecycleSession'; +import type { HeftLifecycle } from '../pluginFramework/HeftLifecycle'; +import type { IHeftTask, HeftTask } from '../pluginFramework/HeftTask'; +import { deleteFilesAsync, type IDeleteOperation } from '../plugins/DeleteFilesPlugin'; import { Constants } from '../utilities/Constants'; -import { OperationStatus } from '../operations/OperationStatus'; export interface IHeftActionRunnerOptions extends IHeftActionOptions { action: IHeftAction; } +/** + * Metadata for an operation that represents a task. + * @public + */ +export interface IHeftTaskOperationMetadata { + task: IHeftTask; + phase: IHeftPhase; +} + +/** + * Metadata for an operation that represents a phase. + * @public + */ +export interface IHeftPhaseOperationMetadata { + phase: IHeftPhase; +} + export function initializeHeft( heftConfiguration: HeftConfiguration, terminal: ITerminal, @@ -69,21 +89,53 @@ export function initializeHeft( terminal.writeVerboseLine(''); } +let _cliAbortSignal: AbortSignal | undefined; +export function ensureCliAbortSignal(terminal: ITerminal): AbortSignal { + if (!_cliAbortSignal) { + // Set up the ability to terminate the build via Ctrl+C and have it exit gracefully if pressed once, + // less gracefully if pressed a second time. + const cliAbortController: AbortController = new AbortController(); + _cliAbortSignal = cliAbortController.signal; + const cli: ReadlineInterface = createInterface(process.stdin, undefined, undefined, true); + let forceTerminate: boolean = false; + cli.on('SIGINT', () => { + cli.close(); + + if (forceTerminate) { + terminal.writeErrorLine(`Forcibly terminating.`); + process.exit(1); + } else { + terminal.writeLine( + Colorize.yellow(Colorize.bold(`Canceling... Press Ctrl+C again to forcibly terminate.`)) + ); + } + + forceTerminate = true; + cliAbortController.abort(); + }); + } + + return _cliAbortSignal; +} + export async function runWithLoggingAsync( fn: () => Promise, action: IHeftAction, loggingManager: LoggingManager, terminal: ITerminal, metricsCollector: MetricsCollector, - cancellationToken: CancellationToken -): Promise { + abortSignal: AbortSignal, + throwOnFailure?: boolean +): Promise { const startTime: number = performance.now(); loggingManager.resetScopedLoggerErrorsAndWarnings(); + let result: OperationStatus = OperationStatus.Failure; + // Execute the action operations let encounteredError: boolean = false; try { - const result: OperationStatus = await fn(); + result = await fn(); if (result === OperationStatus.Failure) { encounteredError = true; } @@ -94,8 +146,8 @@ export async function runWithLoggingAsync( const warningStrings: string[] = loggingManager.getWarningStrings(); const errorStrings: string[] = loggingManager.getErrorStrings(); - const wasCancelled: boolean = cancellationToken.isCancelled; - const encounteredWarnings: boolean = warningStrings.length > 0 || wasCancelled; + const wasAborted: boolean = abortSignal.aborted; + const encounteredWarnings: boolean = warningStrings.length > 0 || wasAborted; encounteredError = encounteredError || errorStrings.length > 0; await metricsCollector.recordAsync( @@ -106,13 +158,13 @@ export async function runWithLoggingAsync( action.getParameterStringMap() ); - const finishedLoggingWord: string = encounteredError ? 'Failed' : wasCancelled ? 'Cancelled' : 'Finished'; + const finishedLoggingWord: string = encounteredError ? 'Failed' : wasAborted ? 'Aborted' : 'Finished'; const duration: number = performance.now() - startTime; const durationSeconds: number = Math.round(duration) / 1000; const finishedLoggingLine: string = `-------------------- ${finishedLoggingWord} (${durationSeconds}s) --------------------`; terminal.writeLine( - Colors.bold( - (encounteredError ? Colors.red : encounteredWarnings ? Colors.yellow : Colors.green)( + Colorize.bold( + (encounteredError ? Colorize.red : encounteredWarnings ? Colorize.yellow : Colorize.green)( finishedLoggingLine ) ) @@ -137,9 +189,11 @@ export async function runWithLoggingAsync( } } - if (encounteredError) { + if (encounteredError && throwOnFailure) { throw new AlreadyReportedError(); } + + return result; } export class HeftActionRunner { @@ -153,14 +207,16 @@ export class HeftActionRunner { private readonly _parallelism: number; public constructor(options: IHeftActionRunnerOptions) { - this._action = options.action; - this._internalHeftSession = options.internalHeftSession; - this._heftConfiguration = options.heftConfiguration; - this._loggingManager = options.loggingManager; - this._terminal = options.terminal; - this._metricsCollector = options.metricsCollector; + const { action, internalHeftSession, heftConfiguration, loggingManager, terminal, metricsCollector } = + options; + this._action = action; + this._internalHeftSession = internalHeftSession; + this._heftConfiguration = heftConfiguration; + this._loggingManager = loggingManager; + this._terminal = terminal; + this._metricsCollector = metricsCollector; - const numberOfCores: number = os.cpus().length; + const numberOfCores: number = heftConfiguration.numberOfCores; // If an explicit parallelism number wasn't provided, then choose a sensible // default. @@ -174,8 +230,6 @@ export class HeftActionRunner { // to the number of CPU cores this._parallelism = numberOfCores; } - - this._metricsCollector.setStartTime(); } protected get parameterManager(): HeftParameterManager { @@ -209,21 +263,17 @@ export class HeftActionRunner { description: 'Use the specified locale for this run, if applicable.' }); - let cleanFlag: CommandLineFlagParameter | undefined; - let cleanCacheFlag: CommandLineFlagParameter | undefined; - if (!this._action.watch) { - // Only enable the clean flags in non-watch mode - cleanFlag = parameterProvider.defineFlagParameter({ - parameterLongName: Constants.cleanParameterLongName, - description: 'If specified, clean the outputs before running each phase.' - }); - cleanCacheFlag = parameterProvider.defineFlagParameter({ - parameterLongName: Constants.cleanCacheParameterLongName, - description: - 'If specified, clean the cache before running each phase. To use this flag, the ' + - `${JSON.stringify(Constants.cleanParameterLongName)} flag must also be provided.` - }); + let cleanFlagDescription: string = + 'If specified, clean the outputs at the beginning of the lifecycle and before running each phase.'; + if (this._action.watch) { + cleanFlagDescription = + `${cleanFlagDescription} Cleaning will only be performed once for the lifecycle and each phase, ` + + `and further incremental runs will not be cleaned for the duration of execution.`; } + const cleanFlag: CommandLineFlagParameter = parameterProvider.defineFlagParameter({ + parameterLongName: Constants.cleanParameterLongName, + description: cleanFlagDescription + }); const parameterManager: HeftParameterManager = new HeftParameterManager({ getIsDebug: () => this._internalHeftSession.debug, @@ -231,8 +281,7 @@ export class HeftActionRunner { getIsProduction: () => productionFlag.value, getIsWatch: () => this._action.watch, getLocales: () => localesParameter.values, - getIsClean: () => !!cleanFlag?.value, - getIsCleanCache: () => !!cleanCacheFlag?.value + getIsClean: () => !!cleanFlag?.value }); // Add all the lifecycle parameters for the action @@ -261,130 +310,111 @@ export class HeftActionRunner { initializeHeft(this._heftConfiguration, terminal, this.parameterManager.defaultParameters.verbose); - const operations: ReadonlySet = this._generateOperations(); + const operations: ReadonlySet> = + this._generateOperations(); - // Set up the ability to terminate the build via Ctrl+C and have it exit gracefully if pressed once, - // less gracefully if pressed a second time. - const cliCancellationTokenSource: CancellationTokenSource = new CancellationTokenSource(); - const cliCancellationToken: CancellationToken = cliCancellationTokenSource.token; - const cli: ReadlineInterface = createInterface(process.stdin, undefined, undefined, true); - let forceTerminate: boolean = false; - cli.on('SIGINT', () => { - cli.close(); + const executionManager: OperationExecutionManager< + IHeftTaskOperationMetadata, + IHeftPhaseOperationMetadata + > = new OperationExecutionManager(operations); - if (forceTerminate) { - terminal.writeErrorLine(`Forcibly terminating.`); - process.exit(1); - } else { - terminal.writeLine( - Colors.yellow(Colors.bold(`Canceling build... Press Ctrl+C again to forcibly terminate.`)) - ); - } + const cliAbortSignal: AbortSignal = ensureCliAbortSignal(this._terminal); - forceTerminate = true; - cliCancellationTokenSource.cancel(); - }); + try { + await _startLifecycleAsync(this._internalHeftSession); - const executionManager: OperationExecutionManager = new OperationExecutionManager(operations); + if (this._action.watch) { + const watchLoop: WatchLoop = this._createWatchLoop(executionManager); - if (this._action.watch) { - await this._executeWatchAsync(executionManager, cliCancellationToken); - } else { - await this._executeOnceAsync(executionManager, cliCancellationToken); + if (process.send) { + await watchLoop.runIPCAsync(); + } else { + await watchLoop.runUntilAbortedAsync(cliAbortSignal, () => { + terminal.writeLine(Colorize.bold('Waiting for changes. Press CTRL + C to exit...')); + terminal.writeLine(''); + }); + } + } else { + await this._executeOnceAsync(executionManager, cliAbortSignal); + } + } finally { + // Invoke this here both to ensure it always runs and that it does so after recordMetrics + // This is treated as a finalizer for any assets created in lifecycle plugins. + // It is the responsibility of the lifecycle plugin to ensure that finish gracefully handles + // aborted runs. + await _finishLifecycleAsync(this._internalHeftSession); } } - private async _executeWatchAsync( - executionManager: OperationExecutionManager, - cliCancellationToken: CancellationToken - ): Promise { - let runRequested: boolean = true; - let isRunning: boolean = true; - let cancellationTokenSource: CancellationTokenSource = new CancellationTokenSource(); - + private _createWatchLoop(executionManager: OperationExecutionManager): WatchLoop { const { _terminal: terminal } = this; - - let resolveRequestRun!: (requestor?: string) => void; - function createRequestRunPromise(): Promise { - return new Promise( - (resolve: (requestor?: string) => void, reject: (err: Error) => void) => { - resolveRequestRun = resolve; - } - ).then((requestor: string | undefined) => { - terminal.writeLine(Colors.bold(`New run requested by ${requestor || 'unknown task'}`)); - runRequested = true; - if (isRunning) { - terminal.writeLine(Colors.bold(`Cancelling incremental build...`)); - // If there's a source file change, we need to cancel the incremental build and wait for the - // execution to finish before we begin execution again. - cancellationTokenSource.cancel(); - } - }); - } - let requestRunPromise: Promise = createRequestRunPromise(); - - function cancelExecution(): void { - cancellationTokenSource.cancel(); - } - - function requestRun(requestor?: string): void { - // The wrapper here allows operation runners to hang onto a single instance, despite the underlying - // promise changing. - resolveRequestRun(requestor); - } - - // eslint-disable-next-line no-constant-condition - while (!cliCancellationToken.isCancelled) { - if (cancellationTokenSource.isCancelled) { - cancellationTokenSource = new CancellationTokenSource(); - cliCancellationToken.onCancelledPromise.finally(cancelExecution); - } - - // Create the cancellation token which is passed to the incremental build. - const cancellationToken: CancellationToken = cancellationTokenSource.token; - - // Write an empty line to the terminal for separation between iterations. We've already iterated - // at this point, so log out that we're about to start a new run. - terminal.writeLine(''); - terminal.writeLine(Colors.bold('Starting incremental build...')); - - // Start the incremental build and wait for a source file to change - runRequested = false; - isRunning = true; - - try { - await this._executeOnceAsync(executionManager, cancellationToken, requestRun); - } catch (err) { - if (!(err instanceof AlreadyReportedError)) { - throw err; - } - } finally { - isRunning = false; - } - - if (!runRequested) { - terminal.writeLine(Colors.bold('Waiting for changes. Press CTRL + C to exit...')); + const watchLoop: WatchLoop = new WatchLoop({ + onBeforeExecute: () => { + // Write an empty line to the terminal for separation between iterations. We've already iterated + // at this point, so log out that we're about to start a new run. terminal.writeLine(''); - await Promise.race([requestRunPromise, cliCancellationToken.onCancelledPromise]); + terminal.writeLine(Colorize.bold('Starting incremental build...')); + }, + executeAsync: (state: IWatchLoopState): Promise => { + return this._executeOnceAsync(executionManager, state.abortSignal, state.requestRun); + }, + onRequestRun: (requestor?: string) => { + terminal.writeLine(Colorize.bold(`New run requested by ${requestor || 'unknown task'}`)); + }, + onAbort: () => { + terminal.writeLine(Colorize.bold(`Cancelling incremental build...`)); } - - requestRunPromise = createRequestRunPromise(); - } + }); + return watchLoop; } private async _executeOnceAsync( - executionManager: OperationExecutionManager, - cancellationToken: CancellationToken, - requestRun?: (requestor?: string) => void - ): Promise { + executionManager: OperationExecutionManager, + abortSignal: AbortSignal, + requestRun?: OperationRequestRunCallback + ): Promise { + const { taskStart, taskFinish, phaseStart, phaseFinish } = this._internalHeftSession.lifecycle.hooks; + // Record this as the start of task execution. + this._metricsCollector.setStartTime(); // Execute the action operations - await runWithLoggingAsync( + return await runWithLoggingAsync( () => { - const operationExecutionManagerOptions: IOperationExecutionOptions = { + const operationExecutionManagerOptions: IOperationExecutionOptions< + IHeftTaskOperationMetadata, + IHeftPhaseOperationMetadata + > = { terminal: this._terminal, parallelism: this._parallelism, - cancellationToken, - requestRun + abortSignal, + requestRun, + beforeExecuteOperation( + operation: Operation + ): void { + if (taskStart.isUsed()) { + taskStart.call({ operation }); + } + }, + afterExecuteOperation( + operation: Operation + ): void { + if (taskFinish.isUsed()) { + taskFinish.call({ operation }); + } + }, + beforeExecuteOperationGroup( + operationGroup: OperationGroupRecord + ): void { + if (operationGroup.metadata.phase && phaseStart.isUsed()) { + phaseStart.call({ operation: operationGroup }); + } + }, + afterExecuteOperationGroup( + operationGroup: OperationGroupRecord + ): void { + if (operationGroup.metadata.phase && phaseFinish.isUsed()) { + phaseFinish.call({ operation: operationGroup }); + } + } }; return executionManager.executeAsync(operationExecutionManagerOptions); @@ -393,35 +423,20 @@ export class HeftActionRunner { this._loggingManager, this._terminal, this._metricsCollector, - cancellationToken + abortSignal, + !requestRun ); } - private _generateOperations(): Set { + private _generateOperations(): Set> { const { selectedPhases } = this._action; - const { - defaultParameters: { clean, cleanCache } - } = this.parameterManager; - - if (cleanCache && !clean) { - throw new Error( - `The ${JSON.stringify(Constants.cleanCacheParameterLongName)} option can only be used in ` + - `conjunction with ${JSON.stringify(Constants.cleanParameterLongName)}.` - ); - } - const operations: Map = new Map(); + const operations: Map< + string, + Operation + > = new Map(); + const operationGroups: Map> = new Map(); const internalHeftSession: InternalHeftSession = this._internalHeftSession; - const startLifecycleOperation: Operation = _getOrCreateLifecycleOperation( - internalHeftSession, - 'start', - operations - ); - const finishLifecycleOperation: Operation = _getOrCreateLifecycleOperation( - internalHeftSession, - 'finish', - operations - ); let hasWarnedAboutSkippedPhases: boolean = false; for (const phase of selectedPhases) { @@ -432,7 +447,7 @@ export class HeftActionRunner { // Only write once, and write with yellow to make it stand out without writing a warning to stderr hasWarnedAboutSkippedPhases = true; this._terminal.writeLine( - Colors.bold( + Colorize.bold( 'The provided list of phases does not contain all phase dependencies. You may need to run the ' + 'excluded phases manually.' ) @@ -443,27 +458,28 @@ export class HeftActionRunner { } // Create operation for the phase start node - const phaseOperation: Operation = _getOrCreatePhaseOperation(internalHeftSession, phase, operations); - // Set the 'start' lifecycle operation as a dependency of all phases to ensure the 'start' lifecycle - // operation runs first - phaseOperation.addDependency(startLifecycleOperation); - // Set the phase operation as a dependency of the 'end' lifecycle operation to ensure the phase - // operation runs first - finishLifecycleOperation.addDependency(phaseOperation); + const phaseOperation: Operation = _getOrCreatePhaseOperation( + internalHeftSession, + phase, + operations, + operationGroups + ); // Create operations for each task for (const task of phase.tasks) { - const taskOperation: Operation = _getOrCreateTaskOperation(internalHeftSession, task, operations); + const taskOperation: Operation = _getOrCreateTaskOperation( + internalHeftSession, + task, + operations, + operationGroups + ); // Set the phase operation as a dependency of the task operation to ensure the phase operation runs first taskOperation.addDependency(phaseOperation); - // Set the task operation as a dependency of the 'stop' lifecycle operation to ensure the task operation - // runs first - finishLifecycleOperation.addDependency(taskOperation); // Set all dependency tasks as dependencies of the task operation for (const dependencyTask of task.dependencyTasks) { taskOperation.addDependency( - _getOrCreateTaskOperation(internalHeftSession, dependencyTask, operations) + _getOrCreateTaskOperation(internalHeftSession, dependencyTask, operations, operationGroups) ); } @@ -475,7 +491,8 @@ export class HeftActionRunner { const consumingPhaseOperation: Operation = _getOrCreatePhaseOperation( internalHeftSession, consumingPhase, - operations + operations, + operationGroups ); consumingPhaseOperation.addDependency(taskOperation); // This is purely to simplify the reported graph for phase circularities @@ -489,36 +506,28 @@ export class HeftActionRunner { } } -function _getOrCreateLifecycleOperation( - internalHeftSession: InternalHeftSession, - type: LifecycleOperationRunnerType, - operations: Map -): Operation { - const key: string = `lifecycle.${type}`; - - let operation: Operation | undefined = operations.get(key); - if (!operation) { - operation = new Operation({ - groupName: 'lifecycle', - runner: new LifecycleOperationRunner({ type, internalHeftSession }) - }); - operations.set(key, operation); - } - return operation; -} - function _getOrCreatePhaseOperation( + this: void, internalHeftSession: InternalHeftSession, phase: HeftPhase, - operations: Map + operations: Map, + operationGroups: Map> ): Operation { const key: string = phase.phaseName; let operation: Operation | undefined = operations.get(key); if (!operation) { + let group: OperationGroupRecord | undefined = operationGroups.get( + phase.phaseName + ); + if (!group) { + group = new OperationGroupRecord(phase.phaseName, { phase }); + operationGroups.set(phase.phaseName, group); + } // Only create the operation. Dependencies are hooked up separately operation = new Operation({ - groupName: phase.phaseName, + group, + name: phase.phaseName, runner: new PhaseOperationRunner({ phase, internalHeftSession }) }); operations.set(key, operation); @@ -527,22 +536,110 @@ function _getOrCreatePhaseOperation( } function _getOrCreateTaskOperation( + this: void, internalHeftSession: InternalHeftSession, task: HeftTask, - operations: Map + operations: Map, + operationGroups: Map> ): Operation { const key: string = `${task.parentPhase.phaseName}.${task.taskName}`; - let operation: Operation | undefined = operations.get(key); + let operation: Operation | undefined = operations.get( + key + ) as Operation; if (!operation) { + const group: OperationGroupRecord | undefined = operationGroups.get( + task.parentPhase.phaseName + ); + if (!group) { + throw new InternalError( + `Task ${task.taskName} in phase ${task.parentPhase.phaseName} has no group. This should not happen.` + ); + } operation = new Operation({ - groupName: task.parentPhase.phaseName, + group, runner: new TaskOperationRunner({ internalHeftSession, task - }) + }), + name: task.taskName, + metadata: { task, phase: task.parentPhase } }); operations.set(key, operation); } return operation; } + +async function _startLifecycleAsync(this: void, internalHeftSession: InternalHeftSession): Promise { + const { clean } = internalHeftSession.parameterManager.defaultParameters; + + // Load and apply the lifecycle plugins + const lifecycle: HeftLifecycle = internalHeftSession.lifecycle; + const { lifecycleLogger } = lifecycle; + await lifecycle.applyPluginsAsync(lifecycleLogger.terminal); + + if (lifecycleLogger.hasErrors) { + throw new AlreadyReportedError(); + } + + if (clean) { + const startTime: number = performance.now(); + lifecycleLogger.terminal.writeVerboseLine('Starting clean'); + + // Grab the additional clean operations from the phase + const deleteOperations: IDeleteOperation[] = []; + + // Delete all temp folders for tasks by default + for (const pluginDefinition of lifecycle.pluginDefinitions) { + const lifecycleSession: IHeftLifecycleSession = + await lifecycle.getSessionForPluginDefinitionAsync(pluginDefinition); + deleteOperations.push({ sourcePath: lifecycleSession.tempFolderPath }); + } + + // Create the options and provide a utility method to obtain paths to delete + const cleanHookOptions: IHeftLifecycleCleanHookOptions = { + addDeleteOperations: (...deleteOperationsToAdd: IDeleteOperation[]) => + deleteOperations.push(...deleteOperationsToAdd) + }; + + // Run the plugin clean hook + if (lifecycle.hooks.clean.isUsed()) { + try { + await lifecycle.hooks.clean.promise(cleanHookOptions); + } catch (e) { + // Log out using the clean logger, and return an error status + if (!(e instanceof AlreadyReportedError)) { + lifecycleLogger.emitError(e as Error); + } + throw new AlreadyReportedError(); + } + } + + // Delete the files if any were specified + if (deleteOperations.length) { + const rootFolderPath: string = internalHeftSession.heftConfiguration.buildFolderPath; + await deleteFilesAsync(rootFolderPath, deleteOperations, lifecycleLogger.terminal); + } + + lifecycleLogger.terminal.writeVerboseLine(`Finished clean (${performance.now() - startTime}ms)`); + + if (lifecycleLogger.hasErrors) { + throw new AlreadyReportedError(); + } + } + + // Run the start hook + if (lifecycle.hooks.toolStart.isUsed()) { + const lifecycleToolStartHookOptions: IHeftLifecycleToolStartHookOptions = {}; + await lifecycle.hooks.toolStart.promise(lifecycleToolStartHookOptions); + + if (lifecycleLogger.hasErrors) { + throw new AlreadyReportedError(); + } + } +} + +async function _finishLifecycleAsync(internalHeftSession: InternalHeftSession): Promise { + const lifecycleToolFinishHookOptions: IHeftLifecycleToolFinishHookOptions = {}; + await internalHeftSession.lifecycle.hooks.toolFinish.promise(lifecycleToolFinishHookOptions); +} diff --git a/apps/heft/src/cli/HeftCommandLineParser.ts b/apps/heft/src/cli/HeftCommandLineParser.ts index 18ec4b3f8da..67389a7d053 100644 --- a/apps/heft/src/cli/HeftCommandLineParser.ts +++ b/apps/heft/src/cli/HeftCommandLineParser.ts @@ -1,25 +1,28 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { ArgumentParser } from 'argparse'; -import { CommandLineParser, type CommandLineFlagParameter } from '@rushstack/ts-command-line'; +import os from 'node:os'; + import { - Terminal, - InternalError, - ConsoleTerminalProvider, - AlreadyReportedError, - type ITerminal -} from '@rushstack/node-core-library'; + CommandLineParser, + type AliasCommandLineAction, + type CommandLineFlagParameter, + type CommandLineAction +} from '@rushstack/ts-command-line'; +import { InternalError, AlreadyReportedError } from '@rushstack/node-core-library'; +import { Terminal, ConsoleTerminalProvider, type ITerminal } from '@rushstack/terminal'; import { MetricsCollector } from '../metrics/MetricsCollector'; import { HeftConfiguration } from '../configuration/HeftConfiguration'; import { InternalHeftSession } from '../pluginFramework/InternalHeftSession'; import { LoggingManager } from '../pluginFramework/logging/LoggingManager'; -import { Constants } from '../utilities/Constants'; import { CleanAction } from './actions/CleanAction'; import { PhaseAction } from './actions/PhaseAction'; import { RunAction } from './actions/RunAction'; import type { IHeftActionOptions } from './actions/IHeftAction'; +import { AliasAction } from './actions/AliasAction'; +import { getToolParameterNamesFromArgs } from '../utilities/CliUtilities'; +import { Constants } from '../utilities/Constants'; /** * This interfaces specifies values for parameters that must be parsed before the CLI @@ -30,6 +33,8 @@ interface IPreInitializationArgumentValues { unmanaged?: boolean; } +const HEFT_TOOL_FILENAME: 'heft' = 'heft'; + export class HeftCommandLineParser extends CommandLineParser { public readonly globalTerminal: ITerminal; @@ -40,10 +45,11 @@ export class HeftCommandLineParser extends CommandLineParser { private readonly _loggingManager: LoggingManager; private readonly _metricsCollector: MetricsCollector; private readonly _heftConfiguration: HeftConfiguration; + private _internalHeftSession: InternalHeftSession | undefined; public constructor() { super({ - toolFilename: 'heft', + toolFilename: HEFT_TOOL_FILENAME, toolDescription: 'Heft is a pluggable build system designed for web projects.' }); @@ -83,15 +89,17 @@ export class HeftCommandLineParser extends CommandLineParser { InternalError.breakInDebugger = true; } + const numberOfCores: number = os.availableParallelism?.() ?? os.cpus().length; this._heftConfiguration = HeftConfiguration.initialize({ cwd: process.cwd(), - terminalProvider: this._terminalProvider + terminalProvider: this._terminalProvider, + numberOfCores }); this._metricsCollector = new MetricsCollector(); } - public async execute(args?: string[]): Promise { + public async executeAsync(args?: string[]): Promise { // Defensively set the exit code to 1 so if the tool crashes for whatever reason, // we'll have a nonzero exit code. process.exitCode = 1; @@ -105,6 +113,7 @@ export class HeftCommandLineParser extends CommandLineParser { loggingManager: this._loggingManager, metricsCollector: this._metricsCollector }); + this._internalHeftSession = internalHeftSession; const actionOptions: IHeftActionOptions = { internalHeftSession: internalHeftSession, @@ -127,18 +136,70 @@ export class HeftCommandLineParser extends CommandLineParser { this.addAction(new PhaseAction({ ...actionOptions, phase, watch: true })); } - return await super.execute(args); + // Add the action aliases last, since we need the targets to be defined before we can add the aliases + const aliasActions: AliasCommandLineAction[] = []; + for (const [ + aliasName, + { actionName, defaultParameters } + ] of internalHeftSession.actionReferencesByAlias) { + const existingAction: CommandLineAction | undefined = this.tryGetAction(aliasName); + if (existingAction) { + throw new Error( + `The alias "${aliasName}" specified in heft.json cannot be used because an action ` + + 'with that name already exists.' + ); + } + const targetAction: CommandLineAction | undefined = this.tryGetAction(actionName); + if (!targetAction) { + throw new Error( + `The action "${actionName}" referred to by alias "${aliasName}" in heft.json could not be found.` + ); + } + aliasActions.push( + new AliasAction({ + terminal: this.globalTerminal, + toolFilename: HEFT_TOOL_FILENAME, + aliasName, + targetAction, + defaultParameters + }) + ); + } + // Add the alias actions. Do this in a second pass to disallow aliases that refer to other aliases. + for (const aliasAction of aliasActions) { + this.addAction(aliasAction); + } + + return await super.executeAsync(args); } catch (e) { - await this._reportErrorAndSetExitCode(e as Error); + await this._reportErrorAndSetExitCodeAsync(e as Error); return false; } } - protected async onExecute(): Promise { + protected override async onExecuteAsync(): Promise { try { - await super.onExecute(); + const selectedAction: CommandLineAction | undefined = this.selectedAction; + + let commandName: string = ''; + let unaliasedCommandName: string = ''; + + if (selectedAction) { + commandName = selectedAction.actionName; + if (selectedAction instanceof AliasAction) { + unaliasedCommandName = selectedAction.targetAction.actionName; + } else { + unaliasedCommandName = selectedAction.actionName; + } + } + + this._internalHeftSession!.parsedCommandLine = { + commandName, + unaliasedCommandName + }; + await super.onExecuteAsync(); } catch (e) { - await this._reportErrorAndSetExitCode(e as Error); + await this._reportErrorAndSetExitCodeAsync(e as Error); } // If we make it here, things are fine and reset the exit code back to 0 @@ -168,19 +229,17 @@ export class HeftCommandLineParser extends CommandLineParser { // try to evaluate any parameters. This is to ensure that the // `--debug` flag is defined correctly before we do this not-so-rigorous // parameter parsing. - throw new InternalError('onDefineParameters() has not yet been called.'); + throw new InternalError('parameters have not yet been defined.'); } - // This is a rough parsing of the --debug parameter - const parser: ArgumentParser = new ArgumentParser({ addHelp: false }); - parser.addArgument(this._debugFlag.longName, { dest: 'debug', action: 'storeTrue' }); - parser.addArgument(this._unmanagedFlag.longName, { dest: 'unmanaged', action: 'storeTrue' }); - - const [result]: IPreInitializationArgumentValues[] = parser.parseKnownArgs(args); - return result; + const toolParameters: Set = getToolParameterNamesFromArgs(args); + return { + debug: toolParameters.has(this._debugFlag.longName), + unmanaged: toolParameters.has(this._unmanagedFlag.longName) + }; } - private async _reportErrorAndSetExitCode(error: Error): Promise { + private async _reportErrorAndSetExitCodeAsync(error: Error): Promise { if (!(error instanceof AlreadyReportedError)) { this.globalTerminal.writeErrorLine(error.toString()); } @@ -190,8 +249,9 @@ export class HeftCommandLineParser extends CommandLineParser { this.globalTerminal.writeErrorLine(error.stack!); } - if (!process.exitCode || process.exitCode > 0) { - process.exit(process.exitCode); + const exitCode: string | number | undefined = process.exitCode; + if (!exitCode || typeof exitCode !== 'number' || exitCode > 0) { + process.exit(exitCode); } else { process.exit(1); } diff --git a/apps/heft/src/cli/actions/AliasAction.ts b/apps/heft/src/cli/actions/AliasAction.ts new file mode 100644 index 00000000000..71fd1e52af3 --- /dev/null +++ b/apps/heft/src/cli/actions/AliasAction.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { ITerminal } from '@rushstack/terminal'; +import { + AliasCommandLineAction, + type IAliasCommandLineActionOptions, + type CommandLineAction +} from '@rushstack/ts-command-line'; + +export interface IAliasActionOptions extends IAliasCommandLineActionOptions { + terminal: ITerminal; +} + +export class AliasAction extends AliasCommandLineAction { + private readonly _toolFilename: string; + private readonly _terminal: ITerminal; + + public constructor(options: IAliasActionOptions) { + super(options); + this._toolFilename = options.toolFilename; + this._terminal = options.terminal; + } + + protected override async onExecuteAsync(): Promise { + const toolFilename: string = this._toolFilename; + const actionName: string = this.actionName; + const targetAction: CommandLineAction = this.targetAction; + const defaultParameters: ReadonlyArray = this.defaultParameters; + const defaultParametersString: string = defaultParameters.join(' '); + + this._terminal.writeLine( + `The "${toolFilename} ${actionName}" alias was expanded to "${toolFilename} ${targetAction.actionName}` + + `${defaultParametersString ? ` ${defaultParametersString}` : ''}".` + ); + + await super.onExecuteAsync(); + } +} diff --git a/apps/heft/src/cli/actions/CleanAction.ts b/apps/heft/src/cli/actions/CleanAction.ts index 39e4f0f3506..11d1c8e4c2b 100644 --- a/apps/heft/src/cli/actions/CleanAction.ts +++ b/apps/heft/src/cli/actions/CleanAction.ts @@ -6,7 +6,8 @@ import { type CommandLineFlagParameter, type CommandLineStringListParameter } from '@rushstack/ts-command-line'; -import type { ITerminal } from '@rushstack/node-core-library'; +import type { ITerminal } from '@rushstack/terminal'; +import { OperationStatus } from '@rushstack/operation-graph'; import type { IHeftAction, IHeftActionOptions } from './IHeftAction'; import type { HeftPhase } from '../../pluginFramework/HeftPhase'; @@ -17,9 +18,7 @@ import type { HeftTaskSession } from '../../pluginFramework/HeftTaskSession'; import { Constants } from '../../utilities/Constants'; import { definePhaseScopingParameters, expandPhases } from './RunAction'; import { deleteFilesAsync, type IDeleteOperation } from '../../plugins/DeleteFilesPlugin'; -import { initializeHeft, runWithLoggingAsync } from '../HeftActionRunner'; -import { CancellationToken } from '../../pluginFramework/CancellationToken'; -import { OperationStatus } from '../../operations/OperationStatus'; +import { ensureCliAbortSignal, initializeHeft, runWithLoggingAsync } from '../HeftActionRunner'; export class CleanAction extends CommandLineAction implements IHeftAction { public readonly watch: boolean = false; @@ -30,7 +29,6 @@ export class CleanAction extends CommandLineAction implements IHeftAction { private readonly _toParameter: CommandLineStringListParameter; private readonly _toExceptParameter: CommandLineStringListParameter; private readonly _onlyParameter: CommandLineStringListParameter; - private readonly _cleanCacheFlag: CommandLineFlagParameter; private _selectedPhases: ReadonlySet | undefined; public constructor(options: IHeftActionOptions) { @@ -54,12 +52,6 @@ export class CleanAction extends CommandLineAction implements IHeftAction { parameterShortName: Constants.verboseParameterShortName, description: 'If specified, log information useful for debugging.' }); - this._cleanCacheFlag = this.defineFlagParameter({ - parameterLongName: Constants.cleanCacheParameterLongName, - description: - 'If specified, clean the cache directories in addition to the temp directories and provided ' + - 'clean operations.' - }); } public get selectedPhases(): ReadonlySet { @@ -84,10 +76,12 @@ export class CleanAction extends CommandLineAction implements IHeftAction { return this._selectedPhases; } - protected async onExecute(): Promise { + protected override async onExecuteAsync(): Promise { const { heftConfiguration } = this._internalHeftSession; - const cancellationToken: CancellationToken = new CancellationToken(); + const abortSignal: AbortSignal = ensureCliAbortSignal(this._terminal); + // Record this as the start of task execution. + this._metricsCollector.setStartTime(); initializeHeft(heftConfiguration, this._terminal, this._verboseFlag.value); await runWithLoggingAsync( this._cleanFilesAsync.bind(this), @@ -95,7 +89,7 @@ export class CleanAction extends CommandLineAction implements IHeftAction { this._internalHeftSession.loggingManager, this._terminal, this._metricsCollector, - cancellationToken + abortSignal ); } @@ -107,16 +101,16 @@ export class CleanAction extends CommandLineAction implements IHeftAction { for (const task of phase.tasks) { const taskSession: HeftTaskSession = phaseSession.getSessionForTask(task); deleteOperations.push({ sourcePath: taskSession.tempFolderPath }); - if (this._cleanCacheFlag.value) { - deleteOperations.push({ sourcePath: taskSession.cacheFolderPath }); - } } // Add the manually specified clean operations deleteOperations.push(...phase.cleanFiles); } // Delete the files - await deleteFilesAsync(deleteOperations, this._terminal); + if (deleteOperations.length) { + const rootFolderPath: string = this._internalHeftSession.heftConfiguration.buildFolderPath; + await deleteFilesAsync(rootFolderPath, deleteOperations, this._terminal); + } return deleteOperations.length === 0 ? OperationStatus.NoOp : OperationStatus.Success; } diff --git a/apps/heft/src/cli/actions/IHeftAction.ts b/apps/heft/src/cli/actions/IHeftAction.ts index 5925bfb503e..ad4a91468da 100644 --- a/apps/heft/src/cli/actions/IHeftAction.ts +++ b/apps/heft/src/cli/actions/IHeftAction.ts @@ -2,7 +2,7 @@ // See LICENSE in the project root for license information. import type { CommandLineAction } from '@rushstack/ts-command-line'; -import type { ITerminal } from '@rushstack/node-core-library'; +import type { ITerminal } from '@rushstack/terminal'; import type { HeftConfiguration } from '../../configuration/HeftConfiguration'; import type { MetricsCollector } from '../../metrics/MetricsCollector'; diff --git a/apps/heft/src/cli/actions/PhaseAction.ts b/apps/heft/src/cli/actions/PhaseAction.ts index c2bcc6542a2..b3891514529 100644 --- a/apps/heft/src/cli/actions/PhaseAction.ts +++ b/apps/heft/src/cli/actions/PhaseAction.ts @@ -47,7 +47,7 @@ export class PhaseAction extends CommandLineAction implements IHeftAction { return this._selectedPhases; } - protected async onExecute(): Promise { + protected override async onExecuteAsync(): Promise { await this._actionRunner.executeAsync(); } } diff --git a/apps/heft/src/cli/actions/RunAction.ts b/apps/heft/src/cli/actions/RunAction.ts index 638f76b3890..6454ea5b901 100644 --- a/apps/heft/src/cli/actions/RunAction.ts +++ b/apps/heft/src/cli/actions/RunAction.ts @@ -6,7 +6,8 @@ import { type CommandLineParameterProvider, type CommandLineStringListParameter } from '@rushstack/ts-command-line'; -import { AlreadyReportedError, type ITerminal } from '@rushstack/node-core-library'; +import { AlreadyReportedError } from '@rushstack/node-core-library'; +import type { ITerminal } from '@rushstack/terminal'; import { Selection } from '../../utilities/Selection'; import { HeftActionRunner } from '../HeftActionRunner'; @@ -78,21 +79,18 @@ export function definePhaseScopingParameters(action: IHeftAction): IScopingParam return { toParameter: action.defineStringListParameter({ parameterLongName: Constants.toParameterLongName, - parameterShortName: Constants.toParameterShortName, description: `The phase to ${action.actionName} to, including all transitive dependencies.`, argumentName: 'PHASE', parameterGroup: ScopedCommandLineAction.ScopingParameterGroup }), toExceptParameter: action.defineStringListParameter({ parameterLongName: Constants.toExceptParameterLongName, - parameterShortName: Constants.toExceptParameterShortName, description: `The phase to ${action.actionName} to (but not include), including all transitive dependencies.`, argumentName: 'PHASE', parameterGroup: ScopedCommandLineAction.ScopingParameterGroup }), onlyParameter: action.defineStringListParameter({ parameterLongName: Constants.onlyParameterLongName, - parameterShortName: Constants.onlyParameterShortName, description: `The phase to ${action.actionName}.`, argumentName: 'PHASE', parameterGroup: ScopedCommandLineAction.ScopingParameterGroup @@ -147,7 +145,7 @@ export class RunAction extends ScopedCommandLineAction implements IHeftAction { this._actionRunner.defineParameters(scopedParameterProvider); } - protected async onExecute(): Promise { + protected override async onExecuteAsync(): Promise { await this._actionRunner.executeAsync(); } } diff --git a/apps/heft/src/configuration/HeftConfiguration.ts b/apps/heft/src/configuration/HeftConfiguration.ts index 9d247603027..e6079a8a3f6 100644 --- a/apps/heft/src/configuration/HeftConfiguration.ts +++ b/apps/heft/src/configuration/HeftConfiguration.ts @@ -1,17 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; +import * as path from 'node:path'; + +import { type IPackageJson, PackageJsonLookup, InternalError, Path } from '@rushstack/node-core-library'; +import { Terminal, type ITerminalProvider, type ITerminal } from '@rushstack/terminal'; import { - Terminal, - type ITerminalProvider, - type IPackageJson, - PackageJsonLookup, - InternalError, - type ITerminal -} from '@rushstack/node-core-library'; -import { trueCasePathSync } from 'true-case-path'; -import { RigConfig } from '@rushstack/rig-package'; + type IProjectConfigurationFileSpecification, + ProjectConfigurationFile +} from '@rushstack/heft-config-file'; +import { type IRigConfig, RigConfig } from '@rushstack/rig-package'; import { Constants } from '../utilities/Constants'; import { RigPackageResolver, type IRigPackageResolver } from './RigPackageResolver'; @@ -29,26 +27,48 @@ export interface IHeftConfigurationInitializationOptions { * Terminal instance to facilitate logging. */ terminalProvider: ITerminalProvider; + + /** + * The number of CPU cores available to the process. This is used to determine how many tasks can be run in parallel. + */ + numberOfCores: number; +} + +interface IHeftConfigurationOptions extends IHeftConfigurationInitializationOptions { + buildFolderPath: string; +} + +interface IProjectConfigurationFileEntry { + options: IProjectConfigurationFileSpecification; + loader: ProjectConfigurationFile; } /** * @public */ export class HeftConfiguration { - private _buildFolderPath!: string; + private _slashNormalizedBuildFolderPath: string | undefined; private _projectConfigFolderPath: string | undefined; - private _cacheFolderPath: string | undefined; private _tempFolderPath: string | undefined; - private _rigConfig: RigConfig | undefined; - private _globalTerminal!: Terminal; - private _terminalProvider!: ITerminalProvider; - private _rigPackageResolver!: RigPackageResolver; + private _rigConfig: IRigConfig | undefined; + private _rigPackageResolver: RigPackageResolver | undefined; + + private readonly _knownConfigurationFiles: Map> = new Map(); /** * Project build folder path. This is the folder containing the project's package.json file. */ - public get buildFolderPath(): string { - return this._buildFolderPath; + public readonly buildFolderPath: string; + + /** + * {@link HeftConfiguration.buildFolderPath} with all path separators converted to forward slashes. + */ + public get slashNormalizedBuildFolderPath(): string { + if (!this._slashNormalizedBuildFolderPath) { + this._slashNormalizedBuildFolderPath = Path.convertToSlashes(this.buildFolderPath); + } + + return this._slashNormalizedBuildFolderPath; } /** @@ -62,22 +82,6 @@ export class HeftConfiguration { return this._projectConfigFolderPath; } - /** - * The project's cache folder. - * - * @remarks This folder exists at \/.cache. In general, this folder is used to store - * cached output from tasks under task-specific subfolders, and is not intended to be directly - * written to. Instead, plugins should write to the directory provided by - * HeftTaskSession.taskCacheFolderPath - */ - public get cacheFolderPath(): string { - if (!this._cacheFolderPath) { - this._cacheFolderPath = path.join(this.buildFolderPath, Constants.cacheFolderName); - } - - return this._cacheFolderPath; - } - /** * The project's temporary folder. * @@ -87,7 +91,7 @@ export class HeftConfiguration { */ public get tempFolderPath(): string { if (!this._tempFolderPath) { - this._tempFolderPath = path.join(this._buildFolderPath, Constants.tempFolderName); + this._tempFolderPath = path.join(this.buildFolderPath, Constants.tempFolderName); } return this._tempFolderPath; @@ -96,7 +100,7 @@ export class HeftConfiguration { /** * The rig.json configuration for this project, if present. */ - public get rigConfig(): RigConfig { + public get rigConfig(): IRigConfig { if (!this._rigConfig) { throw new InternalError( 'The rigConfig cannot be accessed until HeftConfiguration.checkForRigAsync() has been called' @@ -116,22 +120,19 @@ export class HeftConfiguration { rigConfig: this.rigConfig }); } + return this._rigPackageResolver; } /** * Terminal instance to facilitate logging. */ - public get globalTerminal(): ITerminal { - return this._globalTerminal; - } + public readonly globalTerminal: ITerminal; /** * Terminal provider for the provided terminal. */ - public get terminalProvider(): ITerminalProvider { - return this._terminalProvider; - } + public readonly terminalProvider: ITerminalProvider; /** * The Heft tool's package.json @@ -147,7 +148,18 @@ export class HeftConfiguration { return PackageJsonLookup.instance.tryLoadPackageJsonFor(this.buildFolderPath)!; } - private constructor() {} + /** + * The number of CPU cores available to the process. This can be used to determine how many tasks can be run + * in parallel. + */ + public readonly numberOfCores: number; + + private constructor({ terminalProvider, buildFolderPath, numberOfCores }: IHeftConfigurationOptions) { + this.buildFolderPath = buildFolderPath; + this.terminalProvider = terminalProvider; + this.numberOfCores = numberOfCores; + this.globalTerminal = new Terminal(terminalProvider); + } /** * Performs the search for rig.json and initializes the `HeftConfiguration.rigConfig` object. @@ -156,34 +168,84 @@ export class HeftConfiguration { public async _checkForRigAsync(): Promise { if (!this._rigConfig) { this._rigConfig = await RigConfig.loadForProjectFolderAsync({ - projectFolderPath: this._buildFolderPath + projectFolderPath: this.buildFolderPath }); } } + /** + * Attempts to load a riggable project configuration file using blocking, synchronous I/O. + * @param options - The options for the configuration file loader from `@rushstack/heft-config-file`. If invoking this function multiple times for the same file, reuse the same object. + * @param terminal - The terminal to log messages during configuration file loading. + * @returns The configuration file, or undefined if it could not be loaded. + */ + public tryLoadProjectConfigurationFile( + options: IProjectConfigurationFileSpecification, + terminal: ITerminal + ): TConfigFile | undefined { + const loader: ProjectConfigurationFile = this._getConfigFileLoader(options); + return loader.tryLoadConfigurationFileForProject(terminal, this.buildFolderPath, this._rigConfig); + } + + /** + * Attempts to load a riggable project configuration file using asynchronous I/O. + * @param options - The options for the configuration file loader from `@rushstack/heft-config-file`. If invoking this function multiple times for the same file, reuse the same object. + * @param terminal - The terminal to log messages during configuration file loading. + * @returns A promise that resolves to the configuration file, or undefined if it could not be loaded. + */ + public async tryLoadProjectConfigurationFileAsync( + options: IProjectConfigurationFileSpecification, + terminal: ITerminal + ): Promise { + const loader: ProjectConfigurationFile = this._getConfigFileLoader(options); + return loader.tryLoadConfigurationFileForProjectAsync(terminal, this.buildFolderPath, this._rigConfig); + } + /** * @internal */ public static initialize(options: IHeftConfigurationInitializationOptions): HeftConfiguration { - const configuration: HeftConfiguration = new HeftConfiguration(); - const packageJsonPath: string | undefined = PackageJsonLookup.instance.tryGetPackageJsonFilePathFor( options.cwd ); + let buildFolderPath: string; if (packageJsonPath) { - let buildFolderPath: string = path.dirname(packageJsonPath); - // The CWD path's casing may be incorrect on a case-insensitive filesystem. Some tools, like Jest - // expect the casing of the project path to be correct and produce unexpected behavior when the casing - // isn't correct. - // This ensures the casing of the project folder is correct. - buildFolderPath = trueCasePathSync(buildFolderPath); - configuration._buildFolderPath = buildFolderPath; + buildFolderPath = path.dirname(packageJsonPath); + // On Windows it is possible for the drive letter in the CWD to be lowercase, but the normalized naming is uppercase + // Force it to always be uppercase for consistency. + buildFolderPath = + process.platform === 'win32' + ? buildFolderPath.charAt(0).toUpperCase() + buildFolderPath.slice(1) + : buildFolderPath; } else { throw new Error('No package.json file found. Are you in a project folder?'); } - configuration._terminalProvider = options.terminalProvider; - configuration._globalTerminal = new Terminal(options.terminalProvider); + const configuration: HeftConfiguration = new HeftConfiguration({ + ...options, + buildFolderPath + }); return configuration; } + + private _getConfigFileLoader( + options: IProjectConfigurationFileSpecification + ): ProjectConfigurationFile { + let entry: IProjectConfigurationFileEntry | undefined = this._knownConfigurationFiles.get( + options.projectRelativeFilePath + ) as IProjectConfigurationFileEntry | undefined; + + if (!entry) { + entry = { + options: Object.freeze(options), + loader: new ProjectConfigurationFile(options) + }; + } else if (options !== entry.options) { + throw new Error( + `The project configuration file for ${options.projectRelativeFilePath} has already been loaded with different options. Please ensure that options object used to load the configuration file is the same referenced object in all calls.` + ); + } + + return entry.loader; + } } diff --git a/apps/heft/src/configuration/HeftPluginConfiguration.ts b/apps/heft/src/configuration/HeftPluginConfiguration.ts index edb45399c33..4bda7fd937a 100644 --- a/apps/heft/src/configuration/HeftPluginConfiguration.ts +++ b/apps/heft/src/configuration/HeftPluginConfiguration.ts @@ -1,17 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; import { JsonFile, JsonSchema } from '@rushstack/node-core-library'; import { HeftLifecyclePluginDefinition, - HeftPluginDefinitionBase, + type HeftPluginDefinitionBase, HeftTaskPluginDefinition, type IHeftLifecyclePluginDefinitionJson, type IHeftTaskPluginDefinitionJson } from './HeftPluginDefinition'; import type { IHeftConfigurationJsonPluginSpecifier } from '../utilities/CoreConfigFiles'; +import heftPluginSchema from '../schemas/heft-plugin.schema.json'; export interface IHeftPluginConfigurationJson { lifecyclePlugins?: IHeftLifecyclePluginDefinitionJson[]; @@ -24,9 +24,7 @@ const HEFT_PLUGIN_CONFIGURATION_FILENAME: 'heft-plugin.json' = 'heft-plugin.json * Loads and validates the heft-plugin.json file. */ export class HeftPluginConfiguration { - private static _jsonSchema: JsonSchema = JsonSchema.fromFile( - path.join(__dirname, '..', 'schemas', 'heft-plugin.schema.json') - ); + private static _jsonSchema: JsonSchema = JsonSchema.fromLoadedObject(heftPluginSchema); private static _pluginConfigurationPromises: Map> = new Map(); private readonly _heftPluginConfigurationJson: IHeftPluginConfigurationJson; @@ -80,41 +78,6 @@ export class HeftPluginConfiguration { return await heftPluginConfigurationPromise; } - public get lifecyclePluginDefinitions(): ReadonlySet { - if (!this._lifecyclePluginDefinitions) { - this._lifecyclePluginDefinitions = new Set(); - for (const lifecyclePluginDefinitionJson of this._heftPluginConfigurationJson.lifecyclePlugins || []) { - this._lifecyclePluginDefinitions.add( - HeftLifecyclePluginDefinition.loadFromObject({ - heftPluginDefinitionJson: lifecyclePluginDefinitionJson, - packageRoot: this.packageRoot, - packageName: this.packageName - }) - ); - } - } - return this._lifecyclePluginDefinitions; - } - - /** - * Task plugin definitions sourced from the heft-plugin.json file. - */ - public get taskPluginDefinitions(): ReadonlySet { - if (!this._taskPluginDefinitions) { - this._taskPluginDefinitions = new Set(); - for (const taskPluginDefinitionJson of this._heftPluginConfigurationJson.taskPlugins || []) { - this._taskPluginDefinitions.add( - HeftTaskPluginDefinition.loadFromObject({ - heftPluginDefinitionJson: taskPluginDefinitionJson, - packageRoot: this.packageRoot, - packageName: this.packageName - }) - ); - } - } - return this._taskPluginDefinitions; - } - /** * Returns a loaded plugin definition for the provided specifier. Specifiers are normally obtained from the * heft.json file. @@ -123,10 +86,10 @@ export class HeftPluginConfiguration { pluginSpecifier: IHeftConfigurationJsonPluginSpecifier ): HeftPluginDefinitionBase { if (!pluginSpecifier.pluginName) { - const pluginDefinitions: HeftPluginDefinitionBase[] = [ - ...this.lifecyclePluginDefinitions, - ...this.taskPluginDefinitions - ]; + const pluginDefinitions: HeftPluginDefinitionBase[] = ([] as HeftPluginDefinitionBase[]).concat( + Array.from(this._getLifecyclePluginDefinitions()), + Array.from(this._getTaskPluginDefinitions()) + ); // Make an attempt at resolving the plugin without the name by looking for the first plugin if (pluginDefinitions.length > 1) { throw new Error( @@ -150,6 +113,24 @@ export class HeftPluginConfiguration { } } + /** + * Returns if the provided plugin definition is a lifecycle plugin definition. + */ + public isLifecyclePluginDefinition( + pluginDefinition: HeftPluginDefinitionBase + ): pluginDefinition is HeftLifecyclePluginDefinition { + return this._getLifecyclePluginDefinitions().has(pluginDefinition); + } + + /** + * Returns if the provided plugin definition is a task plugin definition. + */ + public isTaskPluginDefinition( + pluginDefinition: HeftPluginDefinitionBase + ): pluginDefinition is HeftTaskPluginDefinition { + return this._getTaskPluginDefinitions().has(pluginDefinition); + } + /** * Returns a loaded lifecycle plugin definition for the provided plugin name. If one can't be found, * returns undefined. @@ -159,7 +140,10 @@ export class HeftPluginConfiguration { ): HeftLifecyclePluginDefinition | undefined { if (!this._lifecyclePluginDefinitionsMap) { this._lifecyclePluginDefinitionsMap = new Map( - [...this.lifecyclePluginDefinitions].map((d: HeftLifecyclePluginDefinition) => [d.pluginName, d]) + Array.from(this._getLifecyclePluginDefinitions()).map((d: HeftLifecyclePluginDefinition) => [ + d.pluginName, + d + ]) ); } return this._lifecyclePluginDefinitionsMap.get(lifecyclePluginName); @@ -172,12 +156,47 @@ export class HeftPluginConfiguration { public tryGetTaskPluginDefinitionByName(taskPluginName: string): HeftTaskPluginDefinition | undefined { if (!this._taskPluginDefinitionsMap) { this._taskPluginDefinitionsMap = new Map( - [...this.taskPluginDefinitions].map((d: HeftTaskPluginDefinition) => [d.pluginName, d]) + Array.from(this._getTaskPluginDefinitions()).map((d: HeftTaskPluginDefinition) => [d.pluginName, d]) ); } return this._taskPluginDefinitionsMap.get(taskPluginName); } + private _getLifecyclePluginDefinitions(): ReadonlySet { + if (!this._lifecyclePluginDefinitions) { + this._lifecyclePluginDefinitions = new Set(); + for (const lifecyclePluginDefinitionJson of this._heftPluginConfigurationJson.lifecyclePlugins || []) { + this._lifecyclePluginDefinitions.add( + HeftLifecyclePluginDefinition.loadFromObject({ + heftPluginDefinitionJson: lifecyclePluginDefinitionJson, + packageRoot: this.packageRoot, + packageName: this.packageName + }) + ); + } + } + return this._lifecyclePluginDefinitions; + } + + /** + * Task plugin definitions sourced from the heft-plugin.json file. + */ + private _getTaskPluginDefinitions(): ReadonlySet { + if (!this._taskPluginDefinitions) { + this._taskPluginDefinitions = new Set(); + for (const taskPluginDefinitionJson of this._heftPluginConfigurationJson.taskPlugins || []) { + this._taskPluginDefinitions.add( + HeftTaskPluginDefinition.loadFromObject({ + heftPluginDefinitionJson: taskPluginDefinitionJson, + packageRoot: this.packageRoot, + packageName: this.packageName + }) + ); + } + } + return this._taskPluginDefinitions; + } + private _validate(heftPluginConfigurationJson: IHeftPluginConfigurationJson, packageName: string): void { if ( !heftPluginConfigurationJson.lifecyclePlugins?.length && diff --git a/apps/heft/src/configuration/HeftPluginDefinition.ts b/apps/heft/src/configuration/HeftPluginDefinition.ts index b6c9e5f4886..4ce4bd0c19a 100644 --- a/apps/heft/src/configuration/HeftPluginDefinition.ts +++ b/apps/heft/src/configuration/HeftPluginDefinition.ts @@ -1,4 +1,8 @@ -import * as path from 'path'; +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as path from 'node:path'; + import { InternalError, JsonSchema } from '@rushstack/node-core-library'; import type { IHeftPlugin } from '../pluginFramework/IHeftPlugin'; @@ -19,6 +23,10 @@ export interface IBaseParameterJson { * The name of the parameter (e.g. \"--verbose\"). This is a required field. */ longName: string; + /** + * An optional short form of the parameter (e.g. \"-v\" instead of \"--verbose\"). + */ + shortName?: string; /** * A detailed description of the parameter, which appears when requesting help for the command (e.g. \"rush --help my-command\"). */ @@ -185,8 +193,6 @@ export interface IHeftPluginDefinitionOptions { } export abstract class HeftPluginDefinitionBase { - private static _loadedPluginPathsByName: Map = new Map(); - private _heftPluginDefinitionJson: IHeftPluginDefinitionJson; private _pluginPackageName: string; private _resolvedEntryPoint: string; @@ -210,20 +216,6 @@ export abstract class HeftPluginDefinitionBase { seenParameters.add(parameter.longName); } - // Ensure that plugin names are unique. Main reason for this restriction is to ensure that command-line - // parameter conflicts can be handled/undocumented synonms can be provided in all scenarios - const existingPluginPath: string | undefined = HeftPluginDefinitionBase._loadedPluginPathsByName.get( - this.pluginName - ); - if (existingPluginPath && existingPluginPath !== this._resolvedEntryPoint) { - throw new Error( - `A plugin named ${JSON.stringify(this.pluginName)} has already been loaded from ` + - `"${existingPluginPath}". Plugins must have unique names.` - ); - } else if (!existingPluginPath) { - HeftPluginDefinitionBase._loadedPluginPathsByName.set(this.pluginName, this._resolvedEntryPoint); - } - // Unfortunately loading the schema is a synchronous process. if (options.heftPluginDefinitionJson.optionsSchema) { const resolvedSchemaPath: string = path.resolve( diff --git a/apps/heft/src/configuration/RigPackageResolver.ts b/apps/heft/src/configuration/RigPackageResolver.ts index 0f08c95e6b7..03a70db6dc4 100644 --- a/apps/heft/src/configuration/RigPackageResolver.ts +++ b/apps/heft/src/configuration/RigPackageResolver.ts @@ -1,15 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; +import * as path from 'node:path'; + import { PackageJsonLookup, Import, - type ITerminal, type INodePackageJson, type IPackageJson } from '@rushstack/node-core-library'; -import type { RigConfig } from '@rushstack/rig-package'; +import type { ITerminal } from '@rushstack/terminal'; +import type { IRigConfig } from '@rushstack/rig-package'; /** * Rig resolves requested tools from the project's Heft rig. @@ -29,7 +30,7 @@ export interface IRigPackageResolver { export interface IRigPackageResolverOptions { buildFolder: string; projectPackageJson: IPackageJson; - rigConfig: RigConfig; + rigConfig: IRigConfig; } /** @@ -38,7 +39,7 @@ export interface IRigPackageResolverOptions { export class RigPackageResolver implements IRigPackageResolver { private readonly _buildFolder: string; private readonly _projectPackageJson: IPackageJson; - private readonly _rigConfig: RigConfig; + private readonly _rigConfig: IRigConfig; private readonly _packageJsonLookup: PackageJsonLookup = new PackageJsonLookup(); private readonly _resolverCache: Map> = new Map(); @@ -100,7 +101,7 @@ export class RigPackageResolver implements IRigPackageResolver { } // See if the project rig has a regular dependency on the package - const rigConfiguration: RigConfig = this._rigConfig; + const rigConfiguration: IRigConfig = this._rigConfig; if (rigConfiguration.rigFound) { const rigFolder: string = rigConfiguration.getResolvedProfileFolder(); const rigPackageJsonPath: string | undefined = diff --git a/apps/heft/src/configuration/types.ts b/apps/heft/src/configuration/types.ts new file mode 100644 index 00000000000..46d9e68f411 --- /dev/null +++ b/apps/heft/src/configuration/types.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +export type { + CustomValidationFunction, + ICustomJsonPathMetadata, + ICustomPropertyInheritance, + IJsonPathMetadata, + IJsonPathMetadataResolverOptions, + IJsonPathsMetadata, + INonCustomJsonPathMetadata, + IOriginalValueOptions, + IProjectConfigurationFileSpecification, + IPropertiesInheritance, + IPropertyInheritance, + IPropertyInheritanceDefaults, + InheritanceType, + PathResolutionMethod, + PropertyInheritanceCustomFunction +} from '@rushstack/heft-config-file'; diff --git a/apps/heft/src/index.ts b/apps/heft/src/index.ts index 09be18b4754..0c6006dfc8c 100644 --- a/apps/heft/src/index.ts +++ b/apps/heft/src/index.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +/// + /** * Heft is a config-driven toolchain that invokes other popular tools such * as TypeScript, ESLint, Jest, Webpack, and API Extractor. You can use it to build @@ -9,6 +11,9 @@ * @packageDocumentation */ +import type * as ConfigurationFile from './configuration/types'; +export type { ConfigurationFile }; + export { HeftConfiguration, type IHeftConfigurationInitializationOptions as _IHeftConfigurationInitializationOptions @@ -18,13 +23,6 @@ export type { IRigPackageResolver } from './configuration/RigPackageResolver'; export type { IHeftPlugin, IHeftTaskPlugin, IHeftLifecyclePlugin } from './pluginFramework/IHeftPlugin'; -export { - CancellationTokenSource, - CancellationToken, - type ICancellationTokenSourceOptions, - type ICancellationTokenOptions as _ICancellationTokenOptions -} from './pluginFramework/CancellationToken'; - export type { IHeftParameters, IHeftDefaultParameters } from './pluginFramework/HeftParameterManager'; export type { @@ -32,10 +30,15 @@ export type { IHeftLifecycleHooks, IHeftLifecycleCleanHookOptions, IHeftLifecycleToolStartHookOptions, - IHeftLifecycleToolFinishHookOptions + IHeftLifecycleToolFinishHookOptions, + IHeftTaskStartHookOptions, + IHeftTaskFinishHookOptions, + IHeftPhaseStartHookOptions, + IHeftPhaseFinishHookOptions } from './pluginFramework/HeftLifecycleSession'; export type { + IHeftParsedCommandLine, IHeftTaskSession, IHeftTaskHooks, IHeftTaskFileOperations, @@ -51,7 +54,14 @@ export type { IRunScript, IRunScriptOptions } from './plugins/RunScriptPlugin'; export type { IFileSelectionSpecifier, IGlobOptions, GlobFn, WatchGlobFn } from './plugins/FileGlobSpecifier'; -export type { IWatchedFileState } from './utilities/WatchFileSystemAdapter'; +export type { + IWatchedFileState, + IWatchFileSystem, + ReaddirDirentCallback, + ReaddirStringCallback, + StatCallback, + IReaddirOptions +} from './utilities/WatchFileSystemAdapter'; export { type IHeftRecordMetricsHookOptions, @@ -73,3 +83,9 @@ export type { CommandLineStringListParameter, CommandLineStringParameter } from '@rushstack/ts-command-line'; + +export type { IHeftTaskOperationMetadata } from './cli/HeftActionRunner'; +export type { IHeftPhaseOperationMetadata } from './cli/HeftActionRunner'; + +export type { IHeftTask } from './pluginFramework/HeftTask'; +export type { IHeftPhase } from './pluginFramework/HeftPhase'; diff --git a/apps/heft/src/metrics/MetricsCollector.ts b/apps/heft/src/metrics/MetricsCollector.ts index 08ca1ec2aa5..b7f6352387d 100644 --- a/apps/heft/src/metrics/MetricsCollector.ts +++ b/apps/heft/src/metrics/MetricsCollector.ts @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as os from 'os'; +import * as os from 'node:os'; +import { performance } from 'node:perf_hooks'; + import { AsyncParallelHook } from 'tapable'; -import { performance } from 'perf_hooks'; + import { InternalError } from '@rushstack/node-core-library'; /** @@ -21,10 +23,24 @@ export interface IMetricsData { encounteredError?: boolean; /** - * The amount of time the command took to execute, in milliseconds. + * The total execution duration of all user-defined tasks from `heft.json`, in milliseconds. + * This metric is for measuring the cumulative time spent on the underlying build steps for a project. + * If running in watch mode, this will be the duration of the most recent incremental build. */ taskTotalExecutionMs: number; + /** + * The total duration before Heft started executing user-defined tasks, in milliseconds. + * This metric is for tracking the contribution of Heft itself to total build duration. + */ + bootDurationMs: number; + + /** + * How long the process has been alive, in milliseconds. + * This metric is for watch mode, to analyze how long developers leave individual Heft sessions running. + */ + totalUptimeMs: number; + /** * The name of the operating system provided by NodeJS. */ @@ -87,12 +103,17 @@ export class MetricsCollector { public readonly recordMetricsHook: AsyncParallelHook = new AsyncParallelHook(['recordMetricsHookOptions']); + private _bootDurationMs: number | undefined; private _startTimeMs: number | undefined; /** * Start metrics log timer. */ public setStartTime(): void { + if (this._bootDurationMs === undefined) { + // Only set this once. This is for tracking boot overhead. + this._bootDurationMs = process.uptime() * 1000; + } this._startTimeMs = performance.now(); } @@ -108,7 +129,8 @@ export class MetricsCollector { performanceData?: Partial, parameters?: Record ): Promise { - if (this._startTimeMs === undefined) { + const { _bootDurationMs, _startTimeMs } = this; + if (_bootDurationMs === undefined || _startTimeMs === undefined) { throw new InternalError('MetricsCollector has not been initialized with setStartTime() yet'); } @@ -117,18 +139,26 @@ export class MetricsCollector { } const filledPerformanceData: IPerformanceData = { - taskTotalExecutionMs: (performance.now() - this._startTimeMs) / 1000, + taskTotalExecutionMs: performance.now() - _startTimeMs, ...(performanceData || {}) }; + const { taskTotalExecutionMs } = filledPerformanceData; + + const cpus: os.CpuInfo[] = os.cpus(); + const metricData: IMetricsData = { command: command, encounteredError: filledPerformanceData.encounteredError, - taskTotalExecutionMs: filledPerformanceData.taskTotalExecutionMs, + bootDurationMs: _bootDurationMs, + taskTotalExecutionMs: taskTotalExecutionMs, + totalUptimeMs: process.uptime() * 1000, machineOs: process.platform, machineArch: process.arch, - machineCores: os.cpus().length, - machineProcessor: os.cpus()[0].model, + machineCores: cpus.length, + // The Node.js model is sometimes padded, for example: + // "AMD Ryzen 7 3700X 8-Core Processor + machineProcessor: cpus[0].model.trim(), machineTotalMemoryMB: os.totalmem(), commandParameters: parameters || {} }; diff --git a/apps/heft/src/operations/IOperationRunner.ts b/apps/heft/src/operations/IOperationRunner.ts deleted file mode 100644 index b71cde79db8..00000000000 --- a/apps/heft/src/operations/IOperationRunner.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import type { OperationStatus } from './OperationStatus'; -import type { OperationError } from './OperationError'; - -import type { CancellationToken } from '../pluginFramework/CancellationToken'; -import type { Stopwatch } from '../utilities/Stopwatch'; - -/** - * Information passed to the executing `IOperationRunner` - * - * @beta - */ -export interface IOperationRunnerContext { - /** - * A cancellation token for the overarching execution. Runners should do their best to gracefully abort - * as soon as possible if the cancellation token is canceled. - */ - cancellationToken: CancellationToken; - - /** - * If this is the first time this operation has been executed. - */ - isFirstRun: boolean; - - /** - * A callback to the overarching orchestrator to request that the operation be invoked again. - * Used in watch mode to signal that inputs have changed. - */ - requestRun?: () => void; -} - -/** - * - */ -export interface IOperationState { - status: OperationStatus; - error: OperationError | undefined; - stopwatch: Stopwatch; -} - -export interface IOperationStates { - readonly state: Readonly | undefined; - readonly lastState: Readonly | undefined; -} - -/** - * The `Operation` class is a node in the dependency graph of work that needs to be scheduled by the - * `OperationExecutionManager`. Each `Operation` has a `runner` member of type `IOperationRunner`, whose - * implementation manages the actual process for running a single operation. - * - * @beta - */ -export interface IOperationRunner { - /** - * Name of the operation, for logging. - */ - readonly name: string; - - /** - * Indicates that this runner is architectural and should not be reported on. - */ - silent: boolean; - - /** - * Method to be executed for the operation. - */ - executeAsync(context: IOperationRunnerContext): Promise; -} diff --git a/apps/heft/src/operations/Operation.ts b/apps/heft/src/operations/Operation.ts deleted file mode 100644 index e30a678d665..00000000000 --- a/apps/heft/src/operations/Operation.ts +++ /dev/null @@ -1,325 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { InternalError, ITerminal } from '@rushstack/node-core-library'; - -import { Stopwatch } from '../utilities/Stopwatch'; -import type { - IOperationRunner, - IOperationRunnerContext, - IOperationState, - IOperationStates -} from './IOperationRunner'; -import { OperationError } from './OperationError'; -import { OperationStatus } from './OperationStatus'; - -/** - * Options for constructing a new Operation. - * @alpha - */ -export interface IOperationOptions { - /** - * The group that this operation belongs to. Will be used for logging and duration tracking. - */ - groupName?: string | undefined; - - /** - * When the scheduler is ready to process this `Operation`, the `runner` implements the actual work of - * running the operation. - */ - runner?: IOperationRunner | undefined; - - /** - * The weight used by the scheduler to determine order of execution. - */ - weight?: number | undefined; -} - -/** - * Information provided to `executeAsync` by the `OperationExecutionManager`. - */ -export interface IExecuteOperationContext extends Omit { - /** - * Function to invoke before execution of an operation, for logging. - */ - beforeExecute(operation: Operation, state: IOperationState): void; - /** - * Function to invoke after execution of an operation, for logging. - */ - afterExecute(operation: Operation, state: IOperationState): void; - /** - * Function used to schedule the concurrency-limited execution of an operation. - */ - queueWork(workFn: () => Promise, priority: number): Promise; - - /** - * A callback to the overarching orchestrator to request that the operation be invoked again. - * Used in watch mode to signal that inputs have changed. - */ - requestRun?: (requestor?: string) => void; - - /** - * Terminal to write output to. - */ - terminal: ITerminal; -} - -/** - * The `Operation` class is a node in the dependency graph of work that needs to be scheduled by the - * `OperationExecutionManager`. Each `Operation` has a `runner` member of type `IOperationRunner`, whose - * implementation manages the actual process of running a single operation. - * - * The graph of `Operation` instances will be cloned into a separate execution graph after processing. - * - * @alpha - */ -export class Operation implements IOperationStates { - /** - * A set of all dependencies which must be executed before this operation is complete. - */ - public readonly dependencies: Set = new Set(); - /** - * A set of all operations that wait for this operation. - */ - public readonly consumers: Set = new Set(); - /** - * If specified, the name of a grouping to which this Operation belongs, for logging start and end times. - */ - public readonly groupName: string | undefined; - - /** - * When the scheduler is ready to process this `Operation`, the `runner` implements the actual work of - * running the operation. - */ - public runner: IOperationRunner | undefined = undefined; - - /** - * This number represents how far away this Operation is from the furthest "root" operation (i.e. - * an operation with no consumers). This helps us to calculate the critical path (i.e. the - * longest chain of projects which must be executed in order, thereby limiting execution speed - * of the entire operation tree. - * - * This number is calculated via a memoized depth-first search, and when choosing the next - * operation to execute, the operation with the highest criticalPathLength is chosen. - * - * Example: - * (0) A - * \ - * (1) B C (0) (applications) - * \ /|\ - * \ / | \ - * (2) D | X (1) (utilities) - * | / \ - * |/ \ - * (2) Y Z (2) (other utilities) - * - * All roots (A & C) have a criticalPathLength of 0. - * B has a score of 1, since A depends on it. - * D has a score of 2, since we look at the longest chain (e.g D->B->A is longer than D->C) - * X has a score of 1, since the only package which depends on it is A - * Z has a score of 2, since only X depends on it, and X has a score of 1 - * Y has a score of 2, since the chain Y->X->C is longer than Y->C - * - * The algorithm is implemented in AsyncOperationQueue.ts as calculateCriticalPathLength() - */ - public criticalPathLength: number | undefined = undefined; - - /** - * The weight for this operation. This scalar is the contribution of this operation to the - * `criticalPathLength` calculation above. Modify to indicate the following: - * - `weight` === 1: indicates that this operation has an average duration - * - `weight` > 1: indicates that this operation takes longer than average and so the scheduler - * should try to favor starting it over other, shorter operations. An example might be an operation that - * bundles an entire application and runs whole-program optimization. - * - `weight` < 1: indicates that this operation takes less time than average and so the scheduler - * should favor other, longer operations over it. An example might be an operation to unpack a cached - * output, or an operation using NullOperationRunner, which might use a value of 0. - */ - public weight: number; - - /** - * The state of this operation the previous time a manager was invoked. - */ - public lastState: IOperationState | undefined = undefined; - - /** - * The current state of this operation - */ - public state: IOperationState | undefined = undefined; - - /** - * A cached execution promise for the current OperationExecutionManager invocation of this operation. - */ - private _promise: Promise | undefined = undefined; - - /** - * If true, then a run of this operation is currently wanted. - * This is used to track state from the `requestRun` callback passed to the runner. - */ - private _runPending: boolean = true; - - public constructor(options?: IOperationOptions) { - this.groupName = options?.groupName; - this.runner = options?.runner; - this.weight = options?.weight || 1; - } - - /** - * The name of this operation, for logging. - */ - public get name(): string | undefined { - return this.runner?.name; - } - - public addDependency(dependency: Operation): void { - this.dependencies.add(dependency); - dependency.consumers.add(this); - } - - public deleteDependency(dependency: Operation): void { - this.dependencies.delete(dependency); - dependency.consumers.delete(this); - } - - public reset(): void { - // Reset operation state - this.lastState = this.state; - - this.state = { - status: OperationStatus.Ready, - error: undefined, - stopwatch: new Stopwatch() - }; - - this._promise = undefined; - this._runPending = true; - } - - /** - * @internal - */ - public async _executeAsync(context: IExecuteOperationContext): Promise { - const { state } = this; - if (!state) { - throw new Error(`Operation state has not been initialized.`); - } - - if (!this._promise) { - this._promise = this._executeInnerAsync(context, state); - } - - return this._promise; - } - - private async _executeInnerAsync( - context: IExecuteOperationContext, - rawState: IOperationState - ): Promise { - const state: IOperationState = rawState; - const { runner } = this; - - const dependencyResults: PromiseSettledResult[] = await Promise.allSettled( - Array.from(this.dependencies, (dependency: Operation) => dependency._executeAsync(context)) - ); - - const { cancellationToken, requestRun, queueWork } = context; - - if (cancellationToken.isCancelled) { - state.status = OperationStatus.Cancelled; - return state.status; - } - - for (const result of dependencyResults) { - if ( - result.status === 'rejected' || - result.value === OperationStatus.Blocked || - result.value === OperationStatus.Failure - ) { - state.status = OperationStatus.Blocked; - return state.status; - } - } - - const innerContext: IOperationRunnerContext = { - cancellationToken, - isFirstRun: !this.lastState, - requestRun: requestRun - ? () => { - switch (this.state?.status) { - case OperationStatus.Ready: - case OperationStatus.Executing: - // If current status has not yet resolved to a fixed value, - // re-executing this operation does not require a full rerun - // of the operation graph. Simply mark that a run is requested. - - // This variable is on the Operation instead of the - // containing closure to deal with scenarios in which - // the runner hangs on to an old copy of the callback. - this._runPending = true; - return; - - case OperationStatus.Blocked: - case OperationStatus.Cancelled: - case OperationStatus.Failure: - case OperationStatus.NoOp: - case OperationStatus.Success: - // The requestRun callback is assumed to remain constant - // throughout the lifetime of the process, so it is safe - // to capture here. - return requestRun(this.name); - default: - throw new InternalError(`Unexpected status: ${this.state?.status}`); - } - } - : undefined - }; - - await queueWork(async () => { - // Redundant variable to satisfy require-atomic-updates - const innerState: IOperationState = state; - - if (cancellationToken.isCancelled) { - innerState.status = OperationStatus.Cancelled; - return innerState.status; - } - - context.beforeExecute(this, innerState); - - innerState.status = OperationStatus.Executing; - innerState.stopwatch.start(); - - while (this._runPending) { - this._runPending = false; - try { - innerState.status = runner ? await runner.executeAsync(innerContext) : OperationStatus.NoOp; - } catch (error) { - innerState.status = OperationStatus.Failure; - innerState.error = error as OperationError; - } - - // Since runner.executeAsync is async, a change could have occurred that requires re-execution - // This operation is still active, so can re-execute immediately, rather than forcing a whole - // new execution pass. - - // As currently written, this does mean that if a job is scheduled with higher priority while - // this operation is still executing, it will still wait for this retry. This may not be desired - // and if it becomes a problem, the retry loop will need to be moved outside of the `queueWork` call. - // This introduces complexity regarding tracking of timing and start/end logging, however. - - if (this._runPending) { - if (cancellationToken.isCancelled) { - innerState.status = OperationStatus.Cancelled; - break; - } else { - context.terminal.writeLine(`Immediate rerun requested. Executing.`); - } - } - } - - state.stopwatch.stop(); - context.afterExecute(this, state); - }, this.criticalPathLength ?? 0); - - return state.status; - } -} diff --git a/apps/heft/src/operations/OperationExecutionManager.ts b/apps/heft/src/operations/OperationExecutionManager.ts deleted file mode 100644 index 5d6b28bb3c7..00000000000 --- a/apps/heft/src/operations/OperationExecutionManager.ts +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import type { ITerminal } from '@rushstack/node-core-library'; - -import { OperationStatus } from './OperationStatus'; -import type { Operation, IExecuteOperationContext } from './Operation'; -import { OperationGroupRecord } from './OperationGroupRecord'; -import { CancellationToken } from '../pluginFramework/CancellationToken'; -import { calculateCriticalPathLengths } from './calculateCriticalPath'; -import type { IOperationState } from './IOperationRunner'; - -export interface IOperationExecutionOptions { - cancellationToken: CancellationToken; - parallelism: number; - terminal: ITerminal; - - requestRun?: (requestor?: string) => void; -} - -/** - * A class which manages the execution of a set of tasks with interdependencies. - * Initially, and at the end of each task execution, all unblocked tasks - * are added to a ready queue which is then executed. This is done continually until all - * tasks are complete, or prematurely fails if any of the tasks fail. - */ -export class OperationExecutionManager { - /** - * The set of operations that will be executed - */ - private readonly _operations: Operation[]; - /** - * Group records are metadata-only entities used for tracking the start and end of a set of related tasks. - * This is the only extent to which the operation graph is aware of Heft phases. - */ - private readonly _groupRecordByName: Map; - /** - * The total number of non-silent operations in the graph. - * Silent operations are generally used to simplify the construction of the graph. - */ - private readonly _trackedOperationCount: number; - - public constructor(operations: ReadonlySet) { - const groupRecordByName: Map = new Map(); - this._groupRecordByName = groupRecordByName; - - let trackedOperationCount: number = 0; - for (const operation of operations) { - const { groupName } = operation; - let group: OperationGroupRecord | undefined = undefined; - if (groupName && !(group = groupRecordByName.get(groupName))) { - group = new OperationGroupRecord(groupName); - groupRecordByName.set(groupName, group); - } - - group?.addOperation(operation); - - if (!operation.runner?.silent) { - // Only count non-silent operations - trackedOperationCount++; - } - } - - this._trackedOperationCount = trackedOperationCount; - - this._operations = calculateCriticalPathLengths(operations); - - for (const consumer of operations) { - for (const dependency of consumer.dependencies) { - if (!operations.has(dependency)) { - throw new Error( - `Operation ${JSON.stringify(consumer.name)} declares a dependency on operation ` + - `${JSON.stringify(dependency.name)} that is not in the set of operations to execute.` - ); - } - } - } - } - - /** - * Executes all operations which have been registered, returning a promise which is resolved when all the - * operations are completed successfully, or rejects when any operation fails. - */ - public async executeAsync(executionOptions: IOperationExecutionOptions): Promise { - let hasReportedFailures: boolean = false; - - const { cancellationToken, parallelism, terminal, requestRun } = executionOptions; - - const startedGroups: Set = new Set(); - const finishedGroups: Set = new Set(); - - const maxParallelism: number = Math.min(this._operations.length, parallelism); - const groupRecords: Map = this._groupRecordByName; - for (const groupRecord of groupRecords.values()) { - groupRecord.reset(); - } - - for (const operation of this._operations) { - operation.reset(); - } - - terminal.writeVerboseLine(`Executing a maximum of ${maxParallelism} simultaneous tasks...`); - - const executionContext: IExecuteOperationContext = { - terminal, - cancellationToken, - - requestRun, - - queueWork: (workFn: () => Promise, priority: number): Promise => { - // TODO: Update to throttle parallelism - // Can just be a standard priority queue from async - return workFn(); - }, - - beforeExecute: (operation: Operation): void => { - // Initialize group if uninitialized and log the group name - const { groupName } = operation; - const groupRecord: OperationGroupRecord | undefined = groupName - ? groupRecords.get(groupName) - : undefined; - if (groupRecord && !startedGroups.has(groupRecord)) { - startedGroups.add(groupRecord); - groupRecord.startTimer(); - terminal.writeLine(` ---- ${groupRecord.name} started ---- `); - } - }, - - afterExecute: (operation: Operation, state: IOperationState): void => { - const { groupName } = operation; - const groupRecord: OperationGroupRecord | undefined = groupName - ? groupRecords.get(groupName) - : undefined; - if (groupRecord) { - groupRecord.setOperationAsComplete(operation, state); - } - - if (state.status === OperationStatus.Failure) { - // This operation failed. Mark it as such and all reachable dependents as blocked. - // Failed operations get reported, even if silent. - // Generally speaking, silent operations shouldn't be able to fail, so this is a safety measure. - const message: string | undefined = state.error?.message; - if (message) { - terminal.writeErrorLine(message); - } - hasReportedFailures = true; - } - - // Log out the group name and duration if it is the last operation in the group - if (groupRecord?.finished && !finishedGroups.has(groupRecord)) { - finishedGroups.add(groupRecord); - const finishedLoggingWord: string = groupRecord.hasFailures - ? 'encountered an error' - : groupRecord.hasCancellations - ? 'cancelled' - : 'finished'; - terminal.writeLine( - ` ---- ${groupRecord.name} ${finishedLoggingWord} (${groupRecord.duration.toFixed(3)}s) ---- ` - ); - } - } - }; - - await Promise.all(this._operations.map((record: Operation) => record._executeAsync(executionContext))); - - const finalStatus: OperationStatus = - this._trackedOperationCount === 0 - ? OperationStatus.NoOp - : cancellationToken.isCancelled - ? OperationStatus.Cancelled - : hasReportedFailures - ? OperationStatus.Failure - : OperationStatus.Success; - - return finalStatus; - } -} diff --git a/apps/heft/src/operations/runners/LifecycleOperationRunner.ts b/apps/heft/src/operations/runners/LifecycleOperationRunner.ts deleted file mode 100644 index dddd694e913..00000000000 --- a/apps/heft/src/operations/runners/LifecycleOperationRunner.ts +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { performance } from 'perf_hooks'; -import { AlreadyReportedError, InternalError } from '@rushstack/node-core-library'; - -import { deleteFilesAsync } from '../../plugins/DeleteFilesPlugin'; -import { OperationStatus } from '../OperationStatus'; -import type { IOperationRunner, IOperationRunnerContext } from '../IOperationRunner'; -import type { InternalHeftSession } from '../../pluginFramework/InternalHeftSession'; -import type { ScopedLogger } from '../../pluginFramework/logging/ScopedLogger'; -import type { HeftLifecycle } from '../../pluginFramework/HeftLifecycle'; -import type { IDeleteOperation } from '../../plugins/DeleteFilesPlugin'; -import type { - IHeftLifecycleCleanHookOptions, - IHeftLifecycleToolStartHookOptions, - IHeftLifecycleToolFinishHookOptions, - IHeftLifecycleSession -} from '../../pluginFramework/HeftLifecycleSession'; - -export type LifecycleOperationRunnerType = 'start' | 'finish'; - -export interface ILifecycleOperationRunnerOptions { - internalHeftSession: InternalHeftSession; - type: LifecycleOperationRunnerType; -} - -export class LifecycleOperationRunner implements IOperationRunner { - private readonly _options: ILifecycleOperationRunnerOptions; - - public readonly silent: boolean = true; - - public get name(): string { - return `Lifecycle ${JSON.stringify(this._options.type)}`; - } - - public constructor(options: ILifecycleOperationRunnerOptions) { - this._options = options; - } - - public async executeAsync(context: IOperationRunnerContext): Promise { - const { internalHeftSession, type } = this._options; - const { clean, cleanCache, watch } = internalHeftSession.parameterManager.defaultParameters; - - if (watch) { - // Avoid running the lifecycle operation when in watch mode - return OperationStatus.NoOp; - } - - const lifecycle: HeftLifecycle = internalHeftSession.lifecycle; - const lifecycleLogger: ScopedLogger = internalHeftSession.loggingManager.requestScopedLogger( - `lifecycle:${this._options.type}` - ); - - switch (type) { - case 'start': { - // We can only apply the plugins once, so only do it during the start operation - lifecycleLogger.terminal.writeVerboseLine('Applying lifecycle plugins'); - await lifecycle.applyPluginsAsync(); - - // Run the clean hook - if (clean) { - const startTime: number = performance.now(); - const cleanLogger: ScopedLogger = - internalHeftSession.loggingManager.requestScopedLogger(`lifecycle:clean`); - cleanLogger.terminal.writeVerboseLine('Starting clean'); - - // Grab the additional clean operations from the phase - const deleteOperations: IDeleteOperation[] = []; - - // Delete all temp folders for tasks by default - for (const pluginDefinition of lifecycle.pluginDefinitions) { - const lifecycleSession: IHeftLifecycleSession = - await lifecycle.getSessionForPluginDefinitionAsync(pluginDefinition); - deleteOperations.push({ sourcePath: lifecycleSession.tempFolderPath }); - - // Also delete the cache folder if requested - if (cleanCache) { - deleteOperations.push({ sourcePath: lifecycleSession.cacheFolderPath }); - } - } - - // Create the options and provide a utility method to obtain paths to delete - const cleanHookOptions: IHeftLifecycleCleanHookOptions = { - addDeleteOperations: (...deleteOperationsToAdd: IDeleteOperation[]) => - deleteOperations.push(...deleteOperationsToAdd) - }; - - // Run the plugin clean hook - if (lifecycle.hooks.clean.isUsed()) { - try { - await lifecycle.hooks.clean.promise(cleanHookOptions); - } catch (e: unknown) { - // Log out using the clean logger, and return an error status - if (!(e instanceof AlreadyReportedError)) { - cleanLogger.emitError(e as Error); - } - return OperationStatus.Failure; - } - } - - // Delete the files if any were specified - if (deleteOperations.length) { - await deleteFilesAsync(deleteOperations, cleanLogger.terminal); - } - - cleanLogger.terminal.writeVerboseLine(`Finished clean (${performance.now() - startTime}ms)`); - } - - // Run the start hook - if (lifecycle.hooks.toolStart.isUsed()) { - const lifecycleToolStartHookOptions: IHeftLifecycleToolStartHookOptions = {}; - await lifecycle.hooks.toolStart.promise(lifecycleToolStartHookOptions); - } - break; - } - case 'finish': { - if (lifecycle.hooks.toolFinish.isUsed()) { - const lifeycleToolFinishHookOptions: IHeftLifecycleToolFinishHookOptions = {}; - await lifecycle.hooks.toolFinish.promise(lifeycleToolFinishHookOptions); - } - break; - } - default: { - // Should never happen, but just in case - throw new InternalError(`Unrecognized lifecycle type: ${this._options.type}`); - } - } - - // Return success and allow for the TaskOperationRunner to execute tasks - return OperationStatus.Success; - } -} diff --git a/apps/heft/src/operations/runners/PhaseOperationRunner.ts b/apps/heft/src/operations/runners/PhaseOperationRunner.ts index c1f13c16011..f6f1ba3ed0d 100644 --- a/apps/heft/src/operations/runners/PhaseOperationRunner.ts +++ b/apps/heft/src/operations/runners/PhaseOperationRunner.ts @@ -1,14 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { performance } from 'perf_hooks'; +import { + type IOperationRunner, + type IOperationRunnerContext, + OperationStatus +} from '@rushstack/operation-graph'; -import { OperationStatus } from '../OperationStatus'; import { deleteFilesAsync, type IDeleteOperation } from '../../plugins/DeleteFilesPlugin'; -import type { IOperationRunner, IOperationRunnerContext } from '../IOperationRunner'; import type { HeftPhase } from '../../pluginFramework/HeftPhase'; import type { HeftPhaseSession } from '../../pluginFramework/HeftPhaseSession'; -import type { HeftTaskSession } from '../../pluginFramework/HeftTaskSession'; import type { InternalHeftSession } from '../../pluginFramework/InternalHeftSession'; export interface IPhaseOperationRunnerOptions { @@ -20,6 +21,7 @@ export class PhaseOperationRunner implements IOperationRunner { public readonly silent: boolean = true; private readonly _options: IPhaseOperationRunnerOptions; + private _isClean: boolean = false; public get name(): string { return `Phase ${JSON.stringify(this._options.phase.phaseName)}`; @@ -31,46 +33,45 @@ export class PhaseOperationRunner implements IOperationRunner { public async executeAsync(context: IOperationRunnerContext): Promise { const { internalHeftSession, phase } = this._options; - const { clean, cleanCache, watch } = internalHeftSession.parameterManager.defaultParameters; + const { clean } = internalHeftSession.parameterManager.defaultParameters; // Load and apply the plugins for this phase only const phaseSession: HeftPhaseSession = internalHeftSession.getSessionForPhase(phase); const { phaseLogger, cleanLogger } = phaseSession; - phaseLogger.terminal.writeVerboseLine('Applying task plugins'); - await phaseSession.applyPluginsAsync(); + await phaseSession.applyPluginsAsync(phaseLogger.terminal); - if (watch) { - // Avoid running the phase operation when in watch mode + if (this._isClean || !clean) { return OperationStatus.NoOp; } // Run the clean hook - if (clean) { - const startTime: number = performance.now(); - - // Grab the additional clean operations from the phase - cleanLogger.terminal.writeVerboseLine('Starting clean'); - const deleteOperations: IDeleteOperation[] = Array.from(phase.cleanFiles); - - // Delete all temp folders for tasks by default - for (const task of phase.tasks) { - const taskSession: HeftTaskSession = phaseSession.getSessionForTask(task); - deleteOperations.push({ sourcePath: taskSession.tempFolderPath }); - - // Also delete the cache folder if requested - if (cleanCache) { - deleteOperations.push({ sourcePath: taskSession.cacheFolderPath }); - } - } - - // Delete the files if any were specified - if (deleteOperations.length) { - await deleteFilesAsync(deleteOperations, cleanLogger.terminal); - } - - cleanLogger.terminal.writeVerboseLine(`Finished clean (${performance.now() - startTime}ms)`); + const startTime: number = performance.now(); + + // Grab the additional clean operations from the phase + cleanLogger.terminal.writeVerboseLine('Starting clean'); + const deleteOperations: IDeleteOperation[] = Array.from(phase.cleanFiles); + + // Delete all temp folders for tasks by default + const tempFolderGlobs: string[] = [ + /* heft@>0.60.0 */ phase.phaseName, + /* heft@<=0.60.0 */ `${phase.phaseName}.*` + ]; + deleteOperations.push({ + sourcePath: internalHeftSession.heftConfiguration.tempFolderPath, + includeGlobs: tempFolderGlobs + }); + + // Delete the files if any were specified + if (deleteOperations.length) { + const rootFolderPath: string = internalHeftSession.heftConfiguration.buildFolderPath; + await deleteFilesAsync(rootFolderPath, deleteOperations, cleanLogger.terminal); } + // Ensure we only run the clean operation once + this._isClean = true; + + cleanLogger.terminal.writeVerboseLine(`Finished clean (${performance.now() - startTime}ms)`); + // Return success and allow for the TaskOperationRunner to execute tasks return OperationStatus.Success; } diff --git a/apps/heft/src/operations/runners/TaskOperationRunner.ts b/apps/heft/src/operations/runners/TaskOperationRunner.ts index bd9e787db81..5cdd397d9b4 100644 --- a/apps/heft/src/operations/runners/TaskOperationRunner.ts +++ b/apps/heft/src/operations/runners/TaskOperationRunner.ts @@ -1,15 +1,25 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { performance } from 'perf_hooks'; +import { createHash, type Hash } from 'node:crypto'; +import { glob } from 'fast-glob'; + +import { + type IOperationRunner, + type IOperationRunnerContext, + OperationStatus +} from '@rushstack/operation-graph'; import { AlreadyReportedError, InternalError } from '@rushstack/node-core-library'; -import { OperationStatus } from '../OperationStatus'; -import { HeftTask } from '../../pluginFramework/HeftTask'; -import { copyFilesAsync } from '../../plugins/CopyFilesPlugin'; +import type { HeftTask } from '../../pluginFramework/HeftTask'; +import { + copyFilesAsync, + type ICopyOperation, + asAbsoluteCopyOperation, + asRelativeCopyOperation +} from '../../plugins/CopyFilesPlugin'; import { deleteFilesAsync } from '../../plugins/DeleteFilesPlugin'; -import type { IOperationRunner, IOperationRunnerContext } from '../IOperationRunner'; import type { HeftTaskSession, IHeftTaskFileOperations, @@ -18,12 +28,12 @@ import type { } from '../../pluginFramework/HeftTaskSession'; import type { HeftPhaseSession } from '../../pluginFramework/HeftPhaseSession'; import type { InternalHeftSession } from '../../pluginFramework/InternalHeftSession'; +import { watchGlobAsync, type IGlobOptions } from '../../plugins/FileGlobSpecifier'; import { - type IGlobOptions, - normalizeFileSelectionSpecifier, - watchGlobAsync -} from '../../plugins/FileGlobSpecifier'; -import { type IWatchedFileState, WatchFileSystemAdapter } from '../../utilities/WatchFileSystemAdapter'; + type IWatchedFileState, + type IWatchFileSystem, + WatchFileSystemAdapter +} from '../../utilities/WatchFileSystemAdapter'; export interface ITaskOperationRunnerOptions { internalHeftSession: InternalHeftSession; @@ -53,6 +63,7 @@ export class TaskOperationRunner implements IOperationRunner { private readonly _options: ITaskOperationRunnerOptions; private _fileOperations: IHeftTaskFileOperations | undefined = undefined; + private _copyConfigHash: string | undefined; private _watchFileSystemAdapter: WatchFileSystemAdapter | undefined = undefined; public readonly silent: boolean = false; @@ -78,20 +89,21 @@ export class TaskOperationRunner implements IOperationRunner { context: IOperationRunnerContext, taskSession: HeftTaskSession ): Promise { - const { cancellationToken, requestRun } = context; + const { abortSignal, requestRun } = context; const { hooks, logger } = taskSession; // Need to clear any errors or warnings from the previous invocation, particularly // if this is an immediate rerun logger.resetErrorsAndWarnings(); + const rootFolderPath: string = this._options.internalHeftSession.heftConfiguration.buildFolderPath; const isWatchMode: boolean = taskSession.parameters.watch && !!requestRun; const { terminal } = logger; // Exit the task early if cancellation is requested - if (cancellationToken.isCancelled) { - return OperationStatus.Cancelled; + if (abortSignal.aborted) { + return OperationStatus.Aborted; } if (!this._fileOperations && hooks.registerFileOperations.isUsed()) { @@ -100,12 +112,31 @@ export class TaskOperationRunner implements IOperationRunner { deleteOperations: new Set() }); - for (const copyOperation of fileOperations.copyOperations) { - // Consolidate fileExtensions, includeGlobs, excludeGlobs - normalizeFileSelectionSpecifier(copyOperation); + let copyConfigHash: string | undefined; + const { copyOperations } = fileOperations; + if (copyOperations.size > 0) { + // Do this here so that we only need to do it once for each Heft invocation + const hasher: Hash | undefined = createHash('sha256'); + const absolutePathCopyOperations: Set = new Set(); + for (const copyOperation of fileOperations.copyOperations) { + // The paths in the `fileOperations` object may be either absolute or relative + // For execution we need absolute paths. + const absoluteOperation: ICopyOperation = asAbsoluteCopyOperation(rootFolderPath, copyOperation); + absolutePathCopyOperations.add(absoluteOperation); + + // For portability of the hash we need relative paths. + const portableCopyOperation: ICopyOperation = asRelativeCopyOperation( + rootFolderPath, + absoluteOperation + ); + hasher.update(JSON.stringify(portableCopyOperation)); + } + fileOperations.copyOperations = absolutePathCopyOperations; + copyConfigHash = hasher.digest('base64'); } this._fileOperations = fileOperations; + this._copyConfigHash = copyConfigHash; } const shouldRunIncremental: boolean = isWatchMode && hooks.runIncremental.isUsed(); @@ -129,7 +160,8 @@ export class TaskOperationRunner implements IOperationRunner { async (): Promise => { // Create the options and provide a utility method to obtain paths to copy const runHookOptions: IHeftTaskRunHookOptions = { - cancellationToken + abortSignal, + globAsync: glob }; // Run the plugin run hook @@ -146,6 +178,9 @@ export class TaskOperationRunner implements IOperationRunner { fs: getWatchFileSystemAdapter() }); }, + get watchFs(): IWatchFileSystem { + return getWatchFileSystemAdapter(); + }, requestRun: requestRun! }; await hooks.runIncremental.promise(runIncrementalHookOptions); @@ -160,15 +195,15 @@ export class TaskOperationRunner implements IOperationRunner { return OperationStatus.Failure; } - if (cancellationToken.isCancelled) { - return OperationStatus.Cancelled; + if (abortSignal.aborted) { + return OperationStatus.Aborted; } return OperationStatus.Success; }, () => `Starting ${shouldRunIncremental ? 'incremental ' : ''}task execution`, () => { - const finishedWord: string = cancellationToken.isCancelled ? 'Cancelled' : 'Finished'; + const finishedWord: string = abortSignal.aborted ? 'Aborted' : 'Finished'; return `${finishedWord} ${shouldRunIncremental ? 'incremental ' : ''}task execution`; }, terminal.writeVerboseLine.bind(terminal) @@ -178,16 +213,21 @@ export class TaskOperationRunner implements IOperationRunner { if (this._fileOperations) { const { copyOperations, deleteOperations } = this._fileOperations; + const copyConfigHash: string | undefined = this._copyConfigHash; await Promise.all([ - copyOperations.size > 0 + copyConfigHash ? copyFilesAsync( copyOperations, logger.terminal, + `${taskSession.tempFolderPath}/file-copy.json`, + copyConfigHash, isWatchMode ? getWatchFileSystemAdapter() : undefined ) : Promise.resolve(), - deleteOperations.size > 0 ? deleteFilesAsync(deleteOperations, logger.terminal) : Promise.resolve() + deleteOperations.size > 0 + ? deleteFilesAsync(rootFolderPath, deleteOperations, logger.terminal) + : Promise.resolve() ]); } @@ -200,8 +240,8 @@ export class TaskOperationRunner implements IOperationRunner { // Even if the entire process has completed, we should mark the operation as cancelled if // cancellation has been requested. - if (cancellationToken.isCancelled) { - return OperationStatus.Cancelled; + if (abortSignal.aborted) { + return OperationStatus.Aborted; } if (logger.hasErrors) { diff --git a/apps/heft/src/pluginFramework/CancellationToken.ts b/apps/heft/src/pluginFramework/CancellationToken.ts deleted file mode 100644 index 8e86151312a..00000000000 --- a/apps/heft/src/pluginFramework/CancellationToken.ts +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { InternalError } from '@rushstack/node-core-library'; - -/** - * Options for the cancellation token. - * - * @internal - */ -export interface ICancellationTokenOptions { - /** - * A cancellation token source to use for the token. - * - * @internal - */ - cancellationTokenSource?: CancellationTokenSource; - - /** - * A static cancellation state. Mutually exclusive with `cancellationTokenSource`. - * If true, CancellationToken.isCancelled will always return true. Otherwise, - * CancellationToken.isCancelled will always return false. - * - * @internal - */ - isCancelled?: boolean; -} - -/** - * Options for the cancellation token source. - * - * @beta - */ -export interface ICancellationTokenSourceOptions { - /** - * Amount of time in milliseconds to wait before cancelling the token. - */ - delayMs?: number; -} - -/** - * A cancellation token. Can be used to signal that an ongoing process has either been cancelled - * or timed out. - * - * @remarks This class will eventually be removed once the `AbortSignal` API is available in - * the lowest supported LTS version of Node.js. See here for more information: - * https://nodejs.org/docs/latest-v16.x/api/globals.html#class-abortsignal - * - * @beta - */ -export class CancellationToken { - private readonly _isCancelled: boolean | undefined; - private readonly _cancellationTokenSource: CancellationTokenSource | undefined; - - /** @internal */ - public constructor(options: ICancellationTokenOptions = {}) { - if (options.cancellationTokenSource && options.isCancelled !== undefined) { - throw new InternalError( - 'CancellationTokenOptions.cancellationTokenSource and CancellationTokenOptions.isCancelled ' + - 'are mutually exclusive. Specify only one.' - ); - } - - this._cancellationTokenSource = options.cancellationTokenSource; - this._isCancelled = options.isCancelled; - } - - /** - * {@inheritdoc CancellationTokenSource.isCancelled} - */ - public get isCancelled(): boolean { - // Returns the cancellation state if it's explicitly set, otherwise returns the cancellation - // state from the source. If that too is not provided, the token is not cancellable. - return this._isCancelled ?? this._cancellationTokenSource?.isCancelled ?? false; - } - - /** - * Obtain a promise that resolves when the token is cancelled. - */ - public get onCancelledPromise(): Promise { - if (this._isCancelled !== undefined) { - // If the token is explicitly set to cancelled, return a resolved promise. - // If the token is explicitly set to not cancelled, return a promise that never resolves. - return this._isCancelled ? Promise.resolve() : new Promise(() => {}); - } else if (this._cancellationTokenSource) { - // Return the promise sourced from the cancellation token source - return this._cancellationTokenSource._onCancelledPromise; - } else { - // Neither provided, token can never be cancelled. Return a promise that never resovles. - return new Promise(() => {}); - } - } -} - -/** - * A cancellation token source. Produces cancellation tokens that can be used to signal that - * an ongoing process has either been cancelled or timed out. - * - * @remarks This class will eventually be removed once the `AbortController` API is available - * in the lowest supported LTS version of Node.js. See here for more information: - * https://nodejs.org/docs/latest-v16.x/api/globals.html#class-abortcontroller - * - * @beta - */ -export class CancellationTokenSource { - private readonly _cancellationToken: CancellationToken; - private readonly _cancellationPromise: Promise; - private _resolveCancellationPromise!: () => void; - private _isCancelled: boolean = false; - - public constructor(options: ICancellationTokenSourceOptions = {}) { - const { delayMs } = options; - this._cancellationToken = new CancellationToken({ cancellationTokenSource: this }); - this._cancellationPromise = new Promise((resolve) => { - this._resolveCancellationPromise = resolve; - }); - if (delayMs !== undefined) { - setTimeout(() => this.cancel(), delayMs); - } - } - - /** - * Whether or not the token has been cancelled. - */ - public get isCancelled(): boolean { - return this._isCancelled; - } - - /** - * Obtain the cancellation token produced by this source. - */ - public get token(): CancellationToken { - return this._cancellationToken; - } - - /** @internal */ - public get _onCancelledPromise(): Promise { - return this._cancellationPromise; - } - - /** - * Cancel the token provided by the source. - */ - public cancel(): void { - this._isCancelled = true; - this._resolveCancellationPromise(); - } -} diff --git a/apps/heft/src/pluginFramework/HeftLifecycle.ts b/apps/heft/src/pluginFramework/HeftLifecycle.ts index 1bdffc4f083..b762e23f238 100644 --- a/apps/heft/src/pluginFramework/HeftLifecycle.ts +++ b/apps/heft/src/pluginFramework/HeftLifecycle.ts @@ -1,14 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { AsyncParallelHook } from 'tapable'; +import { AsyncParallelHook, SyncHook } from 'tapable'; + import { InternalError } from '@rushstack/node-core-library'; import { HeftPluginConfiguration } from '../configuration/HeftPluginConfiguration'; import { HeftPluginHost } from './HeftPluginHost'; import type { InternalHeftSession } from './InternalHeftSession'; import type { IHeftConfigurationJsonPluginSpecifier } from '../utilities/CoreConfigFiles'; -import type { HeftLifecyclePluginDefinition } from '../configuration/HeftPluginDefinition'; +import type { + HeftLifecyclePluginDefinition, + HeftPluginDefinitionBase +} from '../configuration/HeftPluginDefinition'; import type { IHeftLifecyclePlugin, IHeftPlugin } from './IHeftPlugin'; import { HeftLifecycleSession, @@ -16,8 +20,13 @@ import { type IHeftLifecycleHooks, type IHeftLifecycleToolStartHookOptions, type IHeftLifecycleToolFinishHookOptions, - type IHeftLifecycleSession + type IHeftLifecycleSession, + type IHeftTaskStartHookOptions, + type IHeftTaskFinishHookOptions, + type IHeftPhaseStartHookOptions, + type IHeftPhaseFinishHookOptions } from './HeftLifecycleSession'; +import type { ScopedLogger } from './logging/ScopedLogger'; export interface IHeftLifecycleContext { lifecycleSession?: HeftLifecycleSession; @@ -34,6 +43,7 @@ export class HeftLifecycle extends HeftPluginHost { HeftLifecyclePluginDefinition, IHeftLifecyclePlugin > = new Map(); + private _lifecycleLogger: ScopedLogger | undefined; private _isInitialized: boolean = false; @@ -62,7 +72,11 @@ export class HeftLifecycle extends HeftPluginHost { clean: new AsyncParallelHook(), toolStart: new AsyncParallelHook(), toolFinish: new AsyncParallelHook(), - recordMetrics: internalHeftSession.metricsCollector.recordMetricsHook + recordMetrics: internalHeftSession.metricsCollector.recordMetricsHook, + taskStart: new SyncHook(['task']), + taskFinish: new SyncHook(['task']), + phaseStart: new SyncHook(['phase']), + phaseFinish: new SyncHook(['phase']) }; } @@ -144,11 +158,12 @@ export class HeftLifecycle extends HeftPluginHost { let pluginConfigurationIndex: number = 0; for (const pluginSpecifier of this._lifecyclePluginSpecifiers) { const pluginConfiguration: HeftPluginConfiguration = pluginConfigurations[pluginConfigurationIndex++]; - const pluginDefinition: HeftLifecyclePluginDefinition = + const pluginDefinition: HeftPluginDefinitionBase = pluginConfiguration.getPluginDefinitionBySpecifier(pluginSpecifier); // Ensure the plugin is a lifecycle plugin - if (!pluginConfiguration.lifecyclePluginDefinitions.has(pluginDefinition)) { + const isLifecyclePlugin: boolean = pluginConfiguration.isLifecyclePluginDefinition(pluginDefinition); + if (!isLifecyclePlugin) { throw new Error( `Plugin ${JSON.stringify(pluginDefinition.pluginName)} from package ` + `${JSON.stringify(pluginSpecifier.pluginPackage)} is not a lifecycle plugin.` @@ -174,6 +189,15 @@ export class HeftLifecycle extends HeftPluginHost { } } + public get lifecycleLogger(): ScopedLogger { + let logger: ScopedLogger | undefined = this._lifecycleLogger; + if (!logger) { + logger = this._internalHeftSession.loggingManager.requestScopedLogger(`lifecycle`); + this._lifecycleLogger = logger; + } + return logger; + } + public async getSessionForPluginDefinitionAsync( pluginDefinition: HeftLifecyclePluginDefinition ): Promise { diff --git a/apps/heft/src/pluginFramework/HeftLifecycleSession.ts b/apps/heft/src/pluginFramework/HeftLifecycleSession.ts index 0c481b556d4..b95d20d5e16 100644 --- a/apps/heft/src/pluginFramework/HeftLifecycleSession.ts +++ b/apps/heft/src/pluginFramework/HeftLifecycleSession.ts @@ -1,8 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; -import type { AsyncParallelHook } from 'tapable'; +import * as path from 'node:path'; + +import type { AsyncParallelHook, SyncHook } from 'tapable'; + +import type { Operation, OperationGroupRecord } from '@rushstack/operation-graph'; import type { IHeftRecordMetricsHookOptions, MetricsCollector } from '../metrics/MetricsCollector'; import type { ScopedLogger, IScopedLogger } from './logging/ScopedLogger'; @@ -11,6 +14,7 @@ import type { IHeftParameters } from './HeftParameterManager'; import type { IDeleteOperation } from '../plugins/DeleteFilesPlugin'; import type { HeftPluginDefinitionBase } from '../configuration/HeftPluginDefinition'; import type { HeftPluginHost } from './HeftPluginHost'; +import type { IHeftPhaseOperationMetadata, IHeftTaskOperationMetadata } from '../cli/HeftActionRunner'; /** * The lifecycle session is responsible for providing session-specific information to Heft lifecycle @@ -36,15 +40,6 @@ export interface IHeftLifecycleSession { */ readonly parameters: IHeftParameters; - /** - * The cache folder for the lifecycle plugin. This folder is unique for each lifecycle plugin, - * and will not be cleaned when Heft is run with `--clean`. However, it will be cleaned when - * Heft is run with `--clean` and `--clean-cache`. - * - * @public - */ - readonly cacheFolderPath: string; - /** * The temp folder for the lifecycle plugin. This folder is unique for each lifecycle plugin, * and will be cleaned when Heft is run with `--clean`. @@ -55,7 +50,7 @@ export interface IHeftLifecycleSession { /** * The scoped logger for the lifecycle plugin. Messages logged with this logger will be prefixed - * with the plugin name, in the format "[lifecycle:]". It is highly recommended that + * with the plugin name, in the format `[lifecycle:]`. It is highly recommended that * writing to the console be performed via the logger, as it will ensure that logging messages * are labeled with the source of the message. * @@ -76,6 +71,34 @@ export interface IHeftLifecycleSession { ): void; } +/** + * @public + */ +export interface IHeftTaskStartHookOptions { + operation: Operation; +} + +/** + * @public + */ +export interface IHeftTaskFinishHookOptions { + operation: Operation; +} + +/** + * @public + */ +export interface IHeftPhaseStartHookOptions { + operation: OperationGroupRecord; +} + +/** + * @public + */ +export interface IHeftPhaseFinishHookOptions { + operation: OperationGroupRecord; +} + /** * Hooks that are available to the lifecycle plugin. * @@ -102,15 +125,56 @@ export interface IHeftLifecycleHooks { /** * The `toolFinish` hook is called at the end of Heft execution. It is called after all phases have - * completed execution. To use it, call + * completed execution. Plugins that tap this hook are resposible for handling the scenario in which + * `toolStart` threw an error, since this hook is used to clean up any resources allocated earlier + * in the lifecycle and therefore runs even in error conditions. To use it, call * `toolFinish.tapPromise(, )`. * * @public */ toolFinish: AsyncParallelHook; - // TODO: Wire up and document this hook. + /** + * The `recordMetrics` hook is called at the end of every Heft execution pass. It is called after all + * phases have completed execution (or been canceled). In a watch run, it will be called several times + * in between `toolStart` and (if the session is gracefully interrupted via Ctrl+C), `toolFinish`. + * In a non-watch run, it will be invoked exactly once between `toolStart` and `toolFinish`. + * To use it, call `recordMetrics.tapPromise(, )`. + * @public + */ recordMetrics: AsyncParallelHook; + + /** + * The `taskStart` hook is called at the beginning of a task. It is called before the task has begun + * to execute. To use it, call `taskStart.tap(, )`. + * + * @public + */ + taskStart: SyncHook; + + /** + * The `taskFinish` hook is called at the end of a task. It is called after the task has completed + * execution. To use it, call `taskFinish.tap(, )`. + * + * @public + */ + taskFinish: SyncHook; + + /** + * The `phaseStart` hook is called at the beginning of a phase. It is called before the phase has + * begun to execute. To use it, call `phaseStart.tap(, )`. + * + * @public + */ + phaseStart: SyncHook; + + /** + * The `phaseFinish` hook is called at the end of a phase. It is called after the phase has completed + * execution. To use it, call `phaseFinish.tap(, )`. + * + * @public + */ + phaseFinish: SyncHook; } /** @@ -155,7 +219,6 @@ export class HeftLifecycleSession implements IHeftLifecycleSession { public readonly hooks: IHeftLifecycleHooks; public readonly parameters: IHeftParameters; - public readonly cacheFolderPath: string; public readonly tempFolderPath: string; public readonly logger: IScopedLogger; public readonly debug: boolean; @@ -177,9 +240,6 @@ export class HeftLifecycleSession implements IHeftLifecycleSession { // and lifecycle plugin names are enforced to be unique. const uniquePluginFolderName: string = `lifecycle.${options.pluginDefinition.pluginName}`; - // /.cache/. - this.cacheFolderPath = path.join(options.heftConfiguration.cacheFolderPath, uniquePluginFolderName); - // /temp/. this.tempFolderPath = path.join(options.heftConfiguration.tempFolderPath, uniquePluginFolderName); diff --git a/apps/heft/src/pluginFramework/HeftParameterManager.ts b/apps/heft/src/pluginFramework/HeftParameterManager.ts index 82934d7dcfb..edf79c00fd4 100644 --- a/apps/heft/src/pluginFramework/HeftParameterManager.ts +++ b/apps/heft/src/pluginFramework/HeftParameterManager.ts @@ -3,16 +3,16 @@ import { InternalError } from '@rushstack/node-core-library'; import { - CommandLineParameter, - CommandLineParameterProvider, + type CommandLineParameter, + type CommandLineParameterProvider, CommandLineParameterKind, - CommandLineChoiceParameter, - CommandLineChoiceListParameter, - CommandLineFlagParameter, - CommandLineIntegerParameter, - CommandLineIntegerListParameter, - CommandLineStringParameter, - CommandLineStringListParameter + type CommandLineChoiceParameter, + type CommandLineChoiceListParameter, + type CommandLineFlagParameter, + type CommandLineIntegerParameter, + type CommandLineIntegerListParameter, + type CommandLineStringParameter, + type CommandLineStringListParameter } from '@rushstack/ts-command-line'; import type { @@ -34,13 +34,6 @@ export interface IHeftDefaultParameters { */ readonly clean: boolean; - /** - * Whether or not the `--clean-cache` flag was passed to Heft. - * - * @public - */ - readonly cleanCache: boolean; - /** * Whether or not the `--debug` flag was passed to Heft. * @@ -133,7 +126,6 @@ export interface IHeftParameters extends IHeftDefaultParameters { export interface IHeftParameterManagerOptions { getIsClean: () => boolean; - getIsCleanCache: () => boolean; getIsDebug: () => boolean; getIsVerbose: () => boolean; getIsProduction: () => boolean; @@ -143,7 +135,7 @@ export interface IHeftParameterManagerOptions { export class HeftParameterManager { private readonly _options: IHeftParameterManagerOptions; - // plugin defintiion => parameter accessors and defaults + // plugin definition => parameter accessors and defaults private readonly _heftParametersByDefinition: Map = new Map(); // plugin definition => Map< parameter long name => applied parameter > private readonly _parametersByDefinition: Map> = @@ -162,7 +154,6 @@ export class HeftParameterManager { if (!this._defaultParameters) { this._defaultParameters = { clean: this._options.getIsClean(), - cleanCache: this._options.getIsCleanCache(), debug: this._options.getIsDebug(), verbose: this._options.getIsVerbose(), production: this._options.getIsProduction(), @@ -249,105 +240,125 @@ export class HeftParameterManager { /** * Add the parameters specified by a plugin definition to the command line parameter provider. * Duplicate parameters are allowed, as long as they have different parameter scopes. In this - * case, the parameter will only be referencable by the CLI argument + * case, the parameter will only be referenceable by the CLI argument * "--:". If there is no duplicate parameter, it will also be - * referencable by the CLI argument "--". + * referenceable by the CLI argument "--". */ private _addParametersToProvider( pluginDefinition: HeftPluginDefinitionBase, commandLineParameterProvider: CommandLineParameterProvider ): void { + const { + pluginName, + pluginPackageName, + pluginParameterScope: parameterScope, + pluginParameters + } = pluginDefinition; const existingDefinitionWithScope: HeftPluginDefinitionBase | undefined = - this._pluginDefinitionsByScope.get(pluginDefinition.pluginParameterScope); + this._pluginDefinitionsByScope.get(parameterScope); if (existingDefinitionWithScope && existingDefinitionWithScope !== pluginDefinition) { + const { pluginName: existingScopePluginName, pluginPackageName: existingScopePluginPackageName } = + existingDefinitionWithScope; throw new Error( - `Plugin ${JSON.stringify(pluginDefinition.pluginName)} in package ` + - `${JSON.stringify(pluginDefinition.pluginPackageName)} specifies the same parameter scope ` + - `${JSON.stringify(pluginDefinition.pluginParameterScope)} as plugin ` + - `${JSON.stringify(existingDefinitionWithScope.pluginName)} from package ` + - `${JSON.stringify(existingDefinitionWithScope.pluginPackageName)}.` + `Plugin ${JSON.stringify(pluginName)} in package ` + + `${JSON.stringify(pluginPackageName)} specifies the same parameter scope ` + + `${JSON.stringify(parameterScope)} as plugin ` + + `${JSON.stringify(existingScopePluginName)} from package ` + + `${JSON.stringify(existingScopePluginPackageName)}.` ); } else { - this._pluginDefinitionsByScope.set(pluginDefinition.pluginParameterScope, pluginDefinition); + this._pluginDefinitionsByScope.set(parameterScope, pluginDefinition); } const definedPluginParametersByName: Map = this._parametersByDefinition.get(pluginDefinition)!; - for (const parameter of pluginDefinition.pluginParameters) { - // Short names are excluded since it would be difficult and confusing to de-dupe/handle shortname - // conflicts as well as longname conflicts + for (const parameter of pluginParameters) { let definedParameter: CommandLineParameter; + const { description, required, longName: parameterLongName, shortName: parameterShortName } = parameter; switch (parameter.parameterKind) { case 'choiceList': { + const { alternatives } = parameter; definedParameter = commandLineParameterProvider.defineChoiceListParameter({ - description: parameter.description, - required: parameter.required, - alternatives: parameter.alternatives.map((p: IChoiceParameterAlternativeJson) => p.name), - parameterLongName: parameter.longName, - parameterScope: pluginDefinition.pluginParameterScope + description, + required, + alternatives: alternatives.map((p: IChoiceParameterAlternativeJson) => p.name), + parameterLongName, + parameterShortName, + parameterScope }); break; } case 'choice': { + const { alternatives, defaultValue } = parameter; definedParameter = commandLineParameterProvider.defineChoiceParameter({ - description: parameter.description, - required: parameter.required, - alternatives: parameter.alternatives.map((p: IChoiceParameterAlternativeJson) => p.name), - defaultValue: parameter.defaultValue, - parameterLongName: parameter.longName, - parameterScope: pluginDefinition.pluginParameterScope + description, + required, + alternatives: alternatives.map((p: IChoiceParameterAlternativeJson) => p.name), + defaultValue, + parameterLongName, + parameterShortName, + parameterScope }); break; } case 'flag': { definedParameter = commandLineParameterProvider.defineFlagParameter({ - description: parameter.description, - required: parameter.required, - parameterLongName: parameter.longName, - parameterScope: pluginDefinition.pluginParameterScope + description, + required, + parameterLongName, + parameterShortName, + parameterScope }); break; } case 'integerList': { + const { argumentName } = parameter; definedParameter = commandLineParameterProvider.defineIntegerListParameter({ - description: parameter.description, - required: parameter.required, - argumentName: parameter.argumentName, - parameterLongName: parameter.longName, - parameterScope: pluginDefinition.pluginParameterScope + description, + required, + argumentName, + parameterLongName, + parameterShortName, + parameterScope }); break; } case 'integer': { + const { argumentName, defaultValue } = parameter; definedParameter = commandLineParameterProvider.defineIntegerParameter({ - description: parameter.description, - required: parameter.required, - argumentName: parameter.argumentName, - defaultValue: parameter.defaultValue, - parameterLongName: parameter.longName, - parameterScope: pluginDefinition.pluginParameterScope + description, + required, + argumentName, + defaultValue, + parameterLongName, + parameterShortName, + parameterScope }); break; } case 'stringList': { + const { argumentName } = parameter; definedParameter = commandLineParameterProvider.defineStringListParameter({ - description: parameter.description, - required: parameter.required, - argumentName: parameter.argumentName, - parameterLongName: parameter.longName, - parameterScope: pluginDefinition.pluginParameterScope + description, + required, + argumentName, + parameterLongName, + parameterShortName, + parameterScope }); break; } case 'string': { + const { argumentName, defaultValue } = parameter; definedParameter = commandLineParameterProvider.defineStringParameter({ - description: parameter.description, - required: parameter.required, - argumentName: parameter.argumentName, - defaultValue: parameter.defaultValue, - parameterLongName: parameter.longName, - parameterScope: pluginDefinition.pluginParameterScope + description, + required, + argumentName, + defaultValue, + parameterLongName, + parameterShortName, + parameterScope }); break; } diff --git a/apps/heft/src/pluginFramework/HeftPhase.ts b/apps/heft/src/pluginFramework/HeftPhase.ts index 1a6de503ef9..e7ac8e65ca6 100644 --- a/apps/heft/src/pluginFramework/HeftPhase.ts +++ b/apps/heft/src/pluginFramework/HeftPhase.ts @@ -1,14 +1,30 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { HeftTask } from './HeftTask'; +import { HeftTask, type IHeftTask } from './HeftTask'; import type { InternalHeftSession } from './InternalHeftSession'; import type { IHeftConfigurationJsonPhaseSpecifier } from '../utilities/CoreConfigFiles'; import type { IDeleteOperation } from '../plugins/DeleteFilesPlugin'; const RESERVED_PHASE_NAMES: Set = new Set(['lifecycle']); -export class HeftPhase { +/** + * @public + */ +export interface IHeftPhase { + readonly phaseName: string; + readonly phaseDescription: string | undefined; + cleanFiles: ReadonlySet; + consumingPhases: ReadonlySet; + dependencyPhases: ReadonlySet; + tasks: ReadonlySet; + tasksByName: ReadonlyMap; +} + +/** + * @internal + */ +export class HeftPhase implements IHeftPhase { private _internalHeftSession: InternalHeftSession; private _phaseName: string; private _phaseSpecifier: IHeftConfigurationJsonPhaseSpecifier; diff --git a/apps/heft/src/pluginFramework/HeftPluginHost.ts b/apps/heft/src/pluginFramework/HeftPluginHost.ts index b13a48c86d9..9f3dad8edcc 100644 --- a/apps/heft/src/pluginFramework/HeftPluginHost.ts +++ b/apps/heft/src/pluginFramework/HeftPluginHost.ts @@ -2,7 +2,9 @@ // See LICENSE in the project root for license information. import { SyncHook } from 'tapable'; + import { InternalError } from '@rushstack/node-core-library'; +import type { ITerminal } from '@rushstack/terminal'; import type { HeftPluginDefinitionBase } from '../configuration/HeftPluginDefinition'; import type { IHeftPlugin } from './IHeftPlugin'; @@ -12,11 +14,12 @@ export abstract class HeftPluginHost { private readonly _pluginAccessRequestHooks: Map> = new Map(); private _pluginsApplied: boolean = false; - public async applyPluginsAsync(): Promise { + public async applyPluginsAsync(terminal: ITerminal): Promise { if (this._pluginsApplied) { // No need to apply them a second time. return; } + terminal.writeVerboseLine('Applying plugins'); await this.applyPluginsInternalAsync(); this._pluginsApplied = true; } diff --git a/apps/heft/src/pluginFramework/HeftTask.ts b/apps/heft/src/pluginFramework/HeftTask.ts index 3ee476411e4..d5f098bd1d8 100644 --- a/apps/heft/src/pluginFramework/HeftTask.ts +++ b/apps/heft/src/pluginFramework/HeftTask.ts @@ -4,87 +4,34 @@ import { InternalError } from '@rushstack/node-core-library'; import { HeftPluginConfiguration } from '../configuration/HeftPluginConfiguration'; -import { +import type { HeftTaskPluginDefinition, - type HeftPluginDefinitionBase + HeftPluginDefinitionBase } from '../configuration/HeftPluginDefinition'; -import type { HeftPhase } from './HeftPhase'; +import type { HeftPhase, IHeftPhase } from './HeftPhase'; import type { IHeftConfigurationJsonTaskSpecifier, IHeftConfigurationJsonPluginSpecifier } from '../utilities/CoreConfigFiles'; -import type { IHeftTaskPlugin } from '../pluginFramework/IHeftPlugin'; -import type { IScopedLogger } from '../pluginFramework/logging/ScopedLogger'; +import type { IHeftTaskPlugin } from './IHeftPlugin'; +import type { IScopedLogger } from './logging/ScopedLogger'; const RESERVED_TASK_NAMES: Set = new Set(['clean']); -let _copyFilesPluginDefinition: HeftTaskPluginDefinition | undefined; -function _getCopyFilesPluginDefinition(): HeftTaskPluginDefinition { - if (!_copyFilesPluginDefinition) { - _copyFilesPluginDefinition = HeftTaskPluginDefinition.loadFromObject({ - heftPluginDefinitionJson: { - pluginName: 'copy-files-plugin', - entryPoint: './lib/plugins/CopyFilesPlugin', - optionsSchema: './lib/schemas/copy-files-options.schema.json' - }, - packageRoot: `${__dirname}/../..`, - packageName: '@rushstack/heft' - }); - } - return _copyFilesPluginDefinition; -} - -let _deleteFilesPluginDefinition: HeftTaskPluginDefinition | undefined; -function _getDeleteFilesPluginDefinition(): HeftTaskPluginDefinition { - if (!_deleteFilesPluginDefinition) { - _deleteFilesPluginDefinition = HeftTaskPluginDefinition.loadFromObject({ - heftPluginDefinitionJson: { - pluginName: 'delete-files-plugin', - entryPoint: './lib/plugins/DeleteFilesPlugin', - optionsSchema: './lib/schemas/delete-files-options.schema.json' - }, - packageRoot: `${__dirname}/../..`, - packageName: '@rushstack/heft' - }); - } - return _deleteFilesPluginDefinition; -} - -let _runScriptPluginDefinition: HeftTaskPluginDefinition | undefined; -function _getRunScriptPluginDefinition(): HeftTaskPluginDefinition { - if (!_runScriptPluginDefinition) { - _runScriptPluginDefinition = HeftTaskPluginDefinition.loadFromObject({ - heftPluginDefinitionJson: { - pluginName: 'run-script-plugin', - entryPoint: './lib/plugins/RunScriptPlugin', - optionsSchema: './lib/schemas/run-script-options.schema.json' - }, - packageRoot: `${__dirname}/../..`, - packageName: '@rushstack/heft' - }); - } - return _runScriptPluginDefinition; -} - -let _nodeServicePluginDefinition: HeftTaskPluginDefinition | undefined; -function _getNodeServicePluginDefinition(): HeftTaskPluginDefinition { - if (!_nodeServicePluginDefinition) { - _nodeServicePluginDefinition = HeftTaskPluginDefinition.loadFromObject({ - heftPluginDefinitionJson: { - pluginName: 'node-service-plugin', - entryPoint: './lib/plugins/NodeServicePlugin' - }, - packageRoot: `${__dirname}/../..`, - packageName: '@rushstack/heft' - }); - } - return _nodeServicePluginDefinition; +/** + * @public + */ +export interface IHeftTask { + readonly parentPhase: IHeftPhase; + readonly taskName: string; + readonly consumingTasks: ReadonlySet; + readonly dependencyTasks: ReadonlySet; } /** * @internal */ -export class HeftTask { +export class HeftTask implements IHeftTask { private _parentPhase: HeftPhase; private _taskName: string; private _taskSpecifier: IHeftConfigurationJsonTaskSpecifier; @@ -132,7 +79,7 @@ export class HeftTask { } public get pluginOptions(): object | undefined { - return this._taskSpecifier.taskEvent?.options || this._taskSpecifier.taskPlugin?.options; + return this._taskSpecifier.taskPlugin.options; } public get dependencyTasks(): Set { @@ -170,7 +117,7 @@ export class HeftTask { public async ensureInitializedAsync(): Promise { if (!this._taskPluginDefinition) { - this._taskPluginDefinition = await this._loadTaskPluginDefintionAsync(); + this._taskPluginDefinition = await this._loadTaskPluginDefinitionAsync(); this.pluginDefinition.validateOptions(this.pluginOptions); } } @@ -183,58 +130,25 @@ export class HeftTask { return this._taskPlugin; } - private async _loadTaskPluginDefintionAsync(): Promise { - if (this._taskSpecifier.taskEvent && this._taskSpecifier.taskPlugin) { - // This is validated in the schema so this shouldn't happen, but throw just in case. + private async _loadTaskPluginDefinitionAsync(): Promise { + // taskPlugin.pluginPackage should already be resolved to the package root. + // See CoreConfigFiles.heftConfigFileLoader + const pluginSpecifier: IHeftConfigurationJsonPluginSpecifier = this._taskSpecifier.taskPlugin; + const pluginConfiguration: HeftPluginConfiguration = await HeftPluginConfiguration.loadFromPackageAsync( + pluginSpecifier.pluginPackageRoot, + pluginSpecifier.pluginPackage + ); + const pluginDefinition: HeftPluginDefinitionBase = + pluginConfiguration.getPluginDefinitionBySpecifier(pluginSpecifier); + + const isTaskPluginDefinition: boolean = pluginConfiguration.isTaskPluginDefinition(pluginDefinition); + if (!isTaskPluginDefinition) { throw new Error( - `Task ${JSON.stringify(this._taskName)} has both a taskEvent and a taskPlugin. ` + - `Only one of these can be specified.` - ); - } - - if (this._taskSpecifier.taskEvent) { - switch (this._taskSpecifier.taskEvent.eventKind) { - case 'copyFiles': { - return _getCopyFilesPluginDefinition(); - } - case 'deleteFiles': { - return _getDeleteFilesPluginDefinition(); - } - case 'runScript': { - return _getRunScriptPluginDefinition(); - } - case 'nodeService': { - return _getNodeServicePluginDefinition(); - } - default: { - throw new InternalError( - `Unknown task event kind ${JSON.stringify(this._taskSpecifier.taskEvent.eventKind)}` - ); - } - } - } else if (this._taskSpecifier.taskPlugin) { - // taskPlugin.pluginPackage should already be resolved to the package root. - // See CoreConfigFiles.heftConfigFileLoader - const pluginSpecifier: IHeftConfigurationJsonPluginSpecifier = this._taskSpecifier.taskPlugin; - const pluginConfiguration: HeftPluginConfiguration = await HeftPluginConfiguration.loadFromPackageAsync( - pluginSpecifier.pluginPackageRoot, - pluginSpecifier.pluginPackage - ); - const pluginDefinition: HeftPluginDefinitionBase = - pluginConfiguration.getPluginDefinitionBySpecifier(pluginSpecifier); - if (!pluginConfiguration.taskPluginDefinitions.has(pluginDefinition)) { - throw new Error( - `Plugin ${JSON.stringify(pluginSpecifier.pluginName)} specified by task ` + - `${JSON.stringify(this._taskName)} is not a task plugin.` - ); - } - return pluginDefinition as HeftTaskPluginDefinition; - } else { - // This is validated in the schema so this shouldn't happen, but throw just in case - throw new InternalError( - `Task ${JSON.stringify(this._taskName)} has no specified task event or task plugin.` + `Plugin ${JSON.stringify(pluginSpecifier.pluginName)} specified by task ` + + `${JSON.stringify(this._taskName)} is not a task plugin.` ); } + return pluginDefinition; } private _validate(): void { @@ -243,8 +157,8 @@ export class HeftTask { `Task name ${JSON.stringify(this.taskName)} is reserved and cannot be used as a task name.` ); } - if (!this._taskSpecifier.taskEvent && !this._taskSpecifier.taskPlugin) { - throw new Error(`Task ${JSON.stringify(this.taskName)} has no specified task event or task plugin.`); + if (!this._taskSpecifier.taskPlugin) { + throw new Error(`Task ${JSON.stringify(this.taskName)} has no specified task plugin.`); } } } diff --git a/apps/heft/src/pluginFramework/HeftTaskSession.ts b/apps/heft/src/pluginFramework/HeftTaskSession.ts index db7d2d59c9d..9898b11a492 100644 --- a/apps/heft/src/pluginFramework/HeftTaskSession.ts +++ b/apps/heft/src/pluginFramework/HeftTaskSession.ts @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; import { AsyncParallelHook, AsyncSeriesWaterfallHook } from 'tapable'; +import { InternalError } from '@rushstack/node-core-library'; + import type { MetricsCollector } from '../metrics/MetricsCollector'; import type { IScopedLogger } from './logging/ScopedLogger'; import type { HeftTask } from './HeftTask'; @@ -12,8 +13,51 @@ import type { HeftParameterManager, IHeftParameters } from './HeftParameterManag import type { IDeleteOperation } from '../plugins/DeleteFilesPlugin'; import type { ICopyOperation } from '../plugins/CopyFilesPlugin'; import type { HeftPluginHost } from './HeftPluginHost'; -import type { CancellationToken } from './CancellationToken'; -import { WatchGlobFn } from '../plugins/FileGlobSpecifier'; +import type { GlobFn, WatchGlobFn } from '../plugins/FileGlobSpecifier'; +import type { IWatchFileSystem } from '../utilities/WatchFileSystemAdapter'; + +/** + * The type of {@link IHeftTaskSession.parsedCommandLine}, which exposes details about the + * command line that was used to invoke Heft. + * @public + */ +export interface IHeftParsedCommandLine { + /** + * Returns the subcommand passed on the Heft command line, before any aliases have been expanded. + * This can be useful when printing error messages that need to refer to the invoked command line. + * + * @remarks + * For example, if the invoked command was `heft test --verbose`, then `commandName` + * would be `test`. + * + * Suppose the invoked command was `heft start` which is an alias for `heft build-watch --serve`. + * In this case, the `commandName` would be `start`. To get the expanded name `build-watch`, + * use {@link IHeftParsedCommandLine.unaliasedCommandName} instead. + * + * When invoking phases directly using `heft run`, the `commandName` is `run`. + * + * @see {@link IHeftParsedCommandLine.unaliasedCommandName} + */ + readonly commandName: string; + + /** + * Returns the subcommand passed on the Heft command line, after any aliases have been expanded. + * This can be useful when printing error messages that need to refer to the invoked command line. + * + * @remarks + * For example, if the invoked command was `heft test --verbose`, then `unaliasedCommandName` + * would be `test`. + * + * Suppose the invoked command was `heft start` which is an alias for `heft build-watch --serve`. + * In this case, the `unaliasedCommandName` would be `build-watch`. To get the alias name + * `start`, use @see {@link IHeftParsedCommandLine.commandName} instead. + * + * When invoking phases directly using `heft run`, the `unaliasedCommandName` is `run`. + * + * @see {@link IHeftParsedCommandLine.commandName} + */ + readonly unaliasedCommandName: string; +} /** * The task session is responsible for providing session-specific information to Heft task plugins. @@ -47,13 +91,10 @@ export interface IHeftTaskSession { readonly parameters: IHeftParameters; /** - * The cache folder for the task. This folder is unique for each task, and will not be - * cleaned when Heft is run with `--clean`. However, it will be cleaned when Heft is run - * with `--clean` and `--clean-cache`. - * - * @public + * Exposes details about the command line that was used to invoke Heft. + * This value is initially `undefined` and later filled in after the command line has been parsed. */ - readonly cacheFolderPath: string; + readonly parsedCommandLine: IHeftParsedCommandLine; /** * The temp folder for the task. This folder is unique for each task, and will be cleaned @@ -65,7 +106,7 @@ export interface IHeftTaskSession { /** * The scoped logger for the task. Messages logged with this logger will be prefixed with - * the phase and task name, in the format "[:]". It is highly recommended + * the phase and task name, in the format `[:]`. It is highly recommended * that writing to the console be performed via the logger, as it will ensure that logging messages * are labeled with the source of the message. * @@ -123,13 +164,17 @@ export interface IHeftTaskHooks { */ export interface IHeftTaskRunHookOptions { /** - * A cancellation token that is used to signal that the build is cancelled. This - * can be used to stop operations early and allow for a new build to - * be started. + * An abort signal that is used to abort the build. This can be used to stop operations early and allow + * for a new build to be started. * * @beta */ - readonly cancellationToken: CancellationToken; + readonly abortSignal: AbortSignal; + + /** + * Reads the specified globs and returns the result. + */ + readonly globAsync: GlobFn; } /** @@ -151,6 +196,12 @@ export interface IHeftTaskRunIncrementalHookOptions extends IHeftTaskRunHookOpti * If a change to the monitored files is detected, the task will be scheduled for re-execution. */ readonly watchGlobAsync: WatchGlobFn; + + /** + * Access to the file system view that powers `watchGlobAsync`. + * This is useful for plugins that do their own file system operations but still want to leverage Heft for watching. + */ + readonly watchFs: IWatchFileSystem; } /** @@ -184,12 +235,12 @@ export interface IHeftTaskSessionOptions extends IHeftPhaseSessionOptions { export class HeftTaskSession implements IHeftTaskSession { public readonly taskName: string; public readonly hooks: IHeftTaskHooks; - public readonly cacheFolderPath: string; public readonly tempFolderPath: string; public readonly logger: IScopedLogger; private readonly _options: IHeftTaskSessionOptions; private _parameters: IHeftParameters | undefined; + private _parsedCommandLine: IHeftParsedCommandLine; /** * @internal @@ -206,10 +257,14 @@ export class HeftTaskSession implements IHeftTaskSession { return this._parameters; } + public get parsedCommandLine(): IHeftParsedCommandLine { + return this._parsedCommandLine; + } + public constructor(options: IHeftTaskSessionOptions) { const { internalHeftSession: { - heftConfiguration: { cacheFolderPath: cacheFolder, tempFolderPath: tempFolder }, + heftConfiguration: { tempFolderPath: tempFolder }, loggingManager, metricsCollector }, @@ -217,6 +272,12 @@ export class HeftTaskSession implements IHeftTaskSession { task } = options; + if (!options.internalHeftSession.parsedCommandLine) { + // This should not happen + throw new InternalError('Attempt to construct HeftTaskSession before command line has been parsed'); + } + this._parsedCommandLine = options.internalHeftSession.parsedCommandLine; + this.logger = loggingManager.requestScopedLogger(`${phase.phaseName}:${task.taskName}`); this.metricsCollector = metricsCollector; this.taskName = task.taskName; @@ -226,18 +287,16 @@ export class HeftTaskSession implements IHeftTaskSession { registerFileOperations: new AsyncSeriesWaterfallHook(['fileOperations']) }; - // Guranteed to be unique since phases are uniquely named, tasks are uniquely named within - // phases, and neither can have '.' in their names. We will also use the phase name and + // Guaranteed to be unique since phases are uniquely named, tasks are uniquely named within + // phases, and neither can have '/' in their names. We will also use the phase name and // task name as the folder name (instead of the plugin name) since we want to enable re-use // of plugins in multiple phases and tasks while maintaining unique temp/cache folders for // each task. - const uniqueTaskFolderName: string = `${phase.phaseName}.${task.taskName}`; - - // /.cache/. - this.cacheFolderPath = path.join(cacheFolder, uniqueTaskFolderName); + // Having a parent folder for the phase simplifies interaction with the Rush build cache. + const uniqueTaskFolderName: string = `${phase.phaseName}/${task.taskName}`; - // /temp/. - this.tempFolderPath = path.join(tempFolder, uniqueTaskFolderName); + // /temp// + this.tempFolderPath = `${tempFolder}/${uniqueTaskFolderName}`; this._options = options; } diff --git a/apps/heft/src/pluginFramework/IncrementalBuildInfo.ts b/apps/heft/src/pluginFramework/IncrementalBuildInfo.ts new file mode 100644 index 00000000000..3c30d07cec5 --- /dev/null +++ b/apps/heft/src/pluginFramework/IncrementalBuildInfo.ts @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as path from 'node:path'; + +import { FileSystem, Path } from '@rushstack/node-core-library'; + +/** + * Information about an incremental build. This information is used to determine which files need to be rebuilt. + * @beta + */ +export interface IIncrementalBuildInfo { + /** + * A string that represents the configuration inputs for the build. + * If the configuration changes, the old build info object should be discarded. + */ + configHash: string; + + /** + * A map of absolute input file paths to their version strings. + * The version string should change if the file changes. + */ + inputFileVersions: Map; + + /** + * A map of absolute output file paths to the input files they were computed from. + */ + fileDependencies?: Map; +} + +/** + * Serialized version of {@link IIncrementalBuildInfo}. + * @beta + */ +export interface ISerializedIncrementalBuildInfo { + /** + * A string that represents the configuration inputs for the build. + * If the configuration changes, the old build info object should be discarded. + */ + configHash: string; + + /** + * A map of input files to their version strings. + * File paths are specified relative to the folder containing the build info file. + */ + inputFileVersions: Record; + + /** + * Map of output file names to the corresponding index in `Object.entries(inputFileVersions)`. + * File paths are specified relative to the folder containing the build info file. + */ + fileDependencies?: Record; +} + +/** + * Converts an absolute path to a path relative to a base path. + */ +export const makePathRelative: (absolutePath: string, basePath: string) => string = + process.platform === 'win32' + ? (absolutePath: string, basePath: string) => { + // On Windows, need to normalize slashes + return Path.convertToSlashes(path.win32.relative(basePath, absolutePath)); + } + : (absolutePath: string, basePath: string) => { + // On POSIX, can preserve existing slashes + return path.posix.relative(basePath, absolutePath); + }; + +/** + * Serializes a build info object to a portable format that can be written to disk. + * @param state - The build info to serialize + * @param makePathPortable - A function that converts an absolute path to a portable path. This is a separate argument to support cross-platform tests. + * @returns The serialized build info + * @beta + */ +export function serializeBuildInfo( + state: IIncrementalBuildInfo, + makePathPortable: (absolutePath: string) => string +): ISerializedIncrementalBuildInfo { + const fileIndices: Map = new Map(); + const inputFileVersions: Record = {}; + + for (const [absolutePath, version] of state.inputFileVersions) { + const relativePath: string = makePathPortable(absolutePath); + fileIndices.set(absolutePath, fileIndices.size); + inputFileVersions[relativePath] = version; + } + + const { fileDependencies: newFileDependencies } = state; + let fileDependencies: Record | undefined; + if (newFileDependencies) { + fileDependencies = {}; + for (const [absolutePath, dependencies] of newFileDependencies) { + const relativePath: string = makePathPortable(absolutePath); + const indices: number[] = []; + for (const dependency of dependencies) { + const index: number | undefined = fileIndices.get(dependency); + if (index === undefined) { + throw new Error(`Dependency not found: ${dependency}`); + } + indices.push(index); + } + + fileDependencies[relativePath] = indices; + } + } + + const serializedBuildInfo: ISerializedIncrementalBuildInfo = { + configHash: state.configHash, + inputFileVersions, + fileDependencies + }; + + return serializedBuildInfo; +} + +/** + * Deserializes a build info object from its portable format. + * @param serializedBuildInfo - The build info to deserialize + * @param makePathAbsolute - A function that converts a portable path to an absolute path. This is a separate argument to support cross-platform tests. + * @returns The deserialized build info + */ +export function deserializeBuildInfo( + serializedBuildInfo: ISerializedIncrementalBuildInfo, + makePathAbsolute: (relativePath: string) => string +): IIncrementalBuildInfo { + const inputFileVersions: Map = new Map(); + const absolutePathByIndex: string[] = []; + for (const [relativePath, version] of Object.entries(serializedBuildInfo.inputFileVersions)) { + const absolutePath: string = makePathAbsolute(relativePath); + absolutePathByIndex.push(absolutePath); + inputFileVersions.set(absolutePath, version); + } + + let fileDependencies: Map | undefined; + const { fileDependencies: serializedFileDependencies } = serializedBuildInfo; + if (serializedFileDependencies) { + fileDependencies = new Map(); + for (const [relativeOutputFile, indices] of Object.entries(serializedFileDependencies)) { + const absoluteOutputFile: string = makePathAbsolute(relativeOutputFile); + const dependencies: string[] = []; + for (const index of Array.isArray(indices) ? indices : [indices]) { + const dependencyAbsolutePath: string | undefined = absolutePathByIndex[index]; + if (dependencyAbsolutePath === undefined) { + throw new Error(`Dependency index not found: ${index}`); + } + dependencies.push(dependencyAbsolutePath); + } + fileDependencies.set(absoluteOutputFile, dependencies); + } + } + + const buildInfo: IIncrementalBuildInfo = { + configHash: serializedBuildInfo.configHash, + inputFileVersions, + fileDependencies + }; + + return buildInfo; +} + +/** + * Writes a build info object to disk. + * @param state - The build info to write + * @param filePath - The file path to write the build info to + * @beta + */ +export async function writeBuildInfoAsync(state: IIncrementalBuildInfo, filePath: string): Promise { + const basePath: string = path.dirname(filePath); + + const serializedBuildInfo: ISerializedIncrementalBuildInfo = serializeBuildInfo( + state, + (absolutePath: string) => { + return makePathRelative(absolutePath, basePath); + } + ); + + // This file is meant only for machine reading, so don't pretty-print it. + const stringified: string = JSON.stringify(serializedBuildInfo); + + await FileSystem.writeFileAsync(filePath, stringified, { ensureFolderExists: true }); +} + +/** + * Reads a build info object from disk. + * @param filePath - The file path to read the build info from + * @returns The build info object, or undefined if the file does not exist or cannot be parsed + * @beta + */ +export async function tryReadBuildInfoAsync(filePath: string): Promise { + let serializedBuildInfo: ISerializedIncrementalBuildInfo | undefined; + try { + const fileContents: string = await FileSystem.readFileAsync(filePath); + serializedBuildInfo = JSON.parse(fileContents) as ISerializedIncrementalBuildInfo; + } catch (error) { + if (FileSystem.isNotExistError(error)) { + return; + } + throw error; + } + + const basePath: string = path.dirname(filePath); + + const buildInfo: IIncrementalBuildInfo = deserializeBuildInfo( + serializedBuildInfo, + (relativePath: string) => { + return path.resolve(basePath, relativePath); + } + ); + + return buildInfo; +} diff --git a/apps/heft/src/pluginFramework/InternalHeftSession.ts b/apps/heft/src/pluginFramework/InternalHeftSession.ts index b84f63f9cdd..93905e3e94f 100644 --- a/apps/heft/src/pluginFramework/InternalHeftSession.ts +++ b/apps/heft/src/pluginFramework/InternalHeftSession.ts @@ -7,26 +7,28 @@ import { Constants } from '../utilities/Constants'; import { HeftLifecycle } from './HeftLifecycle'; import { HeftPhaseSession } from './HeftPhaseSession'; import { HeftPhase } from './HeftPhase'; -import { CoreConfigFiles, type IHeftConfigurationJson } from '../utilities/CoreConfigFiles'; +import { + CoreConfigFiles, + type IHeftConfigurationJson, + type IHeftConfigurationJsonActionReference +} from '../utilities/CoreConfigFiles'; import type { MetricsCollector } from '../metrics/MetricsCollector'; import type { LoggingManager } from './logging/LoggingManager'; import type { HeftConfiguration } from '../configuration/HeftConfiguration'; +import type { HeftPluginDefinitionBase } from '../configuration/HeftPluginDefinition'; import type { HeftTask } from './HeftTask'; import type { HeftParameterManager } from './HeftParameterManager'; +import type { IHeftParsedCommandLine } from './HeftTaskSession'; export interface IInternalHeftSessionOptions { heftConfiguration: HeftConfiguration; loggingManager: LoggingManager; metricsCollector: MetricsCollector; - debug: boolean; -} -export interface IHeftSessionWatchOptions { - ignoredSourceFileGlobs: readonly string[]; - forbiddenSourceFileGlobs: readonly string[]; + debug: boolean; } -function* getAllTasks(phases: Iterable): Iterable { +function* getAllTasks(phases: Iterable): IterableIterator { for (const phase of phases) { yield* phase.tasks; } @@ -35,11 +37,11 @@ function* getAllTasks(phases: Iterable): Iterable { export class InternalHeftSession { private readonly _phaseSessionsByPhase: Map = new Map(); private readonly _heftConfigurationJson: IHeftConfigurationJson; + private _actionReferencesByAlias: ReadonlyMap | undefined; private _lifecycle: HeftLifecycle | undefined; private _phases: Set | undefined; private _phasesByName: Map | undefined; private _parameterManager: HeftParameterManager | undefined; - private _watchOptions: IHeftSessionWatchOptions | undefined; public readonly heftConfiguration: HeftConfiguration; @@ -47,6 +49,8 @@ export class InternalHeftSession { public readonly metricsCollector: MetricsCollector; + public parsedCommandLine: IHeftParsedCommandLine | undefined; + public readonly debug: boolean; private constructor(heftConfigurationJson: IHeftConfigurationJson, options: IInternalHeftSessionOptions) { @@ -83,6 +87,33 @@ export class InternalHeftSession { { concurrency: Constants.maxParallelism } ); + function* getAllPluginDefinitions(): IterableIterator { + yield* internalHeftSession.lifecycle.pluginDefinitions; + for (const task of getAllTasks(internalHeftSession.phases)) { + yield task.pluginDefinition; + } + } + + const loadedPluginPathsByName: Map> = new Map(); + for (const { pluginName, entryPoint } of getAllPluginDefinitions()) { + let existingPluginPaths: Set | undefined = loadedPluginPathsByName.get(pluginName); + if (!existingPluginPaths) { + existingPluginPaths = new Set(); + loadedPluginPathsByName.set(pluginName, existingPluginPaths); + } + + existingPluginPaths.add(entryPoint); + } + + for (const [pluginName, pluginPaths] of loadedPluginPathsByName) { + if (pluginPaths.size > 1) { + throw new Error( + `Multiple plugins named ${JSON.stringify(pluginName)} were loaded from different paths: ` + + `${Array.from(pluginPaths, (x) => JSON.stringify(x)).join(', ')}. Plugins must have unique names.` + ); + } + } + return internalHeftSession; } @@ -97,6 +128,15 @@ export class InternalHeftSession { this._parameterManager = value; } + public get actionReferencesByAlias(): ReadonlyMap { + if (!this._actionReferencesByAlias) { + this._actionReferencesByAlias = new Map( + Object.entries(this._heftConfigurationJson.aliasesByName || {}) + ); + } + return this._actionReferencesByAlias; + } + public get lifecycle(): HeftLifecycle { if (!this._lifecycle) { this._lifecycle = new HeftLifecycle(this, this._heftConfigurationJson.heftPlugins || []); diff --git a/apps/heft/src/pluginFramework/StaticFileSystemAdapter.ts b/apps/heft/src/pluginFramework/StaticFileSystemAdapter.ts index d4146d0104b..89129078af2 100644 --- a/apps/heft/src/pluginFramework/StaticFileSystemAdapter.ts +++ b/apps/heft/src/pluginFramework/StaticFileSystemAdapter.ts @@ -1,8 +1,11 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import type { ReaddirAsynchronousMethod, ReaddirSynchronousMethod } from '@nodelib/fs.scandir'; -import type { StatAsynchronousMethod, StatSynchronousMethod } from '@nodelib/fs.stat'; +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type * as fs from 'node:fs'; +import * as path from 'node:path'; + import type { FileSystemAdapter } from 'fast-glob'; + import { Path } from '@rushstack/node-core-library'; interface IVirtualFileSystemEntry { @@ -35,7 +38,7 @@ export class StaticFileSystemAdapter implements FileSystemAdapter { private _directoryMap: Map = new Map(); /** { @inheritdoc fs.lstat } */ - public lstat: StatAsynchronousMethod = ((filePath: string, callback: StatCallback) => { + public lstat: FileSystemAdapter['lstat'] = ((filePath: string, callback: StatCallback) => { process.nextTick(() => { let result: fs.Stats; try { @@ -44,13 +47,13 @@ export class StaticFileSystemAdapter implements FileSystemAdapter { callback(e, {} as fs.Stats); return; } - // eslint-disable-next-line @rushstack/no-new-null + callback(null, result); }); - }) as StatAsynchronousMethod; + }) as FileSystemAdapter['lstat']; /** { @inheritdoc fs.lstatSync } */ - public lstatSync: StatSynchronousMethod = ((filePath: string) => { + public lstatSync: FileSystemAdapter['lstatSync'] = ((filePath: string) => { filePath = this._normalizePath(filePath); const entry: IVirtualFileSystemEntry | undefined = this._directoryMap.get(filePath); if (!entry) { @@ -71,20 +74,20 @@ export class StaticFileSystemAdapter implements FileSystemAdapter { isFIFO: () => false, isSocket: () => false }; - }) as StatSynchronousMethod; + }) as FileSystemAdapter['lstatSync']; /** { @inheritdoc fs.stat } */ - public stat: StatAsynchronousMethod = ((filePath: string, callback: StatCallback) => { + public stat: FileSystemAdapter['stat'] = ((filePath: string, callback: StatCallback) => { this.lstat(filePath, callback); - }) as StatAsynchronousMethod; + }) as FileSystemAdapter['stat']; /** { @inheritdoc fs.statSync } */ - public statSync: StatSynchronousMethod = ((filePath: string) => { + public statSync: FileSystemAdapter['statSync'] = ((filePath: string) => { return this.lstatSync(filePath); - }) as StatSynchronousMethod; + }) as FileSystemAdapter['statSync']; /** { @inheritdoc fs.readdir } */ - public readdir: ReaddirAsynchronousMethod = (( + public readdir: FileSystemAdapter['readdir'] = (( filePath: string, optionsOrCallback: IReaddirOptions | ReaddirStringCallback, callback?: ReaddirDirentCallback | ReaddirStringCallback @@ -102,7 +105,7 @@ export class StaticFileSystemAdapter implements FileSystemAdapter { let result: fs.Dirent[] | string[]; try { if (options?.withFileTypes) { - result = this.readdirSync(filePath, options); + result = this.readdirSync(filePath, options) as fs.Dirent[]; } else { result = this.readdirSync(filePath); } @@ -114,17 +117,15 @@ export class StaticFileSystemAdapter implements FileSystemAdapter { // When "withFileTypes" is false or undefined, the callback is expected to return a string array. // Otherwise, we return a fs.Dirent array. if (options?.withFileTypes) { - // eslint-disable-next-line @rushstack/no-new-null (callback as ReaddirDirentCallback)(null, result as fs.Dirent[]); } else { - // eslint-disable-next-line @rushstack/no-new-null (callback as ReaddirStringCallback)(null, result as string[]); } }); - }) as ReaddirAsynchronousMethod; + }) as FileSystemAdapter['readdir']; /** { @inheritdoc fs.readdirSync } */ - public readdirSync: ReaddirSynchronousMethod = ((filePath: string, options?: IReaddirOptions) => { + public readdirSync: FileSystemAdapter['readdirSync'] = ((filePath: string, options?: IReaddirOptions) => { filePath = this._normalizePath(filePath); const virtualDirectory: IVirtualFileSystemEntry | undefined = this._directoryMap.get(filePath); if (!virtualDirectory) { @@ -167,7 +168,7 @@ export class StaticFileSystemAdapter implements FileSystemAdapter { } else { return result.map((entry: IVirtualFileSystemEntry) => entry.name); } - }) as ReaddirSynchronousMethod; + }) as FileSystemAdapter['readdirSync']; /** * Create a new StaticFileSystemAdapter instance with the provided file paths. diff --git a/apps/heft/src/pluginFramework/logging/LoggingManager.ts b/apps/heft/src/pluginFramework/logging/LoggingManager.ts index 06f970c300b..36c243a8084 100644 --- a/apps/heft/src/pluginFramework/logging/LoggingManager.ts +++ b/apps/heft/src/pluginFramework/logging/LoggingManager.ts @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { ScopedLogger } from './ScopedLogger'; import { FileError, - FileLocationStyle, - ITerminalProvider, - IFileErrorFormattingOptions + type FileLocationStyle, + type IFileErrorFormattingOptions } from '@rushstack/node-core-library'; +import type { ITerminalProvider } from '@rushstack/terminal'; +import { ScopedLogger } from './ScopedLogger'; export interface ILoggingManagerOptions { terminalProvider: ITerminalProvider; } @@ -17,12 +17,17 @@ export class LoggingManager { private _options: ILoggingManagerOptions; private _scopedLoggers: Map = new Map(); private _shouldPrintStacks: boolean = false; + private _hasAnyWarnings: boolean = false; private _hasAnyErrors: boolean = false; public get errorsHaveBeenEmitted(): boolean { return this._hasAnyErrors; } + public get warningsHaveBeenEmitted(): boolean { + return this._hasAnyWarnings; + } + public constructor(options: ILoggingManagerOptions) { this._options = options; } @@ -33,6 +38,7 @@ export class LoggingManager { public resetScopedLoggerErrorsAndWarnings(): void { this._hasAnyErrors = false; + this._hasAnyWarnings = false; for (const scopedLogger of this._scopedLoggers.values()) { scopedLogger.resetErrorsAndWarnings(); } @@ -47,7 +53,8 @@ export class LoggingManager { loggerName, terminalProvider: this._options.terminalProvider, getShouldPrintStacks: () => this._shouldPrintStacks, - errorHasBeenEmittedCallback: () => (this._hasAnyErrors = true) + errorHasBeenEmittedCallback: () => (this._hasAnyErrors = true), + warningHasBeenEmittedCallback: () => (this._hasAnyWarnings = true) }); this._scopedLoggers.set(loggerName, scopedLogger); return scopedLogger; diff --git a/apps/heft/src/pluginFramework/logging/MockScopedLogger.ts b/apps/heft/src/pluginFramework/logging/MockScopedLogger.ts new file mode 100644 index 00000000000..1ffe5ec6c3d --- /dev/null +++ b/apps/heft/src/pluginFramework/logging/MockScopedLogger.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { ITerminal } from '@rushstack/terminal'; + +import type { IScopedLogger } from './ScopedLogger'; + +/** + * Implementation of IScopedLogger for use by unit tests. + * + * @internal + */ +export class MockScopedLogger implements IScopedLogger { + public errors: Error[] = []; + public warnings: Error[] = []; + + public loggerName: string = 'mockLogger'; + + public terminal: ITerminal; + + public constructor(terminal: ITerminal) { + this.terminal = terminal; + } + public get hasErrors(): boolean { + return this.errors.length > 0; + } + + public emitError(error: Error): void { + this.errors.push(error); + } + public emitWarning(warning: Error): void { + this.warnings.push(warning); + } + + public resetErrorsAndWarnings(): void { + this.errors.length = 0; + this.warnings.length = 0; + } +} diff --git a/apps/heft/src/pluginFramework/logging/ScopedLogger.ts b/apps/heft/src/pluginFramework/logging/ScopedLogger.ts index a67e4e51c55..357289ecc5d 100644 --- a/apps/heft/src/pluginFramework/logging/ScopedLogger.ts +++ b/apps/heft/src/pluginFramework/logging/ScopedLogger.ts @@ -6,7 +6,7 @@ import { Terminal, type ITerminalProvider, type ITerminal -} from '@rushstack/node-core-library'; +} from '@rushstack/terminal'; import { LoggingManager } from './LoggingManager'; @@ -53,6 +53,7 @@ export interface IScopedLoggerOptions { terminalProvider: ITerminalProvider; getShouldPrintStacks: () => boolean; errorHasBeenEmittedCallback: () => void; + warningHasBeenEmittedCallback: () => void; } export class ScopedLogger implements IScopedLogger { @@ -104,6 +105,7 @@ export class ScopedLogger implements IScopedLogger { * {@inheritdoc IScopedLogger.emitError} */ public emitError(error: Error): void { + this._options.errorHasBeenEmittedCallback(); this._errors.push(error); this.terminal.writeErrorLine(`Error: ${LoggingManager.getErrorMessage(error)}`); if (this._shouldPrintStacks && error.stack) { @@ -115,6 +117,7 @@ export class ScopedLogger implements IScopedLogger { * {@inheritdoc IScopedLogger.emitWarning} */ public emitWarning(warning: Error): void { + this._options.warningHasBeenEmittedCallback(); this._warnings.push(warning); this.terminal.writeWarningLine(`Warning: ${LoggingManager.getErrorMessage(warning)}`); if (this._shouldPrintStacks && warning.stack) { diff --git a/apps/heft/src/pluginFramework/tests/IncrementalBuildInfo.test.ts b/apps/heft/src/pluginFramework/tests/IncrementalBuildInfo.test.ts new file mode 100644 index 00000000000..0a5d3be7f65 --- /dev/null +++ b/apps/heft/src/pluginFramework/tests/IncrementalBuildInfo.test.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import path from 'node:path'; + +import { Path } from '@rushstack/node-core-library'; + +import { + serializeBuildInfo, + deserializeBuildInfo, + type IIncrementalBuildInfo, + type ISerializedIncrementalBuildInfo +} from '../IncrementalBuildInfo'; + +const posixBuildInfo: IIncrementalBuildInfo = { + configHash: 'foobar', + inputFileVersions: new Map([ + ['/a/b/c/file1', '1'], + ['/a/b/c/file2', '2'] + ]), + fileDependencies: new Map([ + ['/a/b/c/output1', ['/a/b/c/file1']], + ['/a/b/c/output2', ['/a/b/c/file1', '/a/b/c/file2']] + ]) +}; + +const win32BuildInfo: IIncrementalBuildInfo = { + configHash: 'foobar', + inputFileVersions: new Map([ + ['A:\\b\\c\\file1', '1'], + ['A:\\b\\c\\file2', '2'] + ]), + fileDependencies: new Map([ + ['A:\\b\\c\\output1', ['A:\\b\\c\\file1']], + ['A:\\b\\c\\output2', ['A:\\b\\c\\file1', 'A:\\b\\c\\file2']] + ]) +}; + +const posixBasePath: string = '/a/b/temp'; +const win32BasePath: string = 'A:\\b\\temp'; + +function posixToPortable(absolutePath: string): string { + return path.posix.relative(posixBasePath, absolutePath); +} +function portableToPosix(portablePath: string): string { + return path.posix.resolve(posixBasePath, portablePath); +} + +function win32ToPortable(absolutePath: string): string { + return Path.convertToSlashes(path.win32.relative(win32BasePath, absolutePath)); +} +function portableToWin32(portablePath: string): string { + return path.win32.resolve(win32BasePath, portablePath); +} + +describe(serializeBuildInfo.name, () => { + it('Round trips correctly (POSIX)', () => { + const serialized: ISerializedIncrementalBuildInfo = serializeBuildInfo(posixBuildInfo, posixToPortable); + + const deserialized: IIncrementalBuildInfo = deserializeBuildInfo(serialized, portableToPosix); + + expect(deserialized).toEqual(posixBuildInfo); + }); + + it('Round trips correctly (Win32)', () => { + const serialized: ISerializedIncrementalBuildInfo = serializeBuildInfo(win32BuildInfo, win32ToPortable); + + const deserialized: IIncrementalBuildInfo = deserializeBuildInfo(serialized, portableToWin32); + + expect(deserialized).toEqual(win32BuildInfo); + }); + + it('Converts (POSIX to Win32)', () => { + const serialized: ISerializedIncrementalBuildInfo = serializeBuildInfo(posixBuildInfo, posixToPortable); + + const deserialized: IIncrementalBuildInfo = deserializeBuildInfo(serialized, portableToWin32); + + expect(deserialized).toEqual(win32BuildInfo); + }); + + it('Converts (Win32 to POSIX)', () => { + const serialized: ISerializedIncrementalBuildInfo = serializeBuildInfo(win32BuildInfo, win32ToPortable); + + const deserialized: IIncrementalBuildInfo = deserializeBuildInfo(serialized, portableToPosix); + + expect(deserialized).toEqual(posixBuildInfo); + }); + + it('Has expected serialized format', () => { + const serializedPosix: ISerializedIncrementalBuildInfo = serializeBuildInfo( + posixBuildInfo, + posixToPortable + ); + const serializedWin32: ISerializedIncrementalBuildInfo = serializeBuildInfo( + win32BuildInfo, + win32ToPortable + ); + + expect(serializedPosix).toMatchSnapshot('posix'); + expect(serializedWin32).toMatchSnapshot('win32'); + + expect(serializedPosix).toEqual(serializedWin32); + }); +}); diff --git a/apps/heft/src/pluginFramework/tests/__snapshots__/IncrementalBuildInfo.test.ts.snap b/apps/heft/src/pluginFramework/tests/__snapshots__/IncrementalBuildInfo.test.ts.snap new file mode 100644 index 00000000000..390725566a9 --- /dev/null +++ b/apps/heft/src/pluginFramework/tests/__snapshots__/IncrementalBuildInfo.test.ts.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`serializeBuildInfo Has expected serialized format: posix 1`] = ` +Object { + "configHash": "foobar", + "fileDependencies": Object { + "../c/output1": Array [ + 0, + ], + "../c/output2": Array [ + 0, + 1, + ], + }, + "inputFileVersions": Object { + "../c/file1": "1", + "../c/file2": "2", + }, +} +`; + +exports[`serializeBuildInfo Has expected serialized format: win32 1`] = ` +Object { + "configHash": "foobar", + "fileDependencies": Object { + "../c/output1": Array [ + 0, + ], + "../c/output2": Array [ + 0, + 1, + ], + }, + "inputFileVersions": Object { + "../c/file1": "1", + "../c/file2": "2", + }, +} +`; diff --git a/apps/heft/src/plugins/CopyFilesPlugin.ts b/apps/heft/src/plugins/CopyFilesPlugin.ts index 7efa77ac58b..2c9d47347a1 100644 --- a/apps/heft/src/plugins/CopyFilesPlugin.ts +++ b/apps/heft/src/plugins/CopyFilesPlugin.ts @@ -1,15 +1,29 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; -import { AlreadyExistsBehavior, FileSystem, Async, ITerminal } from '@rushstack/node-core-library'; +import { createHash } from 'node:crypto'; +import type * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { AlreadyExistsBehavior, FileSystem, Async } from '@rushstack/node-core-library'; +import type { ITerminal } from '@rushstack/terminal'; import { Constants } from '../utilities/Constants'; -import { getFilePathsAsync, type IFileSelectionSpecifier } from './FileGlobSpecifier'; +import { + asAbsoluteFileSelectionSpecifier, + getFileSelectionSpecifierPathsAsync, + type IFileSelectionSpecifier +} from './FileGlobSpecifier'; import type { HeftConfiguration } from '../configuration/HeftConfiguration'; import type { IHeftTaskPlugin } from '../pluginFramework/IHeftPlugin'; import type { IHeftTaskSession, IHeftTaskFileOperations } from '../pluginFramework/HeftTaskSession'; -import { WatchFileSystemAdapter } from '../utilities/WatchFileSystemAdapter'; +import type { WatchFileSystemAdapter } from '../utilities/WatchFileSystemAdapter'; +import { + type IIncrementalBuildInfo, + makePathRelative, + tryReadBuildInfoAsync, + writeBuildInfoAsync +} from '../pluginFramework/IncrementalBuildInfo'; /** * Used to specify a selection of files to copy from a specific source folder to one @@ -66,9 +80,38 @@ interface ICopyDescriptor { hardlink: boolean; } +export function asAbsoluteCopyOperation( + rootFolderPath: string, + copyOperation: ICopyOperation +): ICopyOperation { + const absoluteCopyOperation: ICopyOperation = asAbsoluteFileSelectionSpecifier( + rootFolderPath, + copyOperation + ); + absoluteCopyOperation.destinationFolders = copyOperation.destinationFolders.map((folder) => + path.resolve(rootFolderPath, folder) + ); + return absoluteCopyOperation; +} + +export function asRelativeCopyOperation( + rootFolderPath: string, + copyOperation: ICopyOperation +): ICopyOperation { + return { + ...copyOperation, + destinationFolders: copyOperation.destinationFolders.map((folder) => + makePathRelative(folder, rootFolderPath) + ), + sourcePath: copyOperation.sourcePath && makePathRelative(copyOperation.sourcePath, rootFolderPath) + }; +} + export async function copyFilesAsync( copyOperations: Iterable, terminal: ITerminal, + buildInfoPath: string, + configHash: string, watchFileSystemAdapter?: WatchFileSystemAdapter ): Promise { const copyDescriptorByDestination: Map = await _getCopyDescriptorsAsync( @@ -76,12 +119,12 @@ export async function copyFilesAsync( watchFileSystemAdapter ); - await _copyFilesInnerAsync(copyDescriptorByDestination, terminal); + await _copyFilesInnerAsync(copyDescriptorByDestination, configHash, buildInfoPath, terminal); } async function _getCopyDescriptorsAsync( copyConfigurations: Iterable, - fs: WatchFileSystemAdapter | undefined + fileSystemAdapter: WatchFileSystemAdapter | undefined ): Promise> { // Create a map to deduplicate and prevent double-writes // resolvedDestinationFilePath -> descriptor @@ -91,19 +134,23 @@ async function _getCopyDescriptorsAsync( copyConfigurations, async (copyConfiguration: ICopyOperation) => { // "sourcePath" is required to be a folder. To copy a single file, put the parent folder in "sourcePath" - // and the filename in "includeGlobs" - const sourceFolder: string | undefined = copyConfiguration.sourcePath; - const sourceFilePaths: Set | undefined = await getFilePathsAsync(copyConfiguration, fs); + // and the filename in "includeGlobs". + const sourceFolder: string = copyConfiguration.sourcePath!; + const sourceFiles: Map = await getFileSelectionSpecifierPathsAsync({ + fileGlobSpecifier: copyConfiguration, + fileSystemAdapter + }); // Dedupe and throw if a double-write is detected for (const destinationFolderPath of copyConfiguration.destinationFolders) { - for (const sourceFilePath of sourceFilePaths!) { + // We only need to care about the keys of the map since we know all the keys are paths to files + for (const sourceFilePath of sourceFiles.keys()) { // Only include the relative path from the sourceFolder if flatten is false const resolvedDestinationPath: string = path.resolve( destinationFolderPath, copyConfiguration.flatten ? path.basename(sourceFilePath) - : path.relative(sourceFolder!, sourceFilePath) + : path.relative(sourceFolder, sourceFilePath) ); // Throw if a duplicate copy target with a different source or options is specified @@ -141,16 +188,71 @@ async function _getCopyDescriptorsAsync( async function _copyFilesInnerAsync( copyDescriptors: Map, + configHash: string, + buildInfoPath: string, terminal: ITerminal ): Promise { if (copyDescriptors.size === 0) { return; } - let copiedFolderOrFileCount: number = 0; + let oldBuildInfo: IIncrementalBuildInfo | undefined = await tryReadBuildInfoAsync(buildInfoPath); + if (oldBuildInfo && oldBuildInfo.configHash !== configHash) { + terminal.writeVerboseLine(`File copy configuration changed, discarding incremental state.`); + oldBuildInfo = undefined; + } + + // Since in watch mode only changed files will get passed in, need to ensure that all files from + // the previous build are still tracked. + const inputFileVersions: Map = new Map(oldBuildInfo?.inputFileVersions); + + const buildInfo: IIncrementalBuildInfo = { + configHash, + inputFileVersions + }; + + const allInputFiles: Set = new Set(); + for (const copyDescriptor of copyDescriptors.values()) { + allInputFiles.add(copyDescriptor.sourcePath); + } + + await Async.forEachAsync( + allInputFiles, + async (inputFilePath: string) => { + const fileContent: Buffer = await FileSystem.readFileToBufferAsync(inputFilePath); + const fileHash: string = createHash('sha256').update(fileContent).digest('base64'); + inputFileVersions.set(inputFilePath, fileHash); + }, + { + concurrency: Constants.maxParallelism + } + ); + + const copyDescriptorsWithWork: ICopyDescriptor[] = []; + for (const copyDescriptor of copyDescriptors.values()) { + const { sourcePath } = copyDescriptor; + + const sourceFileHash: string | undefined = inputFileVersions.get(sourcePath); + if (!sourceFileHash) { + throw new Error(`Missing hash for input file: ${sourcePath}`); + } + + if (oldBuildInfo?.inputFileVersions.get(sourcePath) === sourceFileHash) { + continue; + } + + copyDescriptorsWithWork.push(copyDescriptor); + } + + if (copyDescriptorsWithWork.length === 0) { + terminal.writeLine('All requested file copy operations are up to date. Nothing to do.'); + return; + } + + let copiedFileCount: number = 0; let linkedFileCount: number = 0; await Async.forEachAsync( - copyDescriptors.values(), + copyDescriptorsWithWork, async (copyDescriptor: ICopyDescriptor) => { if (copyDescriptor.hardlink) { linkedFileCount++; @@ -163,7 +265,7 @@ async function _copyFilesInnerAsync( `Linked "${copyDescriptor.sourcePath}" to "${copyDescriptor.destinationPath}".` ); } else { - copiedFolderOrFileCount++; + copiedFileCount++; await FileSystem.copyFilesAsync({ sourcePath: copyDescriptor.sourcePath, destinationPath: copyDescriptor.destinationPath, @@ -177,30 +279,12 @@ async function _copyFilesInnerAsync( { concurrency: Constants.maxParallelism } ); - const folderOrFilesPlural: string = copiedFolderOrFileCount === 1 ? '' : 's'; terminal.writeLine( - `Copied ${copiedFolderOrFileCount} folder${folderOrFilesPlural} or file${folderOrFilesPlural} and ` + + `Copied ${copiedFileCount} file${copiedFileCount === 1 ? '' : 's'} and ` + `linked ${linkedFileCount} file${linkedFileCount === 1 ? '' : 's'}` ); -} - -function* _resolveCopyOperationPaths( - heftConfiguration: HeftConfiguration, - copyOperations: Iterable -): IterableIterator { - const { buildFolderPath } = heftConfiguration; - function resolvePath(inputPath: string | undefined): string { - return inputPath ? path.resolve(buildFolderPath, inputPath) : buildFolderPath; - } - - for (const copyOperation of copyOperations) { - yield { - ...copyOperation, - sourcePath: resolvePath(copyOperation.sourcePath), - destinationFolders: copyOperation.destinationFolders.map(resolvePath) - }; - } + await writeBuildInfoAsync(buildInfo, buildInfoPath); } const PLUGIN_NAME: 'copy-files-plugin' = 'copy-files-plugin'; @@ -214,7 +298,7 @@ export default class CopyFilesPlugin implements IHeftTaskPlugin { - for (const operation of _resolveCopyOperationPaths(heftConfiguration, pluginOptions.copyOperations)) { + for (const operation of pluginOptions.copyOperations) { operations.copyOperations.add(operation); } return operations; diff --git a/apps/heft/src/plugins/DeleteFilesPlugin.ts b/apps/heft/src/plugins/DeleteFilesPlugin.ts index 711f2f7d44d..73976dad8f4 100644 --- a/apps/heft/src/plugins/DeleteFilesPlugin.ts +++ b/apps/heft/src/plugins/DeleteFilesPlugin.ts @@ -1,13 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; -import { FileSystem, Async, ITerminal } from '@rushstack/node-core-library'; +import type * as fs from 'node:fs'; + +import { FileSystem, Async } from '@rushstack/node-core-library'; +import type { ITerminal } from '@rushstack/terminal'; import { Constants } from '../utilities/Constants'; import { - getFilePathsAsync, - normalizeFileSelectionSpecifier, + getFileSelectionSpecifierPathsAsync, + asAbsoluteFileSelectionSpecifier, type IFileSelectionSpecifier } from './FileGlobSpecifier'; import type { HeftConfiguration } from '../configuration/HeftConfiguration'; @@ -25,46 +27,73 @@ interface IDeleteFilesPluginOptions { deleteOperations: IDeleteOperation[]; } -async function _getPathsToDeleteAsync(deleteOperations: Iterable): Promise> { - const pathsToDelete: Set = new Set(); +interface IGetPathsToDeleteResult { + filesToDelete: Set; + foldersToDelete: Set; +} + +async function _getPathsToDeleteAsync( + rootFolderPath: string, + deleteOperations: Iterable +): Promise { + const result: IGetPathsToDeleteResult = { + filesToDelete: new Set(), + foldersToDelete: new Set() + }; + await Async.forEachAsync( deleteOperations, async (deleteOperation: IDeleteOperation) => { - if ( - !deleteOperation.fileExtensions?.length && - !deleteOperation.includeGlobs?.length && - !deleteOperation.excludeGlobs?.length - ) { - // If no globs or file extensions are provided add the path to the set of paths to delete - pathsToDelete.add(deleteOperation.sourcePath); - } else { - normalizeFileSelectionSpecifier(deleteOperation); - // Glob the files under the source path and add them to the set of files to delete - const sourceFilePaths: Set = await getFilePathsAsync(deleteOperation); - for (const sourceFilePath of sourceFilePaths) { - pathsToDelete.add(sourceFilePath); + const absoluteSpecifier: IDeleteOperation = asAbsoluteFileSelectionSpecifier( + rootFolderPath, + deleteOperation + ); + + // Glob the files under the source path and add them to the set of files to delete + const sourcePaths: Map = await getFileSelectionSpecifierPathsAsync({ + fileGlobSpecifier: absoluteSpecifier, + includeFolders: true + }); + for (const [sourcePath, dirent] of sourcePaths) { + // If the sourcePath is a folder, add it to the foldersToDelete set. Otherwise, add it to + // the filesToDelete set. Symlinks and junctions are treated as files, and thus will fall + // into the filesToDelete set. + if (dirent.isDirectory()) { + result.foldersToDelete.add(sourcePath); + } else { + result.filesToDelete.add(sourcePath); } } }, { concurrency: Constants.maxParallelism } ); - return pathsToDelete; + return result; } export async function deleteFilesAsync( + rootFolderPath: string, deleteOperations: Iterable, terminal: ITerminal ): Promise { - const pathsToDelete: Set = await _getPathsToDeleteAsync(deleteOperations); + const pathsToDelete: IGetPathsToDeleteResult = await _getPathsToDeleteAsync( + rootFolderPath, + deleteOperations + ); await _deleteFilesInnerAsync(pathsToDelete, terminal); } -async function _deleteFilesInnerAsync(pathsToDelete: Set, terminal: ITerminal): Promise { +async function _deleteFilesInnerAsync( + pathsToDelete: IGetPathsToDeleteResult, + terminal: ITerminal +): Promise { let deletedFiles: number = 0; let deletedFolders: number = 0; + + const { filesToDelete, foldersToDelete } = pathsToDelete; + await Async.forEachAsync( - pathsToDelete, + filesToDelete, async (pathToDelete: string) => { try { await FileSystem.deleteFileAsync(pathToDelete, { throwIfNotExists: true }); @@ -73,16 +102,38 @@ async function _deleteFilesInnerAsync(pathsToDelete: Set, terminal: ITer } catch (error) { // If it doesn't exist, we can ignore the error. if (!FileSystem.isNotExistError(error)) { - // When we encounter an error relating to deleting a directory as if it was a file, - // attempt to delete the folder. Windows throws the unlink not permitted error, while - // linux throws the EISDIR error. - if (FileSystem.isUnlinkNotPermittedError(error) || FileSystem.isDirectoryError(error)) { - await FileSystem.deleteFolderAsync(pathToDelete); - terminal.writeVerboseLine(`Deleted folder "${pathToDelete}".`); - deletedFolders++; - } else { - throw error; - } + throw error; + } + } + }, + { concurrency: Constants.maxParallelism } + ); + + // Reverse the list of matching folders. Assuming that the list of folders came from + // the globber, the folders will be specified in tree-walk order, so by reversing the + // list we delete the deepest folders first and avoid not-exist errors for subfolders + // of an already-deleted parent folder. + const reversedFoldersToDelete: string[] = Array.from(foldersToDelete).reverse(); + + // Clear out any folders that were encountered during the file deletion process. This + // will recursively delete the folder and it's contents. There are two scenarios that + // this handles: + // - Deletions of empty folder structures (ex. when the delete glob is '**/*') + // - Deletions of folders that still contain files (ex. when the delete glob is 'lib') + // In the latter scenario, the count of deleted files will not be tracked. However, + // this is a fair trade-off for the performance benefit of not having to glob the + // folder structure again. + await Async.forEachAsync( + reversedFoldersToDelete, + async (folderToDelete: string) => { + try { + await FileSystem.deleteFolderAsync(folderToDelete); + terminal.writeVerboseLine(`Deleted folder "${folderToDelete}".`); + deletedFolders++; + } catch (error) { + // If it doesn't exist, we can ignore the error. + if (!FileSystem.isNotExistError(error)) { + throw error; } } }, @@ -97,20 +148,6 @@ async function _deleteFilesInnerAsync(pathsToDelete: Set, terminal: ITer } } -function* _resolveDeleteOperationPaths( - heftConfiguration: HeftConfiguration, - deleteOperations: Iterable -): IterableIterator { - const { buildFolderPath } = heftConfiguration; - for (const deleteOperation of deleteOperations) { - const { sourcePath } = deleteOperation; - yield { - ...deleteOperation, - sourcePath: sourcePath ? path.resolve(buildFolderPath, sourcePath) : buildFolderPath - }; - } -} - const PLUGIN_NAME: 'delete-files-plugin' = 'delete-files-plugin'; export default class DeleteFilesPlugin implements IHeftTaskPlugin { @@ -122,10 +159,7 @@ export default class DeleteFilesPlugin implements IHeftTaskPlugin { - for (const deleteOperation of _resolveDeleteOperationPaths( - heftConfiguration, - pluginOptions.deleteOperations - )) { + for (const deleteOperation of pluginOptions.deleteOperations) { fileOperations.deleteOperations.add(deleteOperation); } return fileOperations; diff --git a/apps/heft/src/plugins/FileGlobSpecifier.ts b/apps/heft/src/plugins/FileGlobSpecifier.ts index a4a6fe3658e..96d3223dd5a 100644 --- a/apps/heft/src/plugins/FileGlobSpecifier.ts +++ b/apps/heft/src/plugins/FileGlobSpecifier.ts @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; -import glob, { FileSystemAdapter } from 'fast-glob'; +import type * as fs from 'node:fs'; +import * as path from 'node:path'; + +import glob, { type FileSystemAdapter, type Entry } from 'fast-glob'; import { Async } from '@rushstack/node-core-library'; @@ -19,7 +21,7 @@ export interface IFileSelectionSpecifier { * fileExtensions, excludeGlobs, or includeGlobs are specified, the sourcePath is assumed * to be a folder. If it is not a folder, an error will be thrown. */ - sourcePath: string; + sourcePath?: string; /** * File extensions that should be included from the source folder. Only supported when the sourcePath @@ -71,6 +73,12 @@ export interface IGlobOptions { dot?: boolean; } +export interface IGetFileSelectionSpecifierPathsOptions { + fileGlobSpecifier: IFileSelectionSpecifier; + includeFolders?: boolean; + fileSystemAdapter?: FileSystemAdapter; +} + /** * Glob a set of files and return a list of paths that match the provided patterns. * @@ -129,40 +137,59 @@ export async function watchGlobAsync( return results; } -export async function getFilePathsAsync( - fileGlobSpecifier: IFileSelectionSpecifier, - fs?: FileSystemAdapter -): Promise> { - const rawFiles: string[] = await glob(fileGlobSpecifier.includeGlobs!, { - fs, +export async function getFileSelectionSpecifierPathsAsync( + options: IGetFileSelectionSpecifierPathsOptions +): Promise> { + const { fileGlobSpecifier, includeFolders, fileSystemAdapter } = options; + const rawEntries: Entry[] = await glob(fileGlobSpecifier.includeGlobs!, { + fs: fileSystemAdapter, cwd: fileGlobSpecifier.sourcePath, ignore: fileGlobSpecifier.excludeGlobs, + onlyFiles: !includeFolders, dot: true, - absolute: true + absolute: true, + objectMode: true }); - if (fs && isWatchFileSystemAdapter(fs)) { - const changedFiles: Set = new Set(); + let results: Map; + if (fileSystemAdapter && isWatchFileSystemAdapter(fileSystemAdapter)) { + results = new Map(); await Async.forEachAsync( - rawFiles, - async (file: string) => { - const state: IWatchedFileState = await fs.getStateAndTrackAsync(path.normalize(file)); + rawEntries, + async (entry: Entry) => { + const { path: filePath, dirent } = entry; + if (entry.dirent.isDirectory()) { + return; + } + const state: IWatchedFileState = await fileSystemAdapter.getStateAndTrackAsync( + path.normalize(filePath) + ); if (state.changed) { - changedFiles.add(file); + results.set(filePath, dirent as fs.Dirent); } }, { concurrency: 20 } ); - return changedFiles; + } else { + results = new Map(rawEntries.map((entry) => [entry.path, entry.dirent as fs.Dirent])); } - return new Set(rawFiles); + return results; } -export function normalizeFileSelectionSpecifier(fileGlobSpecifier: IFileSelectionSpecifier): void { - fileGlobSpecifier.includeGlobs = getIncludedGlobPatterns(fileGlobSpecifier); +export function asAbsoluteFileSelectionSpecifier( + rootPath: string, + fileGlobSpecifier: TSpecifier +): TSpecifier { + const { sourcePath } = fileGlobSpecifier; + return { + ...fileGlobSpecifier, + sourcePath: sourcePath ? path.resolve(rootPath, sourcePath) : rootPath, + includeGlobs: getIncludedGlobPatterns(fileGlobSpecifier), + fileExtensions: undefined + }; } function getIncludedGlobPatterns(fileGlobSpecifier: IFileSelectionSpecifier): string[] { diff --git a/apps/heft/src/plugins/NodeServicePlugin.ts b/apps/heft/src/plugins/NodeServicePlugin.ts index f0d3223b93f..dfb8e42760f 100644 --- a/apps/heft/src/plugins/NodeServicePlugin.ts +++ b/apps/heft/src/plugins/NodeServicePlugin.ts @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as child_process from 'child_process'; -import * as process from 'process'; +import * as child_process from 'node:child_process'; +import * as process from 'node:process'; + import { InternalError, SubprocessTerminator } from '@rushstack/node-core-library'; import type { IHeftTaskPlugin } from '../pluginFramework/IHeftPlugin'; @@ -15,6 +16,7 @@ import type { IScopedLogger } from '../pluginFramework/logging/ScopedLogger'; import { CoreConfigFiles } from '../utilities/CoreConfigFiles'; const PLUGIN_NAME: 'node-service-plugin' = 'node-service-plugin'; +const SERVE_PARAMETER_LONG_NAME: '--serve' = '--serve'; export interface INodeServicePluginCompleteConfiguration { commandName: string; @@ -92,9 +94,24 @@ export default class NodeServicePlugin implements IHeftTaskPlugin { // Set this immediately to make it available to the internal methods that use it this._logger = taskSession.logger; - taskSession.hooks.run.tapPromise(PLUGIN_NAME, async () => { - taskSession.logger.terminal.writeWarningLine('Node services can only be run in watch mode.'); - }); + const isServeMode: boolean = taskSession.parameters.getFlagParameter(SERVE_PARAMETER_LONG_NAME).value; + + if (isServeMode && !taskSession.parameters.watch) { + throw new Error( + `The ${JSON.stringify( + SERVE_PARAMETER_LONG_NAME + )} parameter is only available when running in watch mode.` + + ` Try replacing "${taskSession.parsedCommandLine?.unaliasedCommandName}" with` + + ` "${taskSession.parsedCommandLine?.unaliasedCommandName}-watch" in your Heft command line.` + ); + } + + if (!isServeMode) { + taskSession.logger.terminal.writeVerboseLine( + `Not launching the service because the "${SERVE_PARAMETER_LONG_NAME}" parameter was not specified` + ); + return; + } taskSession.hooks.runIncremental.tapPromise( PLUGIN_NAME, @@ -104,17 +121,16 @@ export default class NodeServicePlugin implements IHeftTaskPlugin { ); } - private async _loadStageConfiguration( + private async _loadStageConfigurationAsync( taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration ): Promise { if (!this._rawConfiguration) { - this._rawConfiguration = - await CoreConfigFiles.nodeServiceConfigurationFile.tryLoadConfigurationFileForProjectAsync( - taskSession.logger.terminal, - heftConfiguration.buildFolderPath, - heftConfiguration.rigConfig - ); + this._rawConfiguration = await CoreConfigFiles.tryLoadNodeServiceConfigurationFileAsync( + taskSession.logger.terminal, + heftConfiguration.buildFolderPath, + heftConfiguration.rigConfig + ); // defaults this._configuration = { @@ -148,21 +164,21 @@ export default class NodeServicePlugin implements IHeftTaskPlugin { if (this._shellCommand === undefined) { if (this._configuration.ignoreMissingScript) { taskSession.logger.terminal.writeLine( - `The plugin is disabled because the project's package.json` + + `The node service cannot be started because the project's package.json` + ` does not have a "${this._configuration.commandName}" script` ); } else { throw new Error( - `The node-service task cannot start because the project's package.json ` + + `The node service cannot be started because the project's package.json ` + `does not have a "${this._configuration.commandName}" script` ); } this._pluginEnabled = false; } } else { - taskSession.logger.terminal.writeVerboseLine( - 'The plugin is disabled because its config file was not found: ' + - CoreConfigFiles.nodeServiceConfigurationFile.projectRelativeFilePath + throw new Error( + 'The node service cannot be started because the task config file was not found: ' + + CoreConfigFiles.nodeServiceConfigurationProjectRelativeFilePath ); } } @@ -172,7 +188,7 @@ export default class NodeServicePlugin implements IHeftTaskPlugin { taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration ): Promise { - await this._loadStageConfiguration(taskSession, heftConfiguration); + await this._loadStageConfigurationAsync(taskSession, heftConfiguration); if (!this._pluginEnabled) { return; } @@ -206,7 +222,12 @@ export default class NodeServicePlugin implements IHeftTaskPlugin { // Passing a negative PID terminates the entire group instead of just the one process. // This works because we set detached=true for child_process.spawn() - process.kill(-this._activeChildProcess.pid, 'SIGTERM'); + + const pid: number | undefined = this._activeChildProcess.pid; + if (pid !== undefined) { + // If pid was undefined, the process failed to spawn + process.kill(-pid, 'SIGTERM'); + } this._clearTimeout(); this._timeout = setTimeout(() => { @@ -274,7 +295,10 @@ export default class NodeServicePlugin implements IHeftTaskPlugin { }); SubprocessTerminator.killProcessTreeOnExit(childProcess, SubprocessTerminator.RECOMMENDED_OPTIONS); - const childPid: number = childProcess.pid; + const childPid: number | undefined = childProcess.pid; + if (childPid === undefined) { + throw new InternalError(`Failed to spawn child process`); + } this._logger.terminal.writeVerboseLine(`Started service process #${childPid}`); // Create a promise that resolves when the child process exits @@ -290,7 +314,7 @@ export default class NodeServicePlugin implements IHeftTaskPlugin { this._logger.terminal.writeError(data.toString()); }); - childProcess.on('close', (code: number, signal: string): void => { + childProcess.on('close', (exitCode: number | null, signal: NodeJS.Signals | null): void => { try { // The 'close' event is emitted after a process has ended and the stdio streams of a child process // have been closed. This is distinct from the 'exit' event, since multiple processes might share the @@ -300,7 +324,7 @@ export default class NodeServicePlugin implements IHeftTaskPlugin { if (this._state === State.Running) { this._logger.terminal.writeWarningLine( `The service process #${childPid} terminated unexpectedly` + - this._formatCodeOrSignal(code, signal) + this._formatCodeOrSignal(exitCode, signal) ); this._transitionToStopped(); return; @@ -309,7 +333,7 @@ export default class NodeServicePlugin implements IHeftTaskPlugin { if (this._state === State.Stopping || this._state === State.Killing) { this._logger.terminal.writeVerboseLine( `The service process #${childPid} terminated successfully` + - this._formatCodeOrSignal(code, signal) + this._formatCodeOrSignal(exitCode, signal) ); this._transitionToStopped(); return; @@ -321,6 +345,8 @@ export default class NodeServicePlugin implements IHeftTaskPlugin { childProcess.on('exit', (code: number | null, signal: string | null) => { try { + // Under normal conditions we don't reject the promise here, because 'data' events can continue + // to fire as data is flushed, before finally concluding with the 'close' event. this._logger.terminal.writeVerboseLine( `The service process fired its "exit" event` + this._formatCodeOrSignal(code, signal) ); diff --git a/apps/heft/src/plugins/RunScriptPlugin.ts b/apps/heft/src/plugins/RunScriptPlugin.ts index 11430e11eb1..6c74d9dfd56 100644 --- a/apps/heft/src/plugins/RunScriptPlugin.ts +++ b/apps/heft/src/plugins/RunScriptPlugin.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import * as path from 'node:path'; + import type { HeftConfiguration } from '../configuration/HeftConfiguration'; import type { IHeftTaskPlugin } from '../pluginFramework/IHeftPlugin'; import type { IHeftTaskSession, IHeftTaskRunHookOptions } from '../pluginFramework/HeftTaskSession'; @@ -53,10 +55,10 @@ export default class RunScriptPlugin implements IHeftTaskPlugin { - // The scriptPath property should be fully resolved since it is included in the resolution logic used by - // HeftConfiguration - const resolvedModulePath: string = pluginOptions.scriptPath; - + const resolvedModulePath: string = path.resolve( + heftConfiguration.buildFolderPath, + pluginOptions.scriptPath + ); const runScript: IRunScript = await import(resolvedModulePath); if (!runScript.runAsync) { throw new Error( diff --git a/apps/heft/src/plugins/SetEnvironmentVariablesPlugin.ts b/apps/heft/src/plugins/SetEnvironmentVariablesPlugin.ts new file mode 100644 index 00000000000..fb83903b83b --- /dev/null +++ b/apps/heft/src/plugins/SetEnvironmentVariablesPlugin.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { HeftConfiguration } from '../configuration/HeftConfiguration'; +import type { IHeftTaskSession } from '../pluginFramework/HeftTaskSession'; +import type { IHeftTaskPlugin } from '../pluginFramework/IHeftPlugin'; + +export const PLUGIN_NAME: string = 'set-environment-variables-plugin'; + +export interface ISetEnvironmentVariablesPluginOptions { + environmentVariablesToSet: Record; +} + +export default class SetEnvironmentVariablesPlugin + implements IHeftTaskPlugin +{ + public apply( + taskSession: IHeftTaskSession, + heftConfiguration: HeftConfiguration, + { environmentVariablesToSet }: ISetEnvironmentVariablesPluginOptions + ): void { + taskSession.hooks.run.tap( + { + name: PLUGIN_NAME, + stage: Number.MIN_SAFE_INTEGER + }, + () => { + for (const [key, value] of Object.entries(environmentVariablesToSet)) { + taskSession.logger.terminal.writeLine(`Setting environment variable ${key}=${value}`); + process.env[key] = value; + } + } + ); + } +} diff --git a/apps/heft/src/schemas/copy-files-options.schema.json b/apps/heft/src/schemas/copy-files-options.schema.json index c52a19a8e77..f42d46af61c 100644 --- a/apps/heft/src/schemas/copy-files-options.schema.json +++ b/apps/heft/src/schemas/copy-files-options.schema.json @@ -15,20 +15,13 @@ "items": { "type": "object", "additionalProperties": false, + "required": ["destinationFolders"], "anyOf": [ - { - "required": ["sourcePath"] - }, - { - "required": ["includeGlobs"] - }, - { - "required": ["fileExtensions"] - } + { "required": ["sourcePath"] }, + { "required": ["fileExtensions"] }, + { "required": ["includeGlobs"] }, + { "required": ["excludeGlobs"] } ], - - "required": ["destinationFolders"], - "properties": { "sourcePath": { "title": "Source Path", diff --git a/apps/heft/src/schemas/delete-files-options.schema.json b/apps/heft/src/schemas/delete-files-options.schema.json index d248396ea8c..0e6c0a9ccae 100644 --- a/apps/heft/src/schemas/delete-files-options.schema.json +++ b/apps/heft/src/schemas/delete-files-options.schema.json @@ -16,17 +16,11 @@ "type": "object", "additionalProperties": false, "anyOf": [ - { - "required": ["sourcePath"] - }, - { - "required": ["includeGlobs"] - }, - { - "required": ["fileExtensions"] - } + { "required": ["sourcePath"] }, + { "required": ["fileExtensions"] }, + { "required": ["includeGlobs"] }, + { "required": ["excludeGlobs"] } ], - "properties": { "sourcePath": { "title": "Source Path", diff --git a/apps/heft/src/schemas/heft-legacy.schema.json b/apps/heft/src/schemas/heft-legacy.schema.json new file mode 100644 index 00000000000..45d26aa00f0 --- /dev/null +++ b/apps/heft/src/schemas/heft-legacy.schema.json @@ -0,0 +1,211 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Legacy Heft Configuration", + "description": "Defines configuration used by the legacy version of Heft.", + "type": "object", + + "definitions": { + "anything": { + "type": ["array", "boolean", "integer", "number", "object", "string"], + "items": { "$ref": "#/definitions/anything" } + } + }, + + "additionalProperties": false, + + "properties": { + "$schema": { + "description": "Part of the JSON Schema standard, this optional keyword declares the URL of the schema that the file conforms to. Editors may download the schema and use it to perform syntax highlighting.", + "type": "string" + }, + + "extends": { + "description": "Optionally specifies another JSON config file that this file extends from. This provides a way for standard settings to be shared across multiple projects. To delete an inherited setting, set it to `null` in this file.", + "type": "string" + }, + + "eventActions": { + "type": "array", + "description": "An array of actions (such as deleting files or folders) that should occur during a Heft run.", + + "items": { + "type": "object", + "required": ["actionKind", "heftEvent", "actionId"], + "allOf": [ + { + "properties": { + "actionKind": { + "type": "string", + "description": "The kind of built-in operation that should be performed.", + "enum": ["deleteGlobs", "copyFiles", "runScript"] + }, + + "heftEvent": { + "type": "string", + "description": "The Heft stage when this action should be performed. Note that heft.json event actions are scheduled after any plugin tasks have processed the event. For example, a \"compile\" event action will be performed after the TypeScript compiler has been invoked.", + "enum": ["clean", "pre-compile", "compile", "bundle", "post-build", "test"] + }, + + "actionId": { + "type": "string", + "description": "A user-defined tag whose purpose is to allow configs to replace/delete handlers that were added by other configs." + } + } + }, + { + "oneOf": [ + { + "required": ["globsToDelete"], + "properties": { + "actionKind": { + "type": "string", + "enum": ["deleteGlobs"] + }, + + "heftEvent": { + "type": "string", + "enum": ["clean", "pre-compile", "compile", "bundle", "post-build"] + }, + + "globsToDelete": { + "type": "array", + "description": "Glob patterns to be deleted. The paths are resolved relative to the project folder.", + "items": { + "type": "string", + "pattern": "[^\\\\]" + } + } + } + }, + { + "required": ["copyOperations"], + "properties": { + "actionKind": { + "type": "string", + "enum": ["copyFiles"] + }, + + "heftEvent": { + "type": "string", + "enum": ["pre-compile", "compile", "bundle", "post-build"] + }, + + "copyOperations": { + "type": "array", + "description": "An array of copy operations to run perform during the specified Heft event.", + "items": { + "type": "object", + "required": ["sourceFolder", "destinationFolders"], + "properties": { + "sourceFolder": { + "type": "string", + "description": "The base folder that files will be copied from, relative to the project root. Settings such as \"includeGlobs\" and \"excludeGlobs\" will be resolved relative to this folder. NOTE: Assigning \"sourceFolder\" does not by itself select any files to be copied.", + "pattern": "[^\\\\]" + }, + + "destinationFolders": { + "type": "array", + "description": "One or more folders that files will be copied into, relative to the project root. If you specify more than one destination folder, Heft will read the input files only once, using streams to efficiently write multiple outputs.", + "items": { + "type": "string", + "pattern": "[^\\\\]" + } + }, + + "fileExtensions": { + "type": "array", + "description": "If specified, this option recursively scans all folders under \"sourceFolder\" and includes any files that match the specified extensions. (If \"fileExtensions\" and \"includeGlobs\" are both specified, their selections are added together.)", + "items": { + "type": "string", + "pattern": "^\\.[A-z0-9-_.]*[A-z0-9-_]+$" + } + }, + + "excludeGlobs": { + "type": "array", + "description": "A list of glob patterns that exclude files/folders from being copied. The paths are resolved relative to \"sourceFolder\". These exclusions eliminate items that were selected by the \"includeGlobs\" or \"fileExtensions\" setting.", + "items": { + "type": "string", + "pattern": "[^\\\\]" + } + }, + + "includeGlobs": { + "type": "array", + "description": "A list of glob patterns that select files to be copied. The paths are resolved relative to \"sourceFolder\".", + "items": { + "type": "string", + "pattern": "[^\\\\]" + } + }, + + "flatten": { + "type": "boolean", + "description": "Normally, when files are selected under a child folder, a corresponding folder will be created in the destination folder. Specify flatten=true to discard the source path and copy all matching files to the same folder. If two files have the same name an error will be reported. The default value is false." + }, + + "hardlink": { + "type": "boolean", + "description": "If true, filesystem hard links will be created instead of copying the file. Depending on the operating system, this may be faster. (But note that it may cause unexpected behavior if a tool modifies the link.) The default value is false." + } + } + } + } + } + }, + { + "required": ["scriptPath"], + "properties": { + "actionKind": { + "type": "string", + "enum": ["runScript"] + }, + + "heftEvent": { + "type": "string", + "enum": ["pre-compile", "compile", "bundle", "post-build", "test"] + }, + + "scriptPath": { + "type": "string", + "description": "Path to the script that will be run, relative to the project root.", + "pattern": "[^\\\\]" + }, + + "scriptOptions": { + "type": "object", + "description": "Optional parameters that will be passed to the script at runtime.", + "patternProperties": { + "^.*$": { "$ref": "#/definitions/anything" } + } + } + } + } + ] + } + ] + } + }, + + "heftPlugins": { + "type": "array", + "description": "Defines heft plugins that are used by a project.", + + "items": { + "type": "object", + "required": ["plugin"], + "properties": { + "plugin": { + "description": "Path to the plugin package, relative to the project root.", + "type": "string", + "pattern": "[^\\\\]" + }, + + "options": { + "type": "object" + } + } + } + } + } +} diff --git a/apps/heft/src/schemas/heft-plugin.schema.json b/apps/heft/src/schemas/heft-plugin.schema.json index 8ddf6b2d024..37dd45012d7 100644 --- a/apps/heft/src/schemas/heft-plugin.schema.json +++ b/apps/heft/src/schemas/heft-plugin.schema.json @@ -27,6 +27,12 @@ "type": "string", "pattern": "^-(-[a-z0-9]+)+$" }, + "shortName": { + "title": "Short Name", + "description": "A optional short form of the parameter (e.g. \"-v\" instead of \"--verbose\")", + "type": "string", + "pattern": "^-[a-zA-Z]$" + }, "description": { "title": "Parameter Description", "description": "A detailed description of the parameter, which appears when requesting help for the command (e.g. \"heft phaseName --help my-command\").", @@ -90,6 +96,7 @@ "properties": { "parameterKind": { "$ref": "#/definitions/anything" }, "longName": { "$ref": "#/definitions/anything" }, + "shortName": { "$ref": "#/definitions/anything" }, "description": { "$ref": "#/definitions/anything" }, "required": { "$ref": "#/definitions/anything" }, @@ -145,6 +152,7 @@ "properties": { "parameterKind": { "$ref": "#/definitions/anything" }, "longName": { "$ref": "#/definitions/anything" }, + "shortName": { "$ref": "#/definitions/anything" }, "description": { "$ref": "#/definitions/anything" }, "required": { "$ref": "#/definitions/anything" }, @@ -175,6 +183,7 @@ "properties": { "parameterKind": { "$ref": "#/definitions/anything" }, "longName": { "$ref": "#/definitions/anything" }, + "shortName": { "$ref": "#/definitions/anything" }, "description": { "$ref": "#/definitions/anything" }, "required": { "$ref": "#/definitions/anything" } } @@ -214,6 +223,7 @@ "properties": { "parameterKind": { "$ref": "#/definitions/anything" }, "longName": { "$ref": "#/definitions/anything" }, + "shortName": { "$ref": "#/definitions/anything" }, "description": { "$ref": "#/definitions/anything" }, "required": { "$ref": "#/definitions/anything" }, @@ -251,6 +261,7 @@ "properties": { "parameterKind": { "$ref": "#/definitions/anything" }, "longName": { "$ref": "#/definitions/anything" }, + "shortName": { "$ref": "#/definitions/anything" }, "description": { "$ref": "#/definitions/anything" }, "required": { "$ref": "#/definitions/anything" }, @@ -292,6 +303,7 @@ "properties": { "parameterKind": { "$ref": "#/definitions/anything" }, "longName": { "$ref": "#/definitions/anything" }, + "shortName": { "$ref": "#/definitions/anything" }, "description": { "$ref": "#/definitions/anything" }, "required": { "$ref": "#/definitions/anything" }, @@ -329,6 +341,7 @@ "properties": { "parameterKind": { "$ref": "#/definitions/anything" }, "longName": { "$ref": "#/definitions/anything" }, + "shortName": { "$ref": "#/definitions/anything" }, "description": { "$ref": "#/definitions/anything" }, "required": { "$ref": "#/definitions/anything" }, diff --git a/apps/heft/src/schemas/heft.schema.json b/apps/heft/src/schemas/heft.schema.json index 3b0211d3275..cba91630724 100644 --- a/apps/heft/src/schemas/heft.schema.json +++ b/apps/heft/src/schemas/heft.schema.json @@ -54,6 +54,12 @@ "delete-operation": { "type": "object", "additionalProperties": false, + "anyOf": [ + { "required": ["sourcePath"] }, + { "required": ["fileExtensions"] }, + { "required": ["includeGlobs"] }, + { "required": ["excludeGlobs"] } + ], "properties": { "sourcePath": { "title": "Source Path", @@ -98,7 +104,7 @@ }, "extends": { - "description": "Optionally specifies another JSON config file that this file extends from. This provides a way for standard settings to be shared across multiple projects.", + "description": "Optionally specifies another JSON config file that this file extends from. This provides a way for standard settings to be shared across multiple projects. To delete an inherited setting, set it to `null` in this file.", "type": "string" }, @@ -108,6 +114,34 @@ "items": { "$ref": "#/definitions/heft-plugin" } }, + "aliasesByName": { + "type": "object", + "description": "Defines aliases for existing Heft actions, and allows them to be invoked by name with default parameters.", + "additionalProperties": false, + "patternProperties": { + "^[a-z][a-z0-9]*([-][a-z0-9]+)*$": { + "description": "Defines a Heft action alias.", + "type": "object", + "additionalProperties": false, + "required": ["actionName"], + "properties": { + "actionName": { + "description": "The name of the Heft action to invoke.", + "type": "string", + "pattern": "^[a-z][a-z0-9]*([-][a-z0-9]+)*$" + }, + "defaultParameters": { + "description": "Parameters to pass to the Heft action by default. These parameters will be appended after the specified action and before any user-specified parameters.", + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "phasesByName": { "type": "object", "description": "Heft phases that can be run during an execution of Heft.", @@ -147,29 +181,9 @@ "type": "object", "description": "Defines a Heft task.", "additionalProperties": false, - "oneOf": [ - { - "required": ["taskPlugin"], - "properties": { - "taskPlugin": { "$ref": "#/definitions/heft-plugin" } - } - }, - { - "required": ["taskEvent"], - "properties": { - "taskEvent": { "$ref": "#/definitions/heft-event" } - } - } - ], + "required": ["taskPlugin"], "properties": { - "taskPlugin": { - "description": "A plugin that can be used to extend Heft functionality.", - "type": "object" - }, - "taskEvent": { - "description": "An event that can be used to extend Heft functionality.", - "type": "object" - }, + "taskPlugin": { "$ref": "#/definitions/heft-plugin" }, "taskDependencies": { "type": "array", diff --git a/apps/heft/src/schemas/node-service.schema.json b/apps/heft/src/schemas/node-service.schema.json index ee9700a5308..67af1bc48a9 100644 --- a/apps/heft/src/schemas/node-service.schema.json +++ b/apps/heft/src/schemas/node-service.schema.json @@ -13,7 +13,7 @@ }, "extends": { - "description": "Optionally specifies another JSON config file that this file extends from. This provides a way for standard settings to be shared across multiple projects.", + "description": "Optionally specifies another JSON config file that this file extends from. This provides a way for standard settings to be shared across multiple projects. To delete an inherited setting, set it to `null` in this file.", "type": "string" }, diff --git a/apps/heft/src/schemas/run-script-options.schema.json b/apps/heft/src/schemas/run-script-options.schema.json index be41cfc37b7..1a42badddbb 100644 --- a/apps/heft/src/schemas/run-script-options.schema.json +++ b/apps/heft/src/schemas/run-script-options.schema.json @@ -19,10 +19,7 @@ "title": "Script Path", "type": "string", "description": "Path to the script that will be run, relative to the project root.", - "items": { - "type": "string", - "pattern": "[^\\\\]" - } + "pattern": "[^\\\\]" }, "scriptOptions": { diff --git a/apps/heft/src/schemas/set-environment-variables-plugin.schema.json b/apps/heft/src/schemas/set-environment-variables-plugin.schema.json new file mode 100644 index 00000000000..7ffa21d2967 --- /dev/null +++ b/apps/heft/src/schemas/set-environment-variables-plugin.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "CopyFiles Heft Task Event Options", + "description": "Defines configuration used by the \"copyFiles\" Heft task event.", + "type": "object", + + "additionalProperties": false, + "required": ["environmentVariablesToSet"], + + "properties": { + "environmentVariablesToSet": { + "type": "object", + "additionalProperties": { + "type": "string", + "pattern": ".+" + } + } + } +} diff --git a/apps/heft/src/schemas/templates/heft.json b/apps/heft/src/schemas/templates/heft.json index 38670ddac5b..870a1a538a3 100644 --- a/apps/heft/src/schemas/templates/heft.json +++ b/apps/heft/src/schemas/templates/heft.json @@ -2,145 +2,173 @@ * Defines configuration used by core Heft. */ { - "$schema": "https://developer.microsoft.com/json-schemas/heft/heft.schema.json", + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", /** * Optionally specifies another JSON config file that this file extends from. This provides a way for standard * settings to be shared across multiple projects. + * + * To delete an inherited setting, set it to `null` in this file. */ // "extends": "base-project/config/heft.json", - "eventActions": [ - // { - // /** - // * (Required) The kind of built-in operation that should be performed. - // * The "deleteGlobs" action deletes files or folders that match the specified glob patterns. - // */ - // "actionKind": "deleteGlobs", - // - // /** - // * (Required) The Heft stage when this action should be performed. Note that heft.json event actions - // * are scheduled after any plugin tasks have processed the event. For example, a "compile" event action - // * will be performed after the TypeScript compiler has been invoked. - // * - // * Options: "clean", "pre-compile", "compile", "bundle", "post-build" - // */ - // "heftEvent": "clean", - // + /** + * Defines aliases for existing Heft actions, and allows them to be invoked by + * name with default parameters. The JSON keys is are user-defined names. + * + * For example, the "heft start" alias is conventionally defined to invoke + * "heft build-watch --serve" using a definition like this: + * + * "aliasesByName": { "start": { "actionName": "build-watch", "defaultParameters": [ "--serve" ] } } + */ + "aliasesByName": { + // /** + // * The command-line action name of the Heft alias that is being defined. + // * This JSON key is a user-defined value. + // */ + // "example-alias-name": { // /** - // * (Required) A user-defined tag whose purpose is to allow configs to replace/delete handlers that - // * were added by other configs. + // * The name of the existing Heft command-line action to be invoked by this alias. // */ - // "actionId": "my-example-action", + // "actionName": "example-action", // // /** - // * (Required) Glob patterns to be deleted. The paths are resolved relative to the project folder. - // * Documentation for supported glob syntaxes: https://www.npmjs.com/package/fast-glob + // * A list of command-line parameters to pass to the Heft action by default. + // * These parameters will be appended after the specified action and before + // * any user-specified parameters. // */ - // "globsToDelete": [ - // "dist", - // "lib", - // "lib-esnext", - // "temp" - // ] - // }, - // + // "defaultParameters": [ "--do-some-thing" ] + // } + }, + + /** + * List of Heft lifecycle plugins to be loaded for this project. + */ + "heftPlugins": [ // { // /** - // * (Required) The kind of built-in operation that should be performed. - // * The "copyFiles" action copies files that match the specified patterns. - // */ - // "actionKind": "copyFiles", - // - // /** - // * (Required) The Heft stage when this action should be performed. Note that heft.json event actions - // * are scheduled after any plugin tasks have processed the event. For example, a "compile" event action - // * will be performed after the TypeScript compiler has been invoked. - // * - // * Options: "pre-compile", "compile", "bundle", "post-build" + // * (REQUIRED) The NPM package name for the plugin. // */ - // "heftEvent": "pre-compile", + // "pluginPackage": "@mycorp/heft-example-plugin", // // /** - // * (Required) A user-defined tag whose purpose is to allow configs to replace/delete handlers that - // * were added by other configs. + // * The name of the plugin to load from the NPM package's heft-plugin.json manifest. + // * If not specified, and if the plugin package provides a single plugin, then that + // * plugin will be loaded. // */ - // "actionId": "my-example-action", + // // "pluginName": "example-plugin", // // /** - // * (Required) An array of copy operations to run perform during the specified Heft event. + // * Options to pass to the plugin. This is a custom object whose structure + // * is defined by the plugin. // */ - // "copyOperations": [ - // { - // /** - // * (Required) The base folder that files will be copied from, relative to the project root. - // * Settings such as "includeGlobs" and "excludeGlobs" will be resolved relative - // * to this folder. - // * NOTE: Assigning "sourceFolder" does not by itself select any files to be copied. - // */ - // "sourceFolder": "src", - // - // /** - // * (Required) One or more folders that files will be copied into, relative to the project root. - // * If you specify more than one destination folder, Heft will read the input files only once, using - // * streams to efficiently write multiple outputs. - // */ - // "destinationFolders": ["dist/assets"], - // - // /** - // * If specified, this option recursively scans all folders under "sourceFolder" and includes any files - // * that match the specified extensions. (If "fileExtensions" and "includeGlobs" are both - // * specified, their selections are added together.) - // */ - // "fileExtensions": [".jpg", ".png"], - // - // /** - // * A list of glob patterns that select files to be copied. The paths are resolved relative - // * to "sourceFolder". - // * Documentation for supported glob syntaxes: https://www.npmjs.com/package/fast-glob - // */ - // "includeGlobs": ["assets/*.md"], - // - // /** - // * A list of glob patterns that exclude files/folders from being copied. The paths are resolved relative - // * to "sourceFolder". These exclusions eliminate items that were selected by the "includeGlobs" - // * or "fileExtensions" setting. - // */ - // "excludeGlobs": [], - // - // /** - // * Normally, when files are selected under a child folder, a corresponding folder will be created in - // * the destination folder. Specify flatten=true to discard the source path and copy all matching files - // * to the same folder. If two files have the same name an error will be reported. - // * The default value is false. - // */ - // "flatten": false, - // - // /** - // * If true, filesystem hard links will be created instead of copying the file. Depending on the - // * operating system, this may be faster. (But note that it may cause unexpected behavior if a tool - // * modifies the link.) The default value is false. - // */ - // "hardlink": false - // } - // ] + // // "options": { "example-key": "example-value" } // } ], /** - * The list of Heft plugins to be loaded. + * Heft phases that can be run during an execution of Heft. + * The JSON keys is are user-defined names. */ - "heftPlugins": [ - // { - // /** - // * The path to the plugin package. - // */ - // "plugin": "path/to/my-plugin", - // - // /** - // * An optional object that provides additional settings that may be defined by the plugin. - // */ - // // "options": { } - // } - ] + "phasesByName": { + /** + * The name of the phase, which is used by other fields such as "phaseDependencies". + * This JSON key is a user-defined value. + */ + "example-phase": { + /** + * A description to be shown in the command-line help. + */ + "phaseDescription": "An example phase", + + /** + * A list of delete operations to perform when cleaning at the beginning of phase execution. + * Their structure is similar the options used by the delete-files-plugin. + */ + "cleanFiles": [ + // { + // /** + // * Absolute path to the source file or folder, relative to the project root. + // * If "fileExtensions", "excludeGlobs", or "includeGlobs" are specified, then "sourcePath" + // * is assumed to be a folder; if it is not a folder, an error will be thrown. + // * Settings such as "includeGlobs" and "excludeGlobs" will be resolved relative to this path. + // * If no globs or file extensions are specified, the entire folder will be copied. + // * If this parameter is not provided, it defaults to the project root. + // */ + // // "sourcePath": "lib", + // + // /** + // * If specified, this option recursively scans all folders under "sourcePath" and includes + // * any files that match the specified extensions. If "fileExtensions" and "includeGlobs" + // * are both specified, their selections are added together. + // */ + // // "fileExtensions": [ ".png" ], + // + // /** + // * A list of glob patterns that select files to be copied. The paths are resolved relative + // * to "sourcePath", which must be a folder path. If "fileExtensions" and "includeGlobs" + // * are both specified, their selections are added together. + // * + // * For glob syntax, refer to: https://www.npmjs.com/package/fast-glob + // */ + // // "excludeGlobs": [], + // + // + // /** + // * A list of glob patterns that exclude files or folders from being copied. The paths are resolved + // * relative to "sourcePath", which must be a folder path. These exclusions eliminate items that + // * were selected by the "includeGlobs" or "fileExtensions" setting. + // * + // * For glob syntax, refer to: https://www.npmjs.com/package/fast-glob + // */ + // // "includeGlobs": [ "**/temp" ] + // } + ], + + /** + * A list of phase names that must be run before this phase can start. + */ + "phaseDependencies": [], + + /** + * Heft tasks that are run during an execution of the Heft phase. + * The JSON keys is are user-defined names. + */ + "tasksByName": { + /** + * The name of the task, which is used by other fields such as "taskDependencies". + * This JSON key is a user-defined value. + */ + "example-task": { + /** + * A list of task names that must be run before this task can start. + */ + "taskDependencies": [], + + /** + * (REQUIRED) The Heft plugin to be loaded, which will perform the operation for this task. + */ + "taskPlugin": { + /** + * (REQUIRED) The NPM package name for the plugin. + */ + "pluginPackage": "@mycorp/heft-example-plugin" + + /** + * The name of the plugin to load from the NPM package's heft-plugin.json manifest. + * If not specified, and if the plugin package provides a single plugin, then that + * plugin will be loaded. + */ + // "pluginName": "example-plugin", + + /** + * Options to pass to the plugin. This is a custom object whose structure + * is defined by the plugin. + */ + // "options": { "example-key": "example-value" } + } + } + } + } + } } diff --git a/apps/heft/src/start.ts b/apps/heft/src/start.ts index d4cce67642f..b6c5f1e9391 100644 --- a/apps/heft/src/start.ts +++ b/apps/heft/src/start.ts @@ -8,7 +8,7 @@ import { HeftCommandLineParser } from './cli/HeftCommandLineParser'; const parser: HeftCommandLineParser = new HeftCommandLineParser(); parser - .execute() + .executeAsync() .then(() => { // This should be removed when the issue with aria not tearing down process.exit(process.exitCode === undefined ? 0 : process.exitCode); diff --git a/apps/heft/src/startWithVersionSelector.ts b/apps/heft/src/startWithVersionSelector.ts index eea96101400..d4a3a3dc150 100644 --- a/apps/heft/src/startWithVersionSelector.ts +++ b/apps/heft/src/startWithVersionSelector.ts @@ -1,14 +1,17 @@ // 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 */ + // NOTE: Since startWithVersionSelector.ts is loaded in the same process as start.ts, any dependencies that // we import here may become side-by-side versions. We want to minimize any dependencies. -import * as path from 'path'; -import * as fs from 'fs'; +import * as path from 'node:path'; +import * as fs from 'node:fs'; + import type { IPackageJson } from '@rushstack/node-core-library'; -import { Constants } from './utilities/Constants'; -const HEFT_PACKAGE_NAME: string = '@rushstack/heft'; +import { getToolParameterNamesFromArgs } from './utilities/CliUtilities'; +import { Constants } from './utilities/Constants'; // Excerpted from PackageJsonLookup.tryGetPackageFolderFor() function tryGetPackageFolderFor(resolvedFileOrFolderPath: string): string | undefined { @@ -47,14 +50,15 @@ function tryGetPackageFolderFor(resolvedFileOrFolderPath: string): string | unde * Use "heft --unmanaged" to bypass this feature. */ function tryStartLocalHeft(): boolean { - if (process.argv.indexOf(Constants.unmanagedParameterLongName) >= 0) { + const toolParameters: Set = getToolParameterNamesFromArgs(); + if (toolParameters.has(Constants.unmanagedParameterLongName)) { console.log( `Bypassing the Heft version selector because ${JSON.stringify(Constants.unmanagedParameterLongName)} ` + 'was specified.' ); console.log(); return false; - } else if (process.argv.indexOf(Constants.debugParameterLongName) >= 0) { + } else if (toolParameters.has(Constants.debugParameterLongName)) { // The unmanaged flag could be undiscoverable if it's not in their locally installed version console.log( 'Searching for a locally installed version of Heft. Use the ' + @@ -78,8 +82,8 @@ function tryStartLocalHeft(): boolean { // Does package.json have a dependency on Heft? if ( - !(packageJson.dependencies && packageJson.dependencies[HEFT_PACKAGE_NAME]) && - !(packageJson.devDependencies && packageJson.devDependencies[HEFT_PACKAGE_NAME]) + !(packageJson.dependencies && packageJson.dependencies[Constants.heftPackageName]) && + !(packageJson.devDependencies && packageJson.devDependencies[Constants.heftPackageName]) ) { // No explicit dependency on Heft return false; @@ -87,7 +91,7 @@ function tryStartLocalHeft(): boolean { // To avoid a loading the "resolve" NPM package, let's assume that the Heft dependency must be // installed as "/node_modules/@rushstack/heft". - const heftFolder: string = path.join(projectFolder, 'node_modules', HEFT_PACKAGE_NAME); + const heftFolder: string = path.join(projectFolder, 'node_modules', Constants.heftPackageName); heftEntryPoint = path.join(heftFolder, 'lib', 'start.js'); if (!fs.existsSync(heftEntryPoint)) { diff --git a/apps/heft/src/utilities/CliUtilities.ts b/apps/heft/src/utilities/CliUtilities.ts new file mode 100644 index 00000000000..d322dc45e1f --- /dev/null +++ b/apps/heft/src/utilities/CliUtilities.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * Parse the arguments to the tool being executed and return the tool argument names. + * + * @param argv - The arguments to parse. Defaults to `process.argv`. + */ +export function getToolParameterNamesFromArgs(argv: string[] = process.argv): Set { + const toolParameters: Set = new Set(); + // Skip the first two arguments, which are the path to the Node executable and the path to the Heft + // entrypoint. The remaining arguments are the tool arguments. Grab them until we reach a non-"-"-prefixed + // argument. We can do this simple parsing because the Heft tool only has simple optional flags. + for (let i: number = 2; i < argv.length; ++i) { + const arg: string = argv[i]; + if (!arg.startsWith('-')) { + break; + } + toolParameters.add(arg); + } + return toolParameters; +} diff --git a/apps/heft/src/utilities/Constants.ts b/apps/heft/src/utilities/Constants.ts index dd2c6ad1eee..60e929519e6 100644 --- a/apps/heft/src/utilities/Constants.ts +++ b/apps/heft/src/utilities/Constants.ts @@ -14,26 +14,18 @@ export class Constants { public static cleanParameterLongName: string = '--clean'; - public static cleanCacheParameterLongName: string = '--clean-cache'; - public static debugParameterLongName: string = '--debug'; public static localesParameterLongName: string = '--locales'; public static onlyParameterLongName: string = '--only'; - public static onlyParameterShortName: string = '-o'; - public static productionParameterLongName: string = '--production'; public static toParameterLongName: string = '--to'; - public static toParameterShortName: string = '-t'; - public static toExceptParameterLongName: string = '--to-except'; - public static toExceptParameterShortName: string = '-T'; - public static unmanagedParameterLongName: string = '--unmanaged'; public static verboseParameterLongName: string = '--verbose'; @@ -41,4 +33,6 @@ export class Constants { public static verboseParameterShortName: string = '-v'; public static maxParallelism: number = 100; + + public static heftPackageName: string = '@rushstack/heft'; } diff --git a/apps/heft/src/utilities/CoreConfigFiles.ts b/apps/heft/src/utilities/CoreConfigFiles.ts index 55aafe0b9a2..3ad45c6fda9 100644 --- a/apps/heft/src/utilities/CoreConfigFiles.ts +++ b/apps/heft/src/utilities/CoreConfigFiles.ts @@ -1,25 +1,29 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; +import * as path from 'node:path'; + import { - ConfigurationFile, - IJsonPathMetadataResolverOptions, + ProjectConfigurationFile, InheritanceType, - PathResolutionMethod + PathResolutionMethod, + type IJsonPathMetadataResolverOptions } from '@rushstack/heft-config-file'; -import { Import, ITerminal } from '@rushstack/node-core-library'; -import type { RigConfig } from '@rushstack/rig-package'; +import { Import, PackageJsonLookup, InternalError } from '@rushstack/node-core-library'; +import type { ITerminal } from '@rushstack/terminal'; +import type { IRigConfig } from '@rushstack/rig-package'; import type { IDeleteOperation } from '../plugins/DeleteFilesPlugin'; import type { INodeServicePluginConfiguration } from '../plugins/NodeServicePlugin'; import { Constants } from './Constants'; -export type HeftEventKind = 'copyFiles' | 'deleteFiles' | 'runScript' | 'nodeService'; +export interface IHeftConfigurationJsonActionReference { + actionName: string; + defaultParameters?: string[]; +} -export interface IHeftConfigurationJsonEventSpecifier { - eventKind: HeftEventKind; - options?: object; +export interface IHeftConfigurationJsonAliases { + [aliasName: string]: IHeftConfigurationJsonActionReference; } export interface IHeftConfigurationJsonPluginSpecifier { @@ -31,8 +35,7 @@ export interface IHeftConfigurationJsonPluginSpecifier { export interface IHeftConfigurationJsonTaskSpecifier { taskDependencies?: string[]; - taskEvent?: IHeftConfigurationJsonEventSpecifier; - taskPlugin?: IHeftConfigurationJsonPluginSpecifier; + taskPlugin: IHeftConfigurationJsonPluginSpecifier; } export interface IHeftConfigurationJsonTasks { @@ -52,39 +55,65 @@ export interface IHeftConfigurationJsonPhases { export interface IHeftConfigurationJson { heftPlugins?: IHeftConfigurationJsonPluginSpecifier[]; + aliasesByName?: IHeftConfigurationJsonAliases; phasesByName?: IHeftConfigurationJsonPhases; } export class CoreConfigFiles { - private static _heftConfigFileLoader: ConfigurationFile | undefined; + private static _heftConfigFileLoader: ProjectConfigurationFile | undefined; private static _nodeServiceConfigurationLoader: - | ConfigurationFile + | ProjectConfigurationFile | undefined; + public static heftConfigurationProjectRelativeFilePath: string = `${Constants.projectConfigFolderName}/${Constants.heftConfigurationFilename}`; + + public static nodeServiceConfigurationProjectRelativeFilePath: string = `${Constants.projectConfigFolderName}/${Constants.nodeServiceConfigurationFilename}`; + /** * Returns the loader for the `config/heft.json` config file. */ public static async loadHeftConfigurationFileForProjectAsync( terminal: ITerminal, projectPath: string, - rigConfig?: RigConfig | undefined + rigConfig?: IRigConfig | undefined ): Promise { if (!CoreConfigFiles._heftConfigFileLoader) { + let heftPluginPackageFolder: string | undefined; + const pluginPackageResolver: ( options: IJsonPathMetadataResolverOptions ) => string = (options: IJsonPathMetadataResolverOptions) => { const { propertyValue, configurationFilePath } = options; - const configurationFileDirectory: string = path.dirname(configurationFilePath); - return Import.resolvePackage({ - packageName: propertyValue, - baseFolderPath: configurationFileDirectory - }); + if (propertyValue === Constants.heftPackageName) { + // If the value is "@rushstack/heft", then resolve to the Heft package that is + // installed in the project folder. This avoids issues with mismatched versions + // between the project and the globally installed Heft. Use the PackageJsonLookup + // class to find the package folder to avoid hardcoding the path for compatibility + // with bundling. + if (!heftPluginPackageFolder) { + heftPluginPackageFolder = PackageJsonLookup.instance.tryGetPackageFolderFor(__dirname); + } + + if (!heftPluginPackageFolder) { + // This should never happen + throw new InternalError('Unable to find the @rushstack/heft package folder'); + } + + return heftPluginPackageFolder; + } else { + const configurationFileDirectory: string = path.dirname(configurationFilePath); + return Import.resolvePackage({ + packageName: propertyValue, + baseFolderPath: configurationFileDirectory, + allowSelfReference: true + }); + } }; - const schemaPath: string = path.join(__dirname, '..', 'schemas', 'heft.schema.json'); - CoreConfigFiles._heftConfigFileLoader = new ConfigurationFile({ - projectRelativeFilePath: `${Constants.projectConfigFolderName}/${Constants.heftConfigurationFilename}`, - jsonSchemaPath: schemaPath, + const schemaObject: object = await import('../schemas/heft.schema.json'); + CoreConfigFiles._heftConfigFileLoader = new ProjectConfigurationFile({ + projectRelativeFilePath: CoreConfigFiles.heftConfigurationProjectRelativeFilePath, + jsonSchemaObject: schemaObject, propertyInheritanceDefaults: { array: { inheritanceType: InheritanceType.append }, object: { inheritanceType: InheritanceType.merge } @@ -101,58 +130,122 @@ export class CoreConfigFiles { '$.phasesByName.*.tasksByName.*.taskPlugin.pluginPackage': { pathResolutionMethod: PathResolutionMethod.custom, customResolver: pluginPackageResolver - }, - // Special handling for "runScript" task events to resolve the script path - '$.phasesByName.*.tasksByName[?(@.taskEvent && @.taskEvent.eventKind == "runScript")].taskEvent.options.scriptPath': - { - pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToProjectRoot - } + } } }); } - const configurationFile: IHeftConfigurationJson = - await CoreConfigFiles._heftConfigFileLoader.loadConfigurationFileForProjectAsync( + const heftConfigFileLoader: ProjectConfigurationFile = + CoreConfigFiles._heftConfigFileLoader; + + let configurationFile: IHeftConfigurationJson; + try { + configurationFile = await heftConfigFileLoader.loadConfigurationFileForProjectAsync( terminal, projectPath, rigConfig ); + } catch (e: unknown) { + if ( + !(e instanceof Error) || + !e.message.startsWith('Resolved configuration object does not match schema') + ) { + throw e; + } - // The pluginPackage field was resolved to the root of the package, but we also want to have - // the original plugin package name in the config file. Gather all the plugin specifiers so we can - // add the original data ourselves. - const pluginSpecifiers: IHeftConfigurationJsonPluginSpecifier[] = [ - ...(configurationFile.heftPlugins || []) - ]; - for (const { tasksByName } of Object.values(configurationFile.phasesByName || {})) { - for (const { taskPlugin } of Object.values(tasksByName || {})) { - if (taskPlugin) { - pluginSpecifiers.push(taskPlugin); - } + try { + // If the config file doesn't match the schema, then we should check to see if it does + // match the legacy schema. We don't need to worry about the resulting object, we just + // want to see if it parses. We will use the ConfigurationFile class to load it to ensure + // that we follow the "extends" chain for the entire config file. + const legacySchemaObject: object = await import('../schemas/heft-legacy.schema.json'); + const legacyConfigFileLoader: ProjectConfigurationFile = + new ProjectConfigurationFile({ + projectRelativeFilePath: CoreConfigFiles.heftConfigurationProjectRelativeFilePath, + jsonSchemaObject: legacySchemaObject + }); + await legacyConfigFileLoader.loadConfigurationFileForProjectAsync(terminal, projectPath, rigConfig); + } catch (e2) { + // It doesn't match the legacy schema either. Throw the original error. + throw e; } + // Matches the legacy schema, so throw a more helpful error. + throw new Error( + "This project's Heft configuration appears to be using an outdated schema.\n\n" + + 'Heft 0.51.0 introduced a major breaking change for Heft configuration files. ' + + 'Your project appears to be using the older file format. You will need to ' + + 'migrate your project to the new format. Follow these instructions: ' + + 'https://rushstack.io/link/heft-0.51' + ); } - for (const pluginSpecifier of pluginSpecifiers) { - const pluginPackageName: string = CoreConfigFiles._heftConfigFileLoader.getPropertyOriginalValue({ - parentObject: pluginSpecifier, + // The pluginPackage field was resolved to the root of the package, but we also want to have + // the original plugin package name in the config file. + function getUpdatedPluginSpecifier( + rawSpecifier: IHeftConfigurationJsonPluginSpecifier + ): IHeftConfigurationJsonPluginSpecifier { + const pluginPackageName: string = heftConfigFileLoader.getPropertyOriginalValue({ + parentObject: rawSpecifier, propertyName: 'pluginPackage' })!; - pluginSpecifier.pluginPackageRoot = pluginSpecifier.pluginPackage; - pluginSpecifier.pluginPackage = pluginPackageName; + const newSpecifier: IHeftConfigurationJsonPluginSpecifier = { + ...rawSpecifier, + pluginPackageRoot: rawSpecifier.pluginPackage, + pluginPackage: pluginPackageName + }; + return newSpecifier; } - return configurationFile; + const phasesByName: IHeftConfigurationJsonPhases = {}; + + const normalizedConfigurationFile: IHeftConfigurationJson = { + ...configurationFile, + heftPlugins: configurationFile.heftPlugins?.map(getUpdatedPluginSpecifier) ?? [], + phasesByName + }; + + for (const [phaseName, phase] of Object.entries(configurationFile.phasesByName || {})) { + const tasksByName: IHeftConfigurationJsonTasks = {}; + phasesByName[phaseName] = { + ...phase, + tasksByName + }; + + for (const [taskName, task] of Object.entries(phase.tasksByName || {})) { + if (task.taskPlugin) { + tasksByName[taskName] = { + ...task, + taskPlugin: getUpdatedPluginSpecifier(task.taskPlugin) + }; + } else { + tasksByName[taskName] = task; + } + } + } + + return normalizedConfigurationFile; } - public static get nodeServiceConfigurationFile(): ConfigurationFile { + public static async tryLoadNodeServiceConfigurationFileAsync( + terminal: ITerminal, + projectPath: string, + rigConfig?: IRigConfig | undefined + ): Promise { if (!CoreConfigFiles._nodeServiceConfigurationLoader) { - const schemaPath: string = path.resolve(__dirname, '..', 'schemas', 'node-service.schema.json'); + const schemaObject: object = await import('../schemas/node-service.schema.json'); CoreConfigFiles._nodeServiceConfigurationLoader = - new ConfigurationFile({ - projectRelativeFilePath: `${Constants.projectConfigFolderName}/${Constants.nodeServiceConfigurationFilename}`, - jsonSchemaPath: schemaPath + new ProjectConfigurationFile({ + projectRelativeFilePath: CoreConfigFiles.nodeServiceConfigurationProjectRelativeFilePath, + jsonSchemaObject: schemaObject }); } - return CoreConfigFiles._nodeServiceConfigurationLoader; + + const configurationFile: INodeServicePluginConfiguration | undefined = + await CoreConfigFiles._nodeServiceConfigurationLoader.tryLoadConfigurationFileForProjectAsync( + terminal, + projectPath, + rigConfig + ); + return configurationFile; } } diff --git a/apps/heft/src/utilities/GitUtilities.ts b/apps/heft/src/utilities/GitUtilities.ts index 6e8898f459c..a0f918b1242 100644 --- a/apps/heft/src/utilities/GitUtilities.ts +++ b/apps/heft/src/utilities/GitUtilities.ts @@ -1,13 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; -import { ChildProcess, SpawnSyncReturns } from 'child_process'; -import { default as getGitRepoInfo, GitRepoInfo as IGitRepoInfo } from 'git-repo-info'; -import { Executable, FileSystem, InternalError, Path } from '@rushstack/node-core-library'; +import * as path from 'node:path'; +import type { ChildProcess, SpawnSyncReturns } from 'node:child_process'; + +import { default as getGitRepoInfo, type GitRepoInfo as IGitRepoInfo } from 'git-repo-info'; import { default as ignore, type Ignore as IIgnoreMatcher } from 'ignore'; -// Matches lines starting with "#" and whitepace lines +import { Executable, FileSystem, InternalError, Path, Text } from '@rushstack/node-core-library'; + +// Matches lines starting with "#" and whitespace lines const GITIGNORE_IGNORABLE_LINE_REGEX: RegExp = /^(?:(?:#.*)|(?:\s+))$/; const UNINITIALIZED: 'UNINITIALIZED' = 'UNINITIALIZED'; @@ -188,9 +190,8 @@ export class GitUtilities { let currentPath: string = normalizedWorkingDirectory; while (currentPath.length >= gitRepoRootPath.length) { const gitIgnoreFilePath: string = `${currentPath}/.gitignore`; - const gitIgnorePatterns: string[] | undefined = await this._tryReadGitIgnoreFileAsync( - gitIgnoreFilePath - ); + const gitIgnorePatterns: string[] | undefined = + await this._tryReadGitIgnoreFileAsync(gitIgnoreFilePath); if (gitIgnorePatterns) { rawIgnorePatternsByGitignoreFolder.set(currentPath, gitIgnorePatterns); } @@ -201,9 +202,8 @@ export class GitUtilities { const gitignoreRelativeFilePaths: string[] = await this._findUnignoredFilesAsync('*.gitignore'); for (const gitignoreRelativeFilePath of gitignoreRelativeFilePaths) { const gitignoreFilePath: string = `${normalizedWorkingDirectory}/${gitignoreRelativeFilePath}`; - const gitIgnorePatterns: string[] | undefined = await this._tryReadGitIgnoreFileAsync( - gitignoreFilePath - ); + const gitIgnorePatterns: string[] | undefined = + await this._tryReadGitIgnoreFileAsync(gitignoreFilePath); if (gitIgnorePatterns) { const parentPath: string = gitignoreFilePath.slice(0, gitignoreFilePath.lastIndexOf('/')); rawIgnorePatternsByGitignoreFolder.set(parentPath, gitIgnorePatterns); @@ -284,7 +284,7 @@ export class GitUtilities { const foundIgnorePatterns: string[] = []; if (gitIgnoreContent) { - const gitIgnorePatterns: string[] = gitIgnoreContent.split(/\r?\n/g); + const gitIgnorePatterns: string[] = Text.splitByNewLines(gitIgnoreContent); for (const gitIgnorePattern of gitIgnorePatterns) { // Ignore whitespace-only lines and comments if (gitIgnorePattern.length === 0 || GITIGNORE_IGNORABLE_LINE_REGEX.test(gitIgnorePattern)) { @@ -347,11 +347,13 @@ export class GitUtilities { childProcess.stderr!.on('data', (chunk: Buffer) => { errorMessage += chunk.toString(); }); - childProcess.on('close', (exitCode: number) => { - if (exitCode !== 0) { + childProcess.on('close', (exitCode: number | null, signal: NodeJS.Signals | null) => { + if (exitCode) { reject( new Error(`git exited with error code ${exitCode}${errorMessage ? `: ${errorMessage}` : ''}`) ); + } else if (signal) { + reject(new Error(`git terminated by signal ${signal}`)); } let remainder: string = ''; for (let chunk of stdoutBuffer) { diff --git a/apps/heft/src/utilities/WatchFileSystemAdapter.ts b/apps/heft/src/utilities/WatchFileSystemAdapter.ts index d5d548f75de..97c1ae5bdd3 100644 --- a/apps/heft/src/utilities/WatchFileSystemAdapter.ts +++ b/apps/heft/src/utilities/WatchFileSystemAdapter.ts @@ -1,19 +1,38 @@ -import * as fs from 'fs'; -import * as path from 'path'; +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as fs from 'node:fs'; +import * as path from 'node:path'; -import type { ReaddirAsynchronousMethod, ReaddirSynchronousMethod } from '@nodelib/fs.scandir'; -import type { StatAsynchronousMethod, StatSynchronousMethod } from '@nodelib/fs.stat'; -import type { FileSystemAdapter } from 'fast-glob'; import Watchpack from 'watchpack'; -interface IReaddirOptions { +/** + * Options for `fs.readdir` + * @public + */ +export interface IReaddirOptions { + /** + * If true, readdir will return `fs.Dirent` objects instead of strings. + */ withFileTypes: true; } /* eslint-disable @rushstack/no-new-null */ -type StatCallback = (error: NodeJS.ErrnoException | null, stats: fs.Stats) => void; -type ReaddirStringCallback = (error: NodeJS.ErrnoException | null, files: string[]) => void; -type ReaddirDirentCallback = (error: NodeJS.ErrnoException | null, files: fs.Dirent[]) => void; +/** + * Callback for `fs.stat` and `fs.lstat` + * @public + */ +export type StatCallback = (error: NodeJS.ErrnoException | null, stats: fs.Stats) => void; +/** + * Callback for `fs.readdir` when `withFileTypes` is not specified or false + * @public + */ +export type ReaddirStringCallback = (error: NodeJS.ErrnoException | null, files: string[]) => void; +/** + * Callback for `fs.readdir` when `withFileTypes` is true + * @public + */ +export type ReaddirDirentCallback = (error: NodeJS.ErrnoException | null, files: fs.Dirent[]) => void; /* eslint-enable @rushstack/no-new-null */ /** @@ -27,13 +46,73 @@ export interface IWatchedFileState { changed: boolean; } +/** + * Interface contract for heft plugins to use the `WatchFileSystemAdapter` + * @public + */ +export interface IWatchFileSystem { + /** + * Synchronous readdir. Watches the directory for changes. + * + * @see fs.readdirSync + */ + readdirSync(filePath: string): string[]; + readdirSync(filePath: string, options: IReaddirOptions): fs.Dirent[]; + + /** + * Asynchronous readdir. Watches the directory for changes. + * + * @see fs.readdir + */ + readdir(filePath: string, callback: ReaddirStringCallback): void; + readdir(filePath: string, options: IReaddirOptions, callback: ReaddirDirentCallback): void; + + /** + * Asynchronous lstat. Watches the file for changes, or if it does not exist, watches to see if it is created. + * @see fs.lstat + */ + lstat(filePath: string, callback: StatCallback): void; + + /** + * Synchronous lstat. Watches the file for changes, or if it does not exist, watches to see if it is created. + * @see fs.lstatSync + */ + lstatSync(filePath: string): fs.Stats; + + /** + * Asynchronous stat. Watches the file for changes, or if it does not exist, watches to see if it is created. + * @see fs.stat + */ + stat(filePath: string, callback: StatCallback): void; + + /** + * Synchronous stat. Watches the file for changes, or if it does not exist, watches to see if it is created. + * @see fs.statSync + */ + statSync(filePath: string): fs.Stats; + + /** + * Tells the adapter to track the specified file (or folder) as used. + * Returns an object containing data about the state of said file (or folder). + * Uses promise-based API. + */ + getStateAndTrackAsync(filePath: string): Promise; + + /** + * Tells the adapter to track the specified file (or folder) as used. + * Returns an object containing data about the state of said file (or folder). + * Uses synchronous API. + */ + getStateAndTrack(filePath: string): IWatchedFileState; +} + /** * Interface contract for `WatchFileSystemAdapter` for cross-version compatibility */ -export interface IWatchFileSystemAdapter extends FileSystemAdapter { +export interface IWatchFileSystemAdapter extends IWatchFileSystem { /** * Prepares for incoming glob requests. Any file changed after this method is called - * will trigger teh watch callback in the next invocation. + * will trigger the watch callback in the next invocation. * File mtimes will be recorded at this time for any files that do not receive explicit * stat() or lstat() calls. */ @@ -43,11 +122,6 @@ export interface IWatchFileSystemAdapter extends FileSystemAdapter { * Clears the tracked file lists. */ watch(onChange: () => void): void; - /** - * Tells the adapter to track the specified file (or folder) as used. - * Returns an object containing data about the state of said file (or folder). - */ - getStateAndTrackAsync(filePath: string): Promise; } interface ITimeEntry { @@ -71,7 +145,10 @@ export class WatchFileSystemAdapter implements IWatchFileSystemAdapter { private _times: Map | undefined; /** { @inheritdoc fs.readdirSync } */ - public readdirSync: ReaddirSynchronousMethod = ((filePath: string, options?: IReaddirOptions) => { + public readdirSync: IWatchFileSystemAdapter['readdirSync'] = (( + filePath: string, + options?: IReaddirOptions + ) => { filePath = path.normalize(filePath); try { @@ -88,10 +165,10 @@ export class WatchFileSystemAdapter implements IWatchFileSystemAdapter { this._missing.set(filePath, Date.now()); throw err; } - }) as ReaddirSynchronousMethod; + }) as IWatchFileSystemAdapter['readdirSync']; /** { @inheritdoc fs.readdir } */ - public readdir: ReaddirAsynchronousMethod = ( + public readdir: IWatchFileSystemAdapter['readdir'] = ( filePath: string, optionsOrCallback: IReaddirOptions | ReaddirStringCallback, callback?: ReaddirDirentCallback | ReaddirStringCallback @@ -127,7 +204,7 @@ export class WatchFileSystemAdapter implements IWatchFileSystemAdapter { }; /** { @inheritdoc fs.lstat } */ - public lstat: StatAsynchronousMethod = (filePath: string, callback: StatCallback): void => { + public lstat: IWatchFileSystemAdapter['lstat'] = (filePath: string, callback: StatCallback): void => { filePath = path.normalize(filePath); fs.lstat(filePath, (err: NodeJS.ErrnoException | null, stats: fs.Stats) => { if (err) { @@ -140,7 +217,7 @@ export class WatchFileSystemAdapter implements IWatchFileSystemAdapter { }; /** { @inheritdoc fs.lstatSync } */ - public lstatSync: StatSynchronousMethod = (filePath: string): fs.Stats => { + public lstatSync: IWatchFileSystemAdapter['lstatSync'] = (filePath: string): fs.Stats => { filePath = path.normalize(filePath); try { const stats: fs.Stats = fs.lstatSync(filePath); @@ -153,7 +230,7 @@ export class WatchFileSystemAdapter implements IWatchFileSystemAdapter { }; /** { @inheritdoc fs.stat } */ - public stat: StatAsynchronousMethod = (filePath: string, callback: StatCallback): void => { + public stat: IWatchFileSystemAdapter['stat'] = (filePath: string, callback: StatCallback): void => { filePath = path.normalize(filePath); fs.stat(filePath, (err: NodeJS.ErrnoException | null, stats: fs.Stats) => { if (err) { @@ -166,7 +243,7 @@ export class WatchFileSystemAdapter implements IWatchFileSystemAdapter { }; /** { @inheritdoc fs.statSync } */ - public statSync: StatSynchronousMethod = (filePath: string) => { + public statSync: IWatchFileSystemAdapter['statSync'] = (filePath: string) => { filePath = path.normalize(filePath); try { const stats: fs.Stats = fs.statSync(filePath); @@ -254,4 +331,38 @@ export class WatchFileSystemAdapter implements IWatchFileSystemAdapter { changed: newTime !== oldTime }; } + + /** + * @inheritdoc + */ + public getStateAndTrack(filePath: string): IWatchedFileState { + const normalizedSourcePath: string = path.normalize(filePath); + const oldTime: number | undefined = this._lastFiles?.get(normalizedSourcePath); + let newTimeEntry: ITimeEntry | undefined = this._times?.get(normalizedSourcePath); + + if (!newTimeEntry) { + // Need to record a timestamp, otherwise first rerun will select everything + const stats: fs.Stats | undefined = fs.lstatSync(normalizedSourcePath, { throwIfNoEntry: false }); + if (stats) { + const rounded: number = stats.mtime.getTime() || stats.ctime.getTime() || Date.now(); + newTimeEntry = { + timestamp: rounded, + safeTime: rounded + }; + } else { + this._missing.set(normalizedSourcePath, Date.now()); + } + } + + const newTime: number | undefined = + (newTimeEntry && (newTimeEntry.timestamp ?? newTimeEntry.safeTime)) || this._lastQueryTime; + + if (newTime) { + this._files.set(normalizedSourcePath, newTime); + } + + return { + changed: newTime !== oldTime + }; + } } diff --git a/apps/heft/src/utilities/test/GitUtilities.test.ts b/apps/heft/src/utilities/test/GitUtilities.test.ts index 82d9eec7d35..9b51b6d1f1d 100644 --- a/apps/heft/src/utilities/test/GitUtilities.test.ts +++ b/apps/heft/src/utilities/test/GitUtilities.test.ts @@ -1,9 +1,15 @@ -import * as path from 'path'; +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as path from 'node:path'; import { GitUtilities, type GitignoreFilterFn } from '../GitUtilities'; +import { PackageJsonLookup } from '@rushstack/node-core-library'; describe('GitUtilities', () => { describe('checkIgnoreAsync', () => { - const testFoldersBasePath: string = path.join(__dirname, 'checkIgnoreTests'); + const projectRoot: string = PackageJsonLookup.instance.tryGetPackageFolderFor(__dirname)!; + + const testFoldersBasePath: string = `${projectRoot}/src/utilities/test/checkIgnoreTests`; it('returns all files are ignored', async () => { const testFolderPath: string = path.join(testFoldersBasePath, 'allIgnored'); @@ -37,13 +43,12 @@ describe('GitUtilities', () => { it('returns ignored files specified in the repo gitignore', async () => { // /apps/heft - const testFolderPath: string = path.resolve(__dirname, '..', '..', '..'); - const git = new GitUtilities(testFolderPath); + const git = new GitUtilities(projectRoot); const isUnignoredAsync: GitignoreFilterFn = (await git.tryCreateGitignoreFilterAsync())!; - expect(await isUnignoredAsync(path.join(testFolderPath, 'lib', 'a.txt'))).toEqual(false); - expect(await isUnignoredAsync(path.join(testFolderPath, 'temp', 'a.txt'))).toEqual(false); - expect(await isUnignoredAsync(path.join(testFolderPath, 'dist', 'a.txt'))).toEqual(false); - expect(await isUnignoredAsync(path.join(testFolderPath, 'src', 'a.txt'))).toEqual(true); + expect(await isUnignoredAsync(path.join(projectRoot, 'lib', 'a.txt'))).toEqual(false); + expect(await isUnignoredAsync(path.join(projectRoot, 'temp', 'a.txt'))).toEqual(false); + expect(await isUnignoredAsync(path.join(projectRoot, 'dist', 'a.txt'))).toEqual(false); + expect(await isUnignoredAsync(path.join(projectRoot, 'src', 'a.txt'))).toEqual(true); const ignoredFolderPath: string = path.join(testFoldersBasePath, 'allIgnored'); expect(await isUnignoredAsync(path.join(ignoredFolderPath, 'a.txt'))).toEqual(false); diff --git a/apps/heft/tsconfig.json b/apps/heft/tsconfig.json index da04e9fff9d..1a33d17b873 100644 --- a/apps/heft/tsconfig.json +++ b/apps/heft/tsconfig.json @@ -1,8 +1,3 @@ { - "extends": "./node_modules/@rushstack/heft-node-rig/profiles/default/tsconfig-base.json", - - "compilerOptions": { - "types": ["heft-jest", "node"], - "lib": ["ES2020"] - } + "extends": "./node_modules/decoupled-local-node-rig/profiles/default/tsconfig-base.json" } diff --git a/apps/lockfile-explorer-web/.eslintrc.js b/apps/lockfile-explorer-web/.eslintrc.js deleted file mode 100644 index 288eaa16364..00000000000 --- a/apps/lockfile-explorer-web/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -// This is a workaround for https://github.com/eslint/eslint/issues/3458 -require('@rushstack/eslint-config/patch/modern-module-resolution'); - -module.exports = { - extends: ['@rushstack/eslint-config/profile/web-app', '@rushstack/eslint-config/mixins/react'], - parserOptions: { tsconfigRootDir: __dirname } -}; diff --git a/apps/lockfile-explorer-web/assets/index.html b/apps/lockfile-explorer-web/assets/index.html index d31e23960e4..1c5d6f471a0 100644 --- a/apps/lockfile-explorer-web/assets/index.html +++ b/apps/lockfile-explorer-web/assets/index.html @@ -1,4 +1,4 @@ - + diff --git a/apps/lockfile-explorer-web/config/heft.json b/apps/lockfile-explorer-web/config/heft.json index 7204e0560b5..e031c98a5c5 100644 --- a/apps/lockfile-explorer-web/config/heft.json +++ b/apps/lockfile-explorer-web/config/heft.json @@ -2,21 +2,24 @@ * Defines configuration used by core Heft. */ { - "$schema": "https://developer.microsoft.com/json-schemas/heft/heft.schema.json", + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", /** * Optionally specifies another JSON config file that this file extends from. This provides a way for standard * settings to be shared across multiple projects. + * + * To delete an inherited setting, set it to `null` in this file. */ - "extends": "@rushstack/heft-web-rig/profiles/app/config/heft.json", + "extends": "local-web-rig/profiles/app/config/heft.json", "phasesByName": { "build": { "tasksByName": { "copy-stub": { "taskDependencies": ["typescript"], - "taskEvent": { - "eventKind": "copyFiles", + "taskPlugin": { + "pluginPackage": "@rushstack/heft", + "pluginName": "copy-files-plugin", "options": { "copyOperations": [ { diff --git a/apps/lockfile-explorer-web/config/jest.config.json b/apps/lockfile-explorer-web/config/jest.config.json index 7f2f5dc42b6..dd440826f6c 100644 --- a/apps/lockfile-explorer-web/config/jest.config.json +++ b/apps/lockfile-explorer-web/config/jest.config.json @@ -1,5 +1,5 @@ { - "extends": "@rushstack/heft-web-rig/profiles/app/config/jest.config.json", + "extends": "local-web-rig/profiles/app/config/jest.config.json", // Load the initappcontext.js stub when running tests "setupFiles": ["../lib-commonjs/stub/initappcontext.js"] diff --git a/apps/lockfile-explorer-web/config/rig.json b/apps/lockfile-explorer-web/config/rig.json index 687fc2911bc..26f617ab3fc 100644 --- a/apps/lockfile-explorer-web/config/rig.json +++ b/apps/lockfile-explorer-web/config/rig.json @@ -1,6 +1,6 @@ { "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", - "rigPackageName": "@rushstack/heft-web-rig", + "rigPackageName": "local-web-rig", "rigProfile": "app" } diff --git a/apps/lockfile-explorer-web/eslint.config.js b/apps/lockfile-explorer-web/eslint.config.js new file mode 100644 index 00000000000..9765c392aa3 --- /dev/null +++ b/apps/lockfile-explorer-web/eslint.config.js @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +const webAppProfile = require('local-web-rig/profiles/app/includes/eslint/flat/profile/web-app'); +const reactMixin = require('local-web-rig/profiles/app/includes/eslint/flat/mixins/react'); +const packletsMixin = require('local-web-rig/profiles/app/includes/eslint/flat/mixins/packlets'); + +module.exports = [ + ...webAppProfile, + ...reactMixin, + packletsMixin, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + tsconfigRootDir: __dirname + } + } + } +]; diff --git a/apps/lockfile-explorer-web/package.json b/apps/lockfile-explorer-web/package.json index 1f541497761..7eeabe34cc8 100644 --- a/apps/lockfile-explorer-web/package.json +++ b/apps/lockfile-explorer-web/package.json @@ -6,28 +6,27 @@ "license": "MIT", "scripts": { "build": "heft test --clean", - "start": "heft build-watch", + "start": "heft start", "test": "heft test", "_phase:build": "heft run --only build -- --clean", "_phase:test": "heft run --only test -- --clean" }, "dependencies": { - "@fluentui/react": "^8.96.1", - "react": "~16.13.1", - "react-dom": "~16.13.1", - "@lifaon/path": "~2.1.0", - "@reduxjs/toolkit": "~1.8.6", - "react-redux": "~8.0.4", - "redux": "~4.2.0", - "@rushstack/rush-themed-ui": "workspace:*" + "@reduxjs/toolkit": "~2.11.2", + "@rushstack/rush-themed-ui": "workspace:*", + "prism-react-renderer": "~2.4.1", + "react-dom": "~19.2.3", + "react-redux": "~9.2.0", + "react": "~19.2.3", + "redux": "~5.0.1", + "tslib": "~2.8.1" }, "devDependencies": { - "@rushstack/eslint-config": "workspace:*", - "@rushstack/heft-web-rig": "workspace:*", "@rushstack/heft": "workspace:*", - "@types/heft-jest": "1.0.1", - "@types/react-dom": "16.9.14", - "@types/react": "16.14.23", - "@types/webpack-env": "1.18.0" + "@types/react": "19.2.7", + "@types/react-dom": "19.2.3", + "eslint": "~9.37.0", + "local-web-rig": "workspace:*", + "typescript": "5.8.2" } } diff --git a/apps/lockfile-explorer-web/src/App.tsx b/apps/lockfile-explorer-web/src/App.tsx index e0d110a2c62..243b65c52db 100644 --- a/apps/lockfile-explorer-web/src/App.tsx +++ b/apps/lockfile-explorer-web/src/App.tsx @@ -2,6 +2,7 @@ // See LICENSE in the project root for license information. import React, { useEffect } from 'react'; + import styles from './App.scss'; import { readLockfileAsync } from './parsing/readLockfile'; import { LockfileViewer } from './containers/LockfileViewer'; @@ -17,7 +18,7 @@ import { ConnectionModal } from './components/ConnectionModal'; /** * This React component renders the application page. */ -export const App = (): JSX.Element => { +export const App = (): React.ReactElement => { const dispatch = useAppDispatch(); useEffect(() => { @@ -26,9 +27,10 @@ export const App = (): JSX.Element => { dispatch(loadEntries(lockfile)); } loadLockfileAsync().catch((e) => { + // eslint-disable-next-line no-console console.log(`Failed to read lockfile: ${e}`); }); - }, []); + }, [dispatch]); return ( <> diff --git a/apps/lockfile-explorer-web/src/AppContext.ts b/apps/lockfile-explorer-web/src/AppContext.ts deleted file mode 100644 index 61417a22fbe..00000000000 --- a/apps/lockfile-explorer-web/src/AppContext.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Describes the `window.appContext` object that the Node.js service uses - * to communicate runtime configuration to the web app. - * - * @remarks - * The `dist/index.html` page loads a script `initappcontext.js` to initialize - * this object before the web app starts. - * - * When the app is hosted by Webpack dev server, this is implemented by - * `lockfile-explorer-web/src/stub/initappcontext.ts`. - * - * When the app is hosted by the CLI front end, the `initappcontext.js` content - * is generated by an Express route. - */ -export interface IAppContext { - /** - * The service URL, without the trailing slash. - * - * @example - * Example: `http://localhost:8091` - */ - serviceUrl: string; - - /** - * The `package.json` version for the app. - */ - appVersion: string; - - /** - * Whether the CLI was invoked with the `--debug` parameter. - */ - debugMode: boolean; -} - -declare global { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface Window { - appContext: IAppContext; - } -} diff --git a/apps/lockfile-explorer-web/src/components/ConnectionModal/index.tsx b/apps/lockfile-explorer-web/src/components/ConnectionModal/index.tsx index f946b60558e..afc205df90d 100644 --- a/apps/lockfile-explorer-web/src/components/ConnectionModal/index.tsx +++ b/apps/lockfile-explorer-web/src/components/ConnectionModal/index.tsx @@ -2,13 +2,15 @@ // See LICENSE in the project root for license information. import React, { useCallback, useEffect, useState } from 'react'; + import { Button, Text } from '@rushstack/rush-themed-ui'; + import styles from './styles.scss'; import appStyles from '../../App.scss'; -import { checkAliveAsync } from '../../parsing/getPackageFiles'; -import { ReactNull } from '../../types/ReactNull'; +import { checkAliveAsync } from '../../helpers/lfxApiClient'; +import type { ReactNull } from '../../types/ReactNull'; -export const ConnectionModal = (): JSX.Element | ReactNull => { +export const ConnectionModal = (): React.ReactElement | ReactNull => { const [isAlive, setIsAlive] = useState(true); const [checking, setChecking] = useState(false); const [manualChecked, setManualChecked] = useState(false); @@ -31,6 +33,7 @@ export const ConnectionModal = (): JSX.Element | ReactNull => { setManualChecked(true); keepAliveAsync().catch((e) => { // Keep alive cannot fail + // eslint-disable-next-line no-console console.error(`Unexpected exception: ${e}`); }); }, []); diff --git a/apps/lockfile-explorer-web/src/containers/BookmarksSidebar/index.tsx b/apps/lockfile-explorer-web/src/containers/BookmarksSidebar/index.tsx index eebd3c54643..79e7bd3a3fc 100644 --- a/apps/lockfile-explorer-web/src/containers/BookmarksSidebar/index.tsx +++ b/apps/lockfile-explorer-web/src/containers/BookmarksSidebar/index.tsx @@ -2,28 +2,30 @@ // See LICENSE in the project root for license information. import React, { useCallback } from 'react'; + +import { Button, ScrollArea, Text } from '@rushstack/rush-themed-ui'; + import appStyles from '../../App.scss'; import styles from './styles.scss'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; -import { LockfileEntry } from '../../parsing/LockfileEntry'; +import type { LfxGraphEntry } from '../../packlets/lfx-shared'; import { clearStackAndPush, removeBookmark } from '../../store/slices/entrySlice'; -import { Button, ScrollArea, Text } from '@rushstack/rush-themed-ui'; -export const BookmarksSidebar = (): JSX.Element => { +export const BookmarksSidebar = (): React.ReactElement => { const bookmarks = useAppSelector((state) => state.entry.bookmarkedEntries); const dispatch = useAppDispatch(); const clear = useCallback( - (entry: LockfileEntry) => () => { + (entry: LfxGraphEntry) => () => { dispatch(clearStackAndPush(entry)); }, - [] + [dispatch] ); const deleteEntry = useCallback( - (entry: LockfileEntry) => () => { + (entry: LfxGraphEntry) => () => { dispatch(removeBookmark(entry)); }, - [] + [dispatch] ); return ( diff --git a/apps/lockfile-explorer-web/src/containers/LockfileEntryDetailsView/index.tsx b/apps/lockfile-explorer-web/src/containers/LockfileEntryDetailsView/index.tsx index 8eb049e2043..66bc2f2fc03 100644 --- a/apps/lockfile-explorer-web/src/containers/LockfileEntryDetailsView/index.tsx +++ b/apps/lockfile-explorer-web/src/containers/LockfileEntryDetailsView/index.tsx @@ -2,80 +2,110 @@ // See LICENSE in the project root for license information. import React, { useCallback, useEffect, useState } from 'react'; + import { ScrollArea, Text } from '@rushstack/rush-themed-ui'; + import styles from './styles.scss'; import appStyles from '../../App.scss'; -import { IDependencyType, LockfileDependency } from '../../parsing/LockfileDependency'; +import { LfxDependencyKind, type LfxGraphDependency, type LfxGraphEntry } from '../../packlets/lfx-shared'; +import { readPackageJsonAsync } from '../../helpers/lfxApiClient'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import { pushToStack, selectCurrentEntry } from '../../store/slices/entrySlice'; import { ReactNull } from '../../types/ReactNull'; -import { LockfileEntry } from '../../parsing/LockfileEntry'; import { logDiagnosticInfo } from '../../helpers/logDiagnosticInfo'; import { displaySpecChanges } from '../../helpers/displaySpecChanges'; +import type { IPackageJson } from '../../types/IPackageJson'; enum DependencyType { Determinant, TransitiveReferrer } +enum DependencyKey { + Regular = 'dependencies', + Dev = 'devDependencies', + Peer = 'peerDependencies' +} + interface IInfluencerType { - entry: LockfileEntry; + entry: LfxGraphEntry; type: DependencyType; } -export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => { +export const LockfileEntryDetailsView = (): React.ReactElement | ReactNull => { const selectedEntry = useAppSelector(selectCurrentEntry); const specChanges = useAppSelector((state) => state.workspace.specChanges); const dispatch = useAppDispatch(); - const [inspectDependency, setInspectDependency] = useState(null); + const [inspectDependency, setInspectDependency] = useState(null); const [influencers, setInfluencers] = useState([]); + const [directRefsPackageJSON, setDirectRefsPackageJSON] = useState>( + new Map() + ); useEffect(() => { + async function loadPackageJson(referrers: LfxGraphEntry[]): Promise { + const referrersJsonMap = new Map(); + await Promise.all( + referrers.map(async (ref) => { + const packageJson = await readPackageJsonAsync(ref.packageJsonFolderPath); + referrersJsonMap.set(ref.rawEntryId, packageJson); + return packageJson; + }) + ); + + setDirectRefsPackageJSON(referrersJsonMap); + } + + loadPackageJson(selectedEntry?.referrers || []).catch((e) => { + // eslint-disable-next-line no-console + console.error(`Failed to load referrers package.json: ${e}`); + }); if (selectedEntry) { setInspectDependency(null); } }, [selectedEntry]); const selectResolvedEntry = useCallback( - (dependencyToTrace) => () => { + (dependencyToTrace: LfxGraphDependency) => () => { if (inspectDependency && inspectDependency.entryId === dependencyToTrace.entryId) { if (dependencyToTrace.resolvedEntry) { dispatch(pushToStack(dependencyToTrace.resolvedEntry)); } else { - logDiagnosticInfo('No resolved entry for dependency:', dependencyToTrace); + logDiagnosticInfo('No resolved entry for dependency:', dependencyToTrace.entryId); } } else if (selectedEntry) { + // eslint-disable-next-line no-console console.log('dependency to trace: ', dependencyToTrace); setInspectDependency(dependencyToTrace); // Check if we need to calculate influencers. // If the current dependencyToTrace is a peer dependency then we do - if (dependencyToTrace.dependencyType !== IDependencyType.PEER_DEPENDENCY) { + if (dependencyToTrace.dependencyKind !== LfxDependencyKind.Peer) { return; } // calculate influencers const stack = [selectedEntry]; - const determinants = new Set(); - const transitiveReferrers = new Set(); - const visitedNodes = new Set(); + const determinants = new Set(); + const transitiveReferrers = new Set(); + const visitedNodes = new Set(); visitedNodes.add(selectedEntry); while (stack.length) { const currEntry = stack.pop(); if (currEntry) { - for (const referrer of currEntry.referrers) { + for (const referrer1 of currEntry.referrers) { let hasDependency = false; - for (const dependency of referrer.dependencies) { + for (const dependency of referrer1.dependencies) { if (dependency.name === dependencyToTrace.name) { - determinants.add(referrer); + determinants.add(referrer1); hasDependency = true; break; } } if (!hasDependency) { - if (referrer.transitivePeerDependencies.has(dependencyToTrace.name)) { - transitiveReferrers.add(referrer); + if (referrer1.transitivePeerDependencies.has(dependencyToTrace.name)) { + transitiveReferrers.add(referrer1); } else { // Since this referrer does not declare "dependency", it is a // transitive peer dependency, and we call the referrer a "transitive referrer". @@ -83,50 +113,54 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => { // YAML file. If not, either something is wrong with our algorithm, or else // something has changed about how PNPM manages its "transitivePeerDependencies" // field. + // eslint-disable-next-line no-console console.error( 'Error analyzing influencers: A referrer appears to be missing its "transitivePeerDependencies" field in the YAML file: ', dependencyToTrace, - referrer, + referrer1, currEntry ); } - for (const referrer of currEntry.referrers) { - if (!visitedNodes.has(referrer)) { - stack.push(referrer); - visitedNodes.add(referrer); + + for (const referrer2 of currEntry.referrers) { + if (!visitedNodes.has(referrer2)) { + stack.push(referrer2); + visitedNodes.add(referrer2); } } } } } } - const influencers: IInfluencerType[] = []; + const newInfluencers: IInfluencerType[] = []; for (const determinant of determinants.values()) { - influencers.push({ + newInfluencers.push({ entry: determinant, type: DependencyType.Determinant }); } for (const referrer of transitiveReferrers.values()) { - influencers.push({ + newInfluencers.push({ entry: referrer, type: DependencyType.TransitiveReferrer }); } - setInfluencers(influencers); + setInfluencers(newInfluencers); } }, + // eslint-disable-next-line react-hooks/exhaustive-deps [selectedEntry, inspectDependency] ); const selectResolvedReferencer = useCallback( - (referrer) => () => { + (referrer: LfxGraphEntry) => () => { dispatch(pushToStack(referrer)); }, + // eslint-disable-next-line react-hooks/exhaustive-deps [selectedEntry] ); - const renderDependencyMetadata = (): JSX.Element | ReactNull => { + const renderDependencyMetadata = (): React.ReactElement | ReactNull => { if (!inspectDependency) { return ReactNull; } @@ -138,7 +172,7 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => { Selected Dependency:{' '} - {inspectDependency.name}: {inspectDependency.version} + {inspectDependency.name}: {inspectDependency.versionPath}
@@ -146,11 +180,11 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => { package.json spec:{' '} - {inspectDependency.dependencyType === IDependencyType.PEER_DEPENDENCY + {inspectDependency.dependencyKind === LfxDependencyKind.Peer ? `"${inspectDependency.peerDependencyMeta.version}" ${ inspectDependency.peerDependencyMeta.optional ? 'Optional' : 'Required' } Peer` - : inspectDependency.version} + : inspectDependency.versionPath}
@@ -168,11 +202,9 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => { ); }; - const renderPeerDependencies = (): JSX.Element | ReactNull => { + const renderPeerDependencies = (): React.ReactElement | ReactNull => { if (!selectedEntry) return ReactNull; - const peerDeps = selectedEntry.dependencies.filter( - (d) => d.dependencyType === IDependencyType.PEER_DEPENDENCY - ); + const peerDeps = selectedEntry.dependencies.filter((d) => d.dependencyKind === LfxDependencyKind.Peer); if (!peerDeps.length) { return (
@@ -180,7 +212,7 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => {
); } - if (!inspectDependency || inspectDependency.dependencyType !== IDependencyType.PEER_DEPENDENCY) { + if (!inspectDependency || inspectDependency.dependencyKind !== LfxDependencyKind.Peer) { return (
Select a peer dependency to view its influencers @@ -231,6 +263,24 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => { ); }; + const getDependencyInfo = ( + rawEntryId: string, + entryPackageName: string + ): { type: DependencyKey; version: string } | undefined => { + const packageJson = directRefsPackageJSON.get(rawEntryId); + if (!packageJson) return undefined; + + const dependencyTypes = [DependencyKey.Regular, DependencyKey.Dev, DependencyKey.Peer]; + + for (const type of dependencyTypes) { + const version = packageJson[type]?.[entryPackageName]; + if (version) { + return { type, version }; + } + } + return undefined; + }; + if (!selectedEntry) { return (
@@ -250,7 +300,7 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => {
- {selectedEntry.referrers?.map((referrer: LockfileEntry) => ( + {selectedEntry.referrers?.map((referrer: LfxGraphEntry) => (
{
Entry ID: {referrer.rawEntryId} + + {'Dependency version: '} + {getDependencyInfo(referrer.rawEntryId, selectedEntry.entryPackageName)?.version} +
))} @@ -273,7 +327,7 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => {
- {selectedEntry.dependencies?.map((dependency: LockfileDependency) => ( + {selectedEntry.dependencies?.map((dependency: LfxGraphDependency) => (
{ > Name: {dependency.name}{' '} - {dependency.dependencyType === IDependencyType.PEER_DEPENDENCY + {dependency.dependencyKind === LfxDependencyKind.Peer ? `${ dependency.peerDependencyMeta.optional ? '(Optional)' : '(Non-optional)' } Peer Dependency` : ''}
- Version: {dependency.version} + Version: {dependency.versionPath} Entry ID: {dependency.entryId}
diff --git a/apps/lockfile-explorer-web/src/containers/LockfileViewer/index.tsx b/apps/lockfile-explorer-web/src/containers/LockfileViewer/index.tsx index d4a1083786f..6467ae37f12 100644 --- a/apps/lockfile-explorer-web/src/containers/LockfileViewer/index.tsx +++ b/apps/lockfile-explorer-web/src/containers/LockfileViewer/index.tsx @@ -2,8 +2,11 @@ // See LICENSE in the project root for license information. import React, { useCallback, useEffect, useRef, useState } from 'react'; + +import { Tabs, Checkbox, ScrollArea, Input, Text } from '@rushstack/rush-themed-ui'; + import styles from './styles.scss'; -import { LockfileEntry, LockfileEntryFilter } from '../../parsing/LockfileEntry'; +import { type LfxGraphEntry, LfxGraphEntryKind } from '../../packlets/lfx-shared'; import { ReactNull } from '../../types/ReactNull'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import { @@ -13,34 +16,33 @@ import { setFilter as selectFilter } from '../../store/slices/entrySlice'; import { getFilterFromLocalStorage, saveFilterToLocalStorage } from '../../helpers/localStorage'; -import { Tabs, Checkbox, ScrollArea, Input, Text } from '@rushstack/rush-themed-ui'; interface ILockfileEntryGroup { entryName: string; - versions: LockfileEntry[]; + versions: LfxGraphEntry[]; } -const LockfileEntryLi = ({ group }: { group: ILockfileEntryGroup }): JSX.Element => { +const LockfileEntryLi = ({ group }: { group: ILockfileEntryGroup }): React.ReactElement => { const selectedEntry = useAppSelector(selectCurrentEntry); const activeFilters = useAppSelector((state) => state.entry.filters); const dispatch = useAppDispatch(); - const fieldRef = useRef() as React.MutableRefObject; + const fieldRef = useRef(null); const clear = useCallback( - (entry: LockfileEntry) => () => { + (entry: LfxGraphEntry) => () => { dispatch(pushToStack(entry)); }, - [] + [dispatch] ); useEffect(() => { if (selectedEntry && selectedEntry.entryPackageName === group.entryName) { - fieldRef.current.scrollIntoView({ + fieldRef.current?.scrollIntoView({ behavior: 'smooth' }); } }, [selectedEntry, group]); - if (activeFilters[LockfileEntryFilter.Project]) { + if (activeFilters[LfxGraphEntryKind.Project]) { return (
{group.versions.map((entry) => ( @@ -80,7 +82,7 @@ const LockfileEntryLi = ({ group }: { group: ILockfileEntryGroup }): JSX.Element ); }; -const multipleVersions = (entries: LockfileEntry[]): boolean => { +const multipleVersions = (entries: LfxGraphEntry[]): boolean => { const set = new Set(); for (const entry of entries) { if (set.has(entry.entryPackageVersion)) return true; @@ -89,15 +91,15 @@ const multipleVersions = (entries: LockfileEntry[]): boolean => { return false; }; -export const LockfileViewer = (): JSX.Element | ReactNull => { +export const LockfileViewer = (): React.ReactElement | ReactNull => { const dispatch = useAppDispatch(); const [projectFilter, setProjectFilter] = useState(''); const [packageFilter, setPackageFilter] = useState(''); const entries = useAppSelector(selectFilteredEntries); const activeFilters = useAppSelector((state) => state.entry.filters); const updateFilter = useCallback( - (type: LockfileEntryFilter) => (e: React.ChangeEvent) => { - if (type === LockfileEntryFilter.Project) { + (type: LfxGraphEntryKind) => (e: React.ChangeEvent) => { + if (type === LfxGraphEntryKind.Project) { setProjectFilter(e.target.value); } else { setPackageFilter(e.target.value); @@ -108,42 +110,40 @@ export const LockfileViewer = (): JSX.Element | ReactNull => { ); useEffect(() => { - setProjectFilter(getFilterFromLocalStorage(LockfileEntryFilter.Project)); - setPackageFilter(getFilterFromLocalStorage(LockfileEntryFilter.Package)); + setProjectFilter(getFilterFromLocalStorage(LfxGraphEntryKind.Project)); + setPackageFilter(getFilterFromLocalStorage(LfxGraphEntryKind.Package)); }, []); - if (!entries) return ReactNull; - const getEntriesToShow = (): ILockfileEntryGroup[] => { - let filteredEntries: LockfileEntry[] = entries; - if (projectFilter && activeFilters[LockfileEntryFilter.Project]) { + let filteredEntries: LfxGraphEntry[] = entries; + if (projectFilter && activeFilters[LfxGraphEntryKind.Project]) { filteredEntries = entries.filter((entry) => entry.entryPackageName.indexOf(projectFilter) !== -1); - } else if (packageFilter && activeFilters[LockfileEntryFilter.Package]) { + } else if (packageFilter && activeFilters[LfxGraphEntryKind.Package]) { filteredEntries = entries.filter((entry) => entry.entryPackageName.indexOf(packageFilter) !== -1); } - const reducedEntries = filteredEntries.reduce((groups: { [key in string]: LockfileEntry[] }, item) => { + const reducedEntries = filteredEntries.reduce((groups: { [key: string]: LfxGraphEntry[] }, item) => { const group = groups[item.entryPackageName] || []; group.push(item); groups[item.entryPackageName] = group; return groups; }, {}); let groupedEntries: ILockfileEntryGroup[] = []; - for (const [packageName, entries] of Object.entries(reducedEntries)) { + for (const [packageName, versions] of Object.entries(reducedEntries)) { groupedEntries.push({ entryName: packageName, - versions: entries + versions }); } - if (activeFilters[LockfileEntryFilter.SideBySide]) { + if (activeFilters[LfxGraphEntryKind.SideBySide]) { groupedEntries = groupedEntries.filter((entry) => entry.versions.length > 1); } - if (activeFilters[LockfileEntryFilter.Doppelganger]) { + if (activeFilters[LfxGraphEntryKind.Doppelganger]) { groupedEntries = groupedEntries.filter((entry) => multipleVersions(entry.versions)); } - if (activeFilters[LockfileEntryFilter.Project]) { + if (activeFilters[LfxGraphEntryKind.Project]) { groupedEntries = groupedEntries.sort((a, b) => a.entryName > b.entryName ? 1 : b.entryName > a.entryName ? -1 : 0 ); @@ -153,79 +153,82 @@ export const LockfileViewer = (): JSX.Element | ReactNull => { }; const changeFilter = useCallback( - (filter: LockfileEntryFilter, enabled: boolean) => (): void => { + (filter: LfxGraphEntryKind, enabled: boolean) => (): void => { dispatch(selectFilter({ filter, state: enabled })); }, - [] + [dispatch] ); const togglePackageView = useCallback( (selected: string) => { if (selected === 'Projects') { - dispatch(selectFilter({ filter: LockfileEntryFilter.Project, state: true })); - dispatch(selectFilter({ filter: LockfileEntryFilter.Package, state: false })); + dispatch(selectFilter({ filter: LfxGraphEntryKind.Project, state: true })); + dispatch(selectFilter({ filter: LfxGraphEntryKind.Package, state: false })); } else { - dispatch(selectFilter({ filter: LockfileEntryFilter.Package, state: true })); - dispatch(selectFilter({ filter: LockfileEntryFilter.Project, state: false })); + dispatch(selectFilter({ filter: LfxGraphEntryKind.Package, state: true })); + dispatch(selectFilter({ filter: LfxGraphEntryKind.Project, state: false })); } }, - [activeFilters] + // eslint-disable-next-line react-hooks/exhaustive-deps + [dispatch, activeFilters] ); - return ( -
-
- - - - {getEntriesToShow().map((lockfileEntryGroup) => ( - - ))} - - {activeFilters[LockfileEntryFilter.Package] ? ( -
- - Filters - - - -
- ) : null} + if (!entries) { + return ReactNull; + } else { + return ( +
+
+ + + + {getEntriesToShow().map((lockfileEntryGroup) => ( + + ))} + + {activeFilters[LfxGraphEntryKind.Package] ? ( +
+ + Filters + + + +
+ ) : null} +
-
- ); + ); + } }; diff --git a/apps/lockfile-explorer-web/src/containers/LogoPanel/index.tsx b/apps/lockfile-explorer-web/src/containers/LogoPanel/index.tsx index 0d13a4e4abc..2c9ab618525 100644 --- a/apps/lockfile-explorer-web/src/containers/LogoPanel/index.tsx +++ b/apps/lockfile-explorer-web/src/containers/LogoPanel/index.tsx @@ -2,9 +2,10 @@ // See LICENSE in the project root for license information. import React from 'react'; + import styles from './styles.scss'; -export const LogoPanel = (): JSX.Element => { +export const LogoPanel = (): React.ReactElement => { // TODO: Add a mechanism to keep this in sync with the @rushstack/lockfile-explorer // package version. const appPackageVersion: string = window.appContext.appVersion; @@ -13,16 +14,28 @@ export const LogoPanel = (): JSX.Element => {
- +
- +
{appPackageVersion}
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..be941756ca6 --- /dev/null +++ b/apps/lockfile-explorer-web/src/containers/PackageJsonViewer/CodeBox.tsx @@ -0,0 +1,78 @@ +// 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 }): React.ReactElement => { + 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 94087655684..906c58fd0af 100644 --- a/apps/lockfile-explorer-web/src/containers/PackageJsonViewer/index.tsx +++ b/apps/lockfile-explorer-web/src/containers/PackageJsonViewer/index.tsx @@ -2,25 +2,28 @@ // See LICENSE in the project root for license information. import React, { useCallback, useEffect, useState } from 'react'; -import { readPnpmfileAsync, readPackageSpecAsync, readPackageJsonAsync } from '../../parsing/getPackageFiles'; -import styles from './styles.scss'; + +import { ScrollArea, Tabs, Text } from '@rushstack/rush-themed-ui'; + +import { readPnpmfileAsync, readPackageSpecAsync, readPackageJsonAsync } from '../../helpers/lfxApiClient'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import { selectCurrentEntry } from '../../store/slices/entrySlice'; -import { IPackageJson } from '../../types/IPackageJson'; +import type { IPackageJson } from '../../types/IPackageJson'; import { compareSpec } from '../../parsing/compareSpec'; import { loadSpecChanges } from '../../store/slices/workspaceSlice'; import { displaySpecChanges } from '../../helpers/displaySpecChanges'; import { isEntryModified } from '../../helpers/isEntryModified'; -import { ScrollArea, Tabs, Text } from '@rushstack/rush-themed-ui'; -import { LockfileEntryFilter } from '../../parsing/LockfileEntry'; +import { LfxGraphEntryKind } from '../../packlets/lfx-shared'; +import { CodeBox } from './CodeBox'; +import styles from './styles.scss'; -const PackageView: { [key in string]: string } = { +const PackageView: { [key: string]: string } = { PACKAGE_JSON: 'PACKAGE_JSON', PACKAGE_SPEC: 'PACKAGE_SPEC', PARSED_PACKAGE_JSON: 'PARSED_PACKAGE_JSON' }; -export const PackageJsonViewer = (): JSX.Element => { +export const PackageJsonViewer = (): React.ReactElement => { const dispatch = useAppDispatch(); const [packageJSON, setPackageJSON] = useState(undefined); const [parsedPackageJSON, setParsedPackageJSON] = useState(undefined); @@ -37,19 +40,20 @@ export const PackageJsonViewer = (): JSX.Element => { useEffect(() => { async function loadPnpmFileAsync(): Promise { - const pnpmfile = await readPnpmfileAsync(); - setPnpmfile(pnpmfile); + const repoPnpmfile = await readPnpmfileAsync(); + setPnpmfile(repoPnpmfile); } loadPnpmFileAsync().catch((e) => { + // eslint-disable-next-line no-console console.error(`Failed to load project's pnpm file: ${e}`); }); }, []); 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) { @@ -60,17 +64,19 @@ export const PackageJsonViewer = (): JSX.Element => { if (selectedEntry) { if (selectedEntry.entryPackageName) { loadPackageDetailsAsync(selectedEntry.packageJsonFolderPath).catch((e) => { + // eslint-disable-next-line no-console console.error(`Failed to load project information: ${e}`); }); } else { // This is used to develop the lockfile explorer application in case there is a mistake in our logic + // eslint-disable-next-line no-console console.log('The selected entry has no entry name: ', selectedEntry.entryPackageName); } } - }, [selectedEntry]); + }, [dispatch, selectedEntry]); const renderDep = - (name: boolean): ((dependencyDetails: [string, string]) => JSX.Element) => + (name: boolean): ((dependencyDetails: [string, string]) => React.ReactElement) => (dependencyDetails) => { const [dep, version] = dependencyDetails; if (specChanges.has(dep)) { @@ -149,7 +155,7 @@ export const PackageJsonViewer = (): JSX.Element => { } }; - const renderFile = (): JSX.Element | null => { + const renderFile = (): React.ReactElement | null => { switch (selection) { case PackageView.PACKAGE_JSON: if (!packageJSON) @@ -158,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 ( @@ -168,7 +174,7 @@ export const PackageJsonViewer = (): JSX.Element => { ); } - return
{pnpmfile}
; + return ; case PackageView.PARSED_PACKAGE_JSON: if (!parsedPackageJSON) return ( @@ -183,7 +189,7 @@ export const PackageJsonViewer = (): JSX.Element => { Package Name: - {selectedEntry?.kind === LockfileEntryFilter.Project + {selectedEntry?.kind === LfxGraphEntryKind.Project ? parsedPackageJSON.name : selectedEntry?.displayText} diff --git a/apps/lockfile-explorer-web/src/containers/SelectedEntryPreview/index.tsx b/apps/lockfile-explorer-web/src/containers/SelectedEntryPreview/index.tsx index 14c5ca393f9..310b7e3cf7d 100644 --- a/apps/lockfile-explorer-web/src/containers/SelectedEntryPreview/index.tsx +++ b/apps/lockfile-explorer-web/src/containers/SelectedEntryPreview/index.tsx @@ -2,6 +2,9 @@ // See LICENSE in the project root for license information. import React, { useCallback } from 'react'; + +import { Button, ScrollArea, Text } from '@rushstack/rush-themed-ui'; + import styles from './styles.scss'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import { @@ -11,9 +14,8 @@ import { removeBookmark, selectCurrentEntry } from '../../store/slices/entrySlice'; -import { Button, ScrollArea, Text } from '@rushstack/rush-themed-ui'; -export const SelectedEntryPreview = (): JSX.Element => { +export const SelectedEntryPreview = (): React.ReactElement => { const selectedEntry = useAppSelector(selectCurrentEntry); const isBookmarked = useAppSelector((state) => selectedEntry ? state.entry.bookmarkedEntries.includes(selectedEntry) : false @@ -21,23 +23,23 @@ export const SelectedEntryPreview = (): JSX.Element => { const entryStack = useAppSelector((state) => state.entry.selectedEntryStack); const entryForwardStack = useAppSelector((state) => state.entry.selectedEntryForwardStack); - const useDispatch = useAppDispatch(); + const dispatch = useAppDispatch(); const bookmark = useCallback(() => { - if (selectedEntry) useDispatch(addBookmark(selectedEntry)); - }, [selectedEntry]); + if (selectedEntry) dispatch(addBookmark(selectedEntry)); + }, [dispatch, selectedEntry]); const deleteEntry = useCallback(() => { - if (selectedEntry) useDispatch(removeBookmark(selectedEntry)); - }, [selectedEntry]); + if (selectedEntry) dispatch(removeBookmark(selectedEntry)); + }, [dispatch, selectedEntry]); const pop = useCallback(() => { - useDispatch(popStack()); - }, []); + dispatch(popStack()); + }, [dispatch]); const forward = useCallback(() => { - useDispatch(forwardStack()); - }, []); + dispatch(forwardStack()); + }, [dispatch]); - const renderButtonRow = (): JSX.Element => { + const renderButtonRow = (): React.ReactElement => { return (