diff --git a/.craft.yml b/.craft.yml index f8774b57..cc9ffee4 100644 --- a/.craft.yml +++ b/.craft.yml @@ -1,5 +1,6 @@ -minVersion: '0.30.0' -changelogPolicy: auto +minVersion: '2.14.0' +changelog: + policy: auto preReleaseCommand: >- node -p " const {execSync} = require('child_process'); @@ -42,4 +43,6 @@ targets: target: getsentry/craft targetFormat: '{{{target}}}:latest' - name: github + floatingTags: + - 'v{major}' - name: gh-pages diff --git a/.eslintrc.js b/.eslintrc.js index d1d6e708..5d0c425d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,6 +4,7 @@ module.exports = { es2017: true, node: true, }, + ignorePatterns: ['docs/**'], parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], extends: [ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a00268fd..62db42f7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,11 @@ on: - master - release/** pull_request: + workflow_call: + +concurrency: + group: ${{ github.ref_name || github.sha }} + cancel-in-progress: true jobs: test: @@ -52,10 +57,19 @@ jobs: run: yarn install --frozen-lockfile - name: Build run: yarn build --define:process.env.CRAFT_BUILD_SHA='"'${{ github.sha }}'"' + - name: Smoke Test + run: ./dist/craft --help - name: NPM Pack run: npm pack - - name: Docs - run: cd docs && zip -r ../gh-pages _site/ + - name: Build Docs + working-directory: docs + run: | + yarn install --frozen-lockfile + yarn build + - name: Package Docs + run: | + cp .nojekyll docs/dist/ + cd docs/dist && zip -r ../../gh-pages.zip . - name: Archive Artifacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: diff --git a/.github/workflows/changelog-preview.yml b/.github/workflows/changelog-preview.yml new file mode 100644 index 00000000..95d6bdcb --- /dev/null +++ b/.github/workflows/changelog-preview.yml @@ -0,0 +1,105 @@ +name: Changelog Preview + +on: + # Allow this workflow to be called from other repositories + workflow_call: + inputs: + craft-version: + description: 'Version of Craft to use (tag or "latest")' + required: false + type: string + default: 'latest' + + # Also run on PRs in this repository (for dogfooding) + # Includes 'edited' and 'labeled' to update when PR title/description/labels change + pull_request: + types: [opened, synchronize, reopened, edited, labeled] + +permissions: + pull-requests: write + +jobs: + preview: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Install Craft using the shared install action + # TODO: Change to @v2 or @master after this PR is merged + - name: Install Craft + uses: getsentry/craft/install@pull/669/head + with: + craft-version: ${{ inputs.craft-version || 'latest' }} + + - name: Generate Changelog Preview + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CRAFT_LOG_LEVEL: Warn + run: | + PR_NUMBER="${{ github.event.pull_request.number }}" + + # Generate changelog with current PR injected and highlighted (JSON format) + echo "Running craft changelog --pr $PR_NUMBER --format json..." + RESULT=$(craft changelog --pr "$PR_NUMBER" --format json 2>/dev/null || echo '{"changelog":"","bumpType":null}') + + # Extract fields from JSON + CHANGELOG=$(echo "$RESULT" | jq -r '.changelog // ""') + BUMP_TYPE=$(echo "$RESULT" | jq -r '.bumpType // "none"') + + if [[ -z "$CHANGELOG" ]]; then + CHANGELOG="_No changelog entries will be generated from this PR._" + fi + + # Format bump type for display + case "$BUMP_TYPE" in + major) BUMP_BADGE="🔴 **Major** (breaking changes)" ;; + minor) BUMP_BADGE="🟡 **Minor** (new features)" ;; + patch) BUMP_BADGE="🟢 **Patch** (bug fixes)" ;; + *) BUMP_BADGE="⚪ **None** (no version bump detected)" ;; + esac + + # Build comment body using a temp file (safer than heredoc) + COMMENT_FILE=$(mktemp) + cat > "$COMMENT_FILE" << CRAFT_CHANGELOG_COMMENT_END + + ## Suggested Version Bump + + ${BUMP_BADGE} + + ## 📋 Changelog Preview + + This is how your changes will appear in the changelog. + Entries from this PR are highlighted with a left border (blockquote style). + + --- + + ${CHANGELOG} + + --- + + 🤖 This preview updates automatically when you update the PR. + CRAFT_CHANGELOG_COMMENT_END + + # Find existing comment with our marker + COMMENT_ID=$(gh api \ + "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ + --jq '.[] | select(.body | contains("")) | .id' \ + | head -1) + + if [[ -n "$COMMENT_ID" ]]; then + echo "Updating existing comment $COMMENT_ID..." + gh api -X PATCH \ + "repos/$GITHUB_REPOSITORY/issues/comments/$COMMENT_ID" \ + -F body=@"$COMMENT_FILE" + else + echo "Creating new comment..." + gh api -X POST \ + "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ + -F body=@"$COMMENT_FILE" + fi + + rm -f "$COMMENT_FILE" diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml new file mode 100644 index 00000000..d5bbfa65 --- /dev/null +++ b/.github/workflows/docs-preview.yml @@ -0,0 +1,69 @@ +name: Docs Preview + +on: + pull_request: + paths: + - 'docs/**' + - '.github/workflows/docs-preview.yml' + +permissions: + contents: write + pull-requests: write + +jobs: + preview: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Build Docs for Preview + working-directory: docs + env: + # Override base path for PR preview + DOCS_BASE_PATH: /craft/pr-preview/pr-${{ github.event.pull_request.number }} + run: | + yarn install --frozen-lockfile + yarn build + + - name: Ensure .nojekyll at gh-pages root + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Try to fetch the gh-pages branch + if git fetch origin gh-pages:gh-pages 2>/dev/null; then + # Branch exists remotely, check if .nojekyll is present + if git show gh-pages:.nojekyll &>/dev/null; then + echo ".nojekyll already exists at gh-pages root" + else + echo "Adding .nojekyll to existing gh-pages branch" + git checkout gh-pages + touch .nojekyll + git add .nojekyll + git commit -m "Add .nojekyll to disable Jekyll processing" + git push origin gh-pages + git checkout - + fi + else + # Branch doesn't exist, create it as an orphan branch + echo "Creating gh-pages branch with .nojekyll" + git checkout --orphan gh-pages + git rm -rf . + touch .nojekyll + git add .nojekyll + git commit -m "Initialize gh-pages with .nojekyll" + git push origin gh-pages + git checkout - + fi + + - name: Deploy Preview + uses: rossjrw/pr-preview-action@v1 + with: + source-dir: docs/dist/ + preview-branch: gh-pages + umbrella-dir: pr-preview + action: auto diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7dd901ec..4a8d49c0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,25 @@ name: Release +concurrency: ${{ github.workflow }}-${{ github.ref }} on: workflow_dispatch: inputs: version: description: Version to release - required: false + required: true + default: "auto" force: description: Force a release even when there are release-blockers (optional) required: false + jobs: + build: + name: Build + uses: ./.github/workflows/build.yml + permissions: + contents: read + release: + needs: build runs-on: ubuntu-latest name: 'Release a new version' permissions: @@ -27,7 +37,7 @@ jobs: token: ${{ steps.token.outputs.token }} fetch-depth: 0 - name: Prepare release - uses: getsentry/action-prepare-release@3cea80dc3938c0baf5ec4ce752ecb311f8780cdc # v1 + uses: ./ env: GITHUB_TOKEN: ${{ steps.token.outputs.token }} with: diff --git a/.gitignore b/.gitignore index c53729a0..03e03aaf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,11 @@ coverage/ dist/ node_modules/ +# Docs build artifacts +docs/dist/ +docs/node_modules/ +docs/.astro/ + yarn-error.log npm-debug.log diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..1d43909f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,68 @@ +# AGENTS.md + +This file provides guidance for AI coding assistants working with the Craft codebase. + +## Package Management + +- **Always use `yarn`** (v1) for package management. Never use `npm` or `pnpm`. +- Node.js version is managed by [Volta](https://volta.sh/) (currently v22.12.0). +- Install dependencies with `yarn install --frozen-lockfile`. + +## Development Commands + +| Command | Description | +|---------|-------------| +| `yarn build` | Build the project (outputs to `dist/craft`) | +| `yarn test` | Run tests | +| `yarn lint` | Run ESLint | +| `yarn fix` | Auto-fix lint issues | + +To manually test changes: + +```bash +yarn build && ./dist/craft +``` + +## Code Style + +- **TypeScript** is used throughout the codebase. +- **Prettier** with single quotes and no arrow parens (configured in `.prettierrc.yml`). +- **ESLint** extends `@typescript-eslint/recommended`. +- Unused variables prefixed with `_` are allowed (e.g., `_unusedParam`). + +## Project Structure + +``` +src/ +├── __mocks__/ # Test mocks +├── __tests__/ # Test files (*.test.ts) +├── artifact_providers/ # Artifact provider implementations +├── commands/ # CLI command implementations +├── schemas/ # JSON schema and TypeScript types for config +├── status_providers/ # Status provider implementations +├── targets/ # Release target implementations +├── types/ # Shared TypeScript types +├── utils/ # Utility functions +├── config.ts # Configuration loading +├── index.ts # CLI entry point +└── logger.ts # Logging utilities +dist/ +└── craft # Single bundled executable (esbuild output) +``` + +## Testing + +- Tests use **Jest** with `ts-jest`. +- Test files are located in `src/__tests__/` and follow the `*.test.ts` naming pattern. +- Run tests with `yarn test`. + +## CI/CD + +- Main branch is `master`. +- CI runs tests on Node.js 20 and 22. +- Craft releases itself using its own tooling (dogfooding). + +## Configuration + +- Project configuration lives in `.craft.yml` at the repository root. +- The configuration schema is defined in `src/schemas/`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7efc3d00..eaa7bb07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,84 @@ # Changelog +## 2.15.0 + +### New Features ✨ + +#### Github + +- feat(github): Integrate action-prepare-release into Craft repo by @BYK in [#667](https://github.com/getsentry/craft/pull/667) +- feat(github): Emit resolved version to GITHUB_OUTPUTS on prepare by @BYK in [#666](https://github.com/getsentry/craft/pull/666) + +## 2.14.1 + +### Bug Fixes 🐛 + +#### Changelog + +- fix(changelog): Fix whitespace related issues by @BYK in [#664](https://github.com/getsentry/craft/pull/664) +- fix(changelog): Add ref and perf to internal changes prefixes by @BYK in [#662](https://github.com/getsentry/craft/pull/662) + +### Build / dependencies / internal 🔧 + +- ci(deps): Upgrade action-prepare-release to latest by @BYK in [#663](https://github.com/getsentry/craft/pull/663) + +- ci(release): Add support for auto versioning by @BYK in [#665](https://github.com/getsentry/craft/pull/665) + +## 2.14.0 + +### New Features ✨ + +- feat(docker): Add support for multiple registries by @BYK in [#657](https://github.com/getsentry/craft/pull/657) + +- feat: Add automatic version bumping based on conventional commits by @BYK in [#656](https://github.com/getsentry/craft/pull/656) +- feat: Add `skip-changelog` label by default by @BYK in [#655](https://github.com/getsentry/craft/pull/655) + +### Bug Fixes 🐛 + +- fix(changelog): Unscoped entries should be grouped under "other" by @BYK in [#659](https://github.com/getsentry/craft/pull/659) + +### Build / dependencies / internal 🔧 + +- ci: Update action-prepare-release to v1.6.5 by @BYK in [#654](https://github.com/getsentry/craft/pull/654) + +### Other + +- fix(docker): Support regional Artifact Registry endpoints in isGoogleCloudRegistry by @BYK in [#661](https://github.com/getsentry/craft/pull/661) + +## 2.13.1 + +### Build / dependencies / internal 🔧 + +- ci: Fix release input desc and concurrency by @BYK in [#653](https://github.com/getsentry/craft/pull/653) + +### Bug Fixes 🐛 + +- fix: Fix startup issue with yargs by @BYK in [#651](https://github.com/getsentry/craft/pull/651) + +### Documentation 📚 + +- docs: Add AGENTS.md by @BYK in [#652](https://github.com/getsentry/craft/pull/652) + +## 2.13.0 + +### New Features ✨ + +- feat(npm): Add workspaces support by @BYK in [#645](https://github.com/getsentry/craft/pull/645) +- feat(changelog): Add grouping by scope by @BYK in [#644](https://github.com/getsentry/craft/pull/644) +- feat(changelog): Add section ordering by @BYK in [#640](https://github.com/getsentry/craft/pull/640) + +### Build / dependencies / internal 🔧 + +- build(deps): bump jws from 4.0.0 to 4.0.1 by @dependabot in [#650](https://github.com/getsentry/craft/pull/650) + +### Bug Fixes 🐛 + +- fix(changelog): default matcher should match scopes with dashes by @BYK in [#641](https://github.com/getsentry/craft/pull/641) + +### Other + +- build(deps-dev): bump @octokit/request-error from 6.1.8 to 7.0.0 by @dependabot in [#643](https://github.com/getsentry/craft/pull/643) + ## 2.12.1 ### Bug Fixes 🐛 diff --git a/Dockerfile b/Dockerfile index f28edf11..9d3b78a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,8 +44,6 @@ RUN apt-get -qq update \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -COPY Gemfile Gemfile.lock ./ - RUN python3 -m venv /venv && pip install twine==6.1.0 pkginfo==1.12.1.2 --no-cache RUN : \ @@ -78,7 +76,8 @@ RUN : \ && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --profile minimal -y \ && cargo --version \ && cargo install cargo-hack \ - && gem install -g --no-document \ + # Pin CocoaPods version to avoid bugs like https://github.com/CocoaPods/CocoaPods/issues/12081 + && gem install cocoapods -v 1.16.2 --no-document \ # Install https://github.com/getsentry/symbol-collector && symbol_collector_url=$(curl -s https://api.github.com/repos/getsentry/symbol-collector/releases/tags/1.17.0 | \ jq -r '.assets[].browser_download_url | select(endswith("symbolcollector-console-linux-x64.zip"))') \ diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 93ff5d27..00000000 --- a/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -# Gemfile -# Pin CocoaPods Version to avoid that bugs in CocoaPods like -# https://github.com/CocoaPods/CocoaPods/issues/12081 break our release -# workflow. -gem "cocoapods", "= 1.16.2" diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index c0eac5ab..00000000 --- a/Gemfile.lock +++ /dev/null @@ -1,114 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.7) - base64 - nkf - rexml - activesupport (7.2.2.1) - base64 - benchmark (>= 0.3) - bigdecimal - concurrent-ruby (~> 1.0, >= 1.3.1) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - logger (>= 1.4.2) - minitest (>= 5.1) - securerandom (>= 0.3) - tzinfo (~> 2.0, >= 2.0.5) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) - algoliasearch (1.27.5) - httpclient (~> 2.8, >= 2.8.3) - json (>= 1.5.1) - atomos (0.1.3) - base64 (0.2.0) - benchmark (0.4.0) - bigdecimal (3.1.9) - claide (1.1.0) - cocoapods (1.16.2) - addressable (~> 2.8) - claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.16.2) - cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 2.1, < 3.0) - cocoapods-plugins (>= 1.0.0, < 2.0) - cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.6.0, < 2.0) - cocoapods-try (>= 1.1.0, < 2.0) - colored2 (~> 3.1) - escape (~> 0.0.4) - fourflusher (>= 2.3.0, < 3.0) - gh_inspector (~> 1.0) - molinillo (~> 0.8.0) - nap (~> 1.0) - ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.27.0, < 2.0) - cocoapods-core (1.16.2) - activesupport (>= 5.0, < 8) - addressable (~> 2.8) - algoliasearch (~> 1.0) - concurrent-ruby (~> 1.1) - fuzzy_match (~> 2.0.4) - nap (~> 1.0) - netrc (~> 0.11) - public_suffix (~> 4.0) - typhoeus (~> 1.0) - cocoapods-deintegrate (1.0.5) - cocoapods-downloader (2.1) - cocoapods-plugins (1.0.0) - nap - cocoapods-search (1.0.1) - cocoapods-trunk (1.6.0) - nap (>= 0.8, < 2.0) - netrc (~> 0.11) - cocoapods-try (1.2.0) - colored2 (3.1.2) - concurrent-ruby (1.3.5) - connection_pool (2.5.0) - drb (2.2.1) - escape (0.0.4) - ethon (0.16.0) - ffi (>= 1.15.0) - ffi (1.17.1) - fourflusher (2.3.1) - fuzzy_match (2.0.4) - gh_inspector (1.1.3) - httpclient (2.9.0) - mutex_m - i18n (1.14.7) - concurrent-ruby (~> 1.0) - json (2.10.2) - logger (1.6.6) - minitest (5.25.5) - molinillo (0.8.0) - mutex_m (0.3.0) - nanaimo (0.4.0) - nap (1.1.0) - netrc (0.11.0) - nkf (0.2.0) - public_suffix (4.0.7) - rexml (3.4.1) - ruby-macho (2.5.1) - securerandom (0.4.1) - typhoeus (1.4.1) - ethon (>= 0.9.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - xcodeproj (1.27.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.4.0) - rexml (>= 3.3.6, < 4.0) - -PLATFORMS - ruby - -DEPENDENCIES - cocoapods (= 1.16.2) - -BUNDLED WITH - 2.4.20 diff --git a/README.md b/README.md index 30639e39..7fc86b4e 100644 --- a/README.md +++ b/README.md @@ -3,1594 +3,163 @@

-# Craft: Universal Release Tool (And More) +# Craft: Universal Release Tool -[![Travis](https://img.shields.io/travis/getsentry/craft.svg)](https://travis-ci.org/getsentry/craft) [![GitHub release](https://img.shields.io/github/release/getsentry/craft.svg)](https://github.com/getsentry/craft/releases/latest) [![npm version](https://img.shields.io/npm/v/@sentry/craft.svg)](https://www.npmjs.com/package/@sentry/craft) [![license](https://img.shields.io/github/license/getsentry/craft.svg)](https://github.com/getsentry/craft/blob/master/LICENSE) -`craft` is a command line tool that helps to automate and pipeline package releases. It suggests, and -then enforces a specific workflow for managing release branches, changelogs, artifact publishing, etc. +Craft is a command line tool that helps automate and pipeline package releases. It enforces a specific workflow for managing release branches, changelogs, and artifact publishing. -## Table of Contents +📚 **[Full Documentation](https://getsentry.github.io/craft/)** -- [Installation](#installation) - - [Binary](#binary) - - [npm (not recommended)](#npm-not-recommended) -- [Usage](#usage) -- [Caveats](#caveats) -- [Global Configuration](#global-configuration) - - [Environment Files](#environment-files) -- [Workflow](#workflow) - - [`craft prepare`: Preparing a New Release](#craft-prepare-preparing-a-new-release) - - [`craft publish`: Publishing the Release](#craft-publish-publishing-the-release) - - [Example](#example) -- [Configuration File: `.craft.yml`](#configuration-file-craftyml) - - [GitHub project](#github-project) - - [Pre-release Command](#pre-release-command) - - [Post-release Command](#post-release-command) - - [Release Branch Name](#release-branch-name) - - [Changelog Policies](#changelog-policies) - - [Minimal Version](#minimal-version) - - [Required Files](#required-files) -- [Status Provider](#status-provider) -- [Artifact Provider](#artifact-provider) -- [Target Configurations](#target-configurations) - - [Per-target options](#per-target-options) - - [GitHub (`github`)](#github-github) - - [NPM (`npm`)](#npm-npm) - - [Python Package Index (`pypi`)](#python-package-index-pypi) - - [Sentry internal PyPI (`sentry-pypi`)](#sentry-internal-pypi-sentry-pypi) - - [Homebrew (`brew`)](#homebrew-brew) - - [NuGet (`nuget`)](#nuget-nuget) - - [Rust Crates (`crates`)](#rust-crates-crates) - - [Google Cloud Storage (`gcs`)](#google-cloud-storage-gcs) - - [GitHub Pages (`gh-pages`)](#github-pages-gh-pages) - - [Sentry Release Registry (`registry`)](#sentry-release-registry-registry) - - [Cocoapods (`cocoapods`)](#cocoapods-cocoapods) - - [Docker (`docker`)](#docker-docker) - - [Ruby Gems Index (`gem`)](#ruby-gems-index-gem) - - [AWS Lambda Layer (`aws-lambda-layer`)](#aws-lambda-layer-aws-lambda-layer) - - [Unity Package Manager (`upm`)](#unity-package-manager-upm) - - [Maven central (`maven`)](#maven-central-maven) - - [Symbol Collector (`symbol-collector`)](#symbol-collector-symbol-collector) - - [pub.dev (`pub-dev`)](#pubdev-pub-dev) - - [Hex (`hex`)](#hex-hex) - - [Commit on Git Repository (`commit-on-git-repository`)](#git-repository-commit-on-git-repository) -- [Integrating Your Project with `craft`](#integrating-your-project-with-craft) -- [Pre-release (Version-bumping) Script: Conventions](#pre-release-version-bumping-script-conventions) -- [Post-release Script: Conventions](#post-release-script-conventions) +## Quick Start -## Installation +### Installation -### Binary - -`craft` is [distributed as a minified single JS binary](https://github.com/getsentry/craft/releases/latest). - -### npm (not recommended) - -Recommendation is to used this file directly but one can also install `craft` as -an [NPM package](https://yarn.pm/@sentry/craft) and can be installed via `yarn` -or `npm`: - -```shell -yarn global add @sentry/craft -``` +Download the [latest binary release](https://github.com/getsentry/craft/releases/latest), or install via npm: ```shell npm install -g @sentry/craft ``` -## Usage - -```shell -$ craft -h -craft - -Commands: - craft prepare NEW-VERSION 🚢 Prepare a new release branch - [aliases: p, prerelease, prepublish, prepare, release] - craft publish NEW-VERSION 🛫 Publish artifacts [aliases: pp, publish] - craft targets List defined targets as JSON array - craft config Print the parsed, processed, and validated Craft - config for the current project in pretty-JSON. - craft artifacts 📦 Manage artifacts [aliases: a, artifact] - -Options: - --no-input Suppresses all user prompts [default: false] - --dry-run Dry run mode: do not perform any real actions - --log-level Logging level - [choices: "Fatal", "Error", "Warn", "Log", "Info", "Success", "Debug", - "Trace", "Silent", "Verbose"] [default: "Info"] - -v, --version Show version number [boolean] - -h, --help Show help [boolean] -``` - - -### Version naming conventions - -Craft currently supports [semantic versioning (semver)](https://semver.org)-like versions for the `NEW-VERSION` argument passed to its `prepare` and `publish` commands. This means, releases made with craft need to follow a general pattern as follows: - -```txt -..(-)?(-)? -``` - -- The ``, `` and `` numbers are required -- The `` and `` identifiers are optional - -#### Preview releases (``) - -Preview or pre-release identifiers **must** include one of the following identifiers - -```txt -preview|pre|rc|dev|alpha|beta|unstable|a|b -``` - -and may additionally include incremental pre-release version numbers. -Adding identifiers other than the ones listed above result in Craft either rejecting the release (if not parse-able) or the release being treated by individual targets as a stable release. - -Examples: - -```txt -1.0.0-preview -1.0.0-alpha.0 -1.0.0-beta.1 -1.0.0-rc.20 -1.0.0-a - -// invalid or incorrectly treated -1.0.0-foo -1.0.0-canary.0 -``` - -#### Special Case: Python Post Releases - - -Python has the concept of post releases, which craft handles implicitly. A post release is indicated by a `-\d+` suffix to the semver version, for example: `1.0.0-1`. -Given that we only consider certain identifiers as [pre-releases](#preview-releases-prerelease), post releases are considered stable releases. - -### Build identifiers (``) - -Craft supports adding a build identifier to your version, for example if you release the same package version for different platforms or architectures. -You can also combine build and pre-release identifiers but in this case, the pre-release identifier has to come first. - -Examples: - -```txt -// valid -1.0.0+x86_64 -1.0.0-rc.1+x86_64 - -// invalid or incorrectly treated -1.0.0+rc.1+x86_64 -1.0.0+x86_64-beta.0 -``` - -## Caveats - -- When interacting with remote GitHub repositories, `craft` uses the - remote `origin` by default. If you have a different setup, set the - `CRAFT_REMOTE` environment variable or the `--remote` option to the git remote - you are using. - -## Global Configuration - -Global configuration for `craft` can be done either by using environment -variables or by adding values to a configuration file (see below). - -All command line flags can be set through environment variables by prefixing -them with `CRAFT_` and converting them to UPPERCASE_UNDERSCORED versions: +### Usage ```shell -CRAFT_LOG_LEVEL=Debug -CRAFT_DRY_RUN=1 -CRAFT_NO_INPUT=0 -``` - -Since Craft heavily relies on GitHub, it needs the `GITHUB_TOKEN` environment -variable to be set to a proper -[GitHub Personal Access Token](https://github.com/settings/tokens) for almost -anything. The token only needs `repo` scope (`repo:status` and `public_repo` -subscopes, to be precise). - -Additional environment variables may be required when publishing to specific -targets (e.g. `TWINE_USERNAME` and `TWINE_PASSWORD` for PyPI target). - -### Environment Files - -`craft` will read configuration variables (keys, tokens, etc.) from the -following locations: - -- `$HOME/.craft.env` -- `$PROJECT_DIR/.craft.env` -- the shell's environment - -where `$HOME` is the current user's home directory, and `$PROJECT_DIR` is the -directory where `.craft.yml` is located. - -These locations will be checked in the order specified above, with values -found in one location overwriting anything found in previous locations. In other -words, environment variables will take precedence over either configuration -file, and the project-specific file will take precedence over the file in -`$HOME`. - -The env files must be written in shell (`sh`/`bash`) format. -Leading `export` is allowed. - -Example: - -```shell -# ~/.craft.env -GITHUB_TOKEN=token123 -export NUGET_API_TOKEN=abcdefgh -``` - -## Workflow - -### `craft prepare`: Preparing a New Release - -This command will create a new release branch, check the changelog entries, -run a version-bumping script, and push this branch to GitHub. We expect -that CI triggered by pushing this branch will result in release artifacts -being built and uploaded to the artifact provider you wish to use during the -subsequent `publish` step. - -```shell -craft prepare NEW-VERSION - -🚢 Prepare a new release branch - -Positionals: - NEW-VERSION The new version you want to release [string] [required] - -Options: - --no-input Suppresses all user prompts [default: false] - --dry-run Dry run mode: do not perform any real actions - --log-level Logging level - [choices: "Fatal", "Error", "Warn", "Log", "Info", "Success", "Debug", - "Trace", "Silent", "Verbose"] [default: "Info"] - --rev, -r Source revision (git SHA or tag) to prepare from (if not - branch head) [string] - --no-push Do not push the release branch [boolean] [default: false] - --no-git-checks Ignore local git changes and unsynchronized remotes - [boolean] [default: false] - --no-changelog Do not check for changelog entries [boolean] [default: false] - --publish Run "publish" right after "release"[boolean] [default: false] - --remote The git remote to use when pushing - [string] [default: "origin"] - -v, --version Show version number [boolean] - -h, --help Show help [boolean] -``` - -### `craft publish`: Publishing the Release - -The command will find a release branch for the provided version. The normal flow -is for this release branch to be created automatically by `craft prepare`, but -that's not strictly necessary. Then, it subscribes to the latest status checks on -that branch. Once the checks pass, it downloads the release artifacts from the -artifact provider configured in `.craft.yml` and uploads them to the targets named -on the command line (and pre-configured in `.craft.yml`). - -```shell -craft publish NEW-VERSION - -🛫 Publish artifacts - -Positionals: - NEW-VERSION Version to publish [string] [required] - -Options: - --no-input Suppresses all user prompts [default: false] - --dry-run Dry run mode: do not perform any real actions - --log-level Logging level - [choices: "Fatal", "Error", "Warn", "Log", "Info", "Success", "Debug", - "Trace", "Silent", "Verbose"] [default: "Info"] - --target, -t Publish to this target - [string] [choices: "npm", "gcs", "registry", "docker", "github", "gh-pages", - "all", "none"] [default: "all"] - --rev, -r Source revision (git SHA or tag) to publish (if not release - branch head) [string] - --no-merge Do not merge the release branch after publishing - [boolean] [default: false] - --keep-branch Do not remove release branch after merging it - [boolean] [default: false] - --keep-downloads Keep all downloaded files [boolean] [default: false] - --no-status-check Do not check for build status [boolean] [default: false] - -v, --version Show version number [boolean] - -h, --help Show help [boolean] -``` - -### Example - -Let's imagine we want to release a new version of our package, and the version -in question is `1.2.3`. - -We run `prepare` command first: - -`$ craft prepare 1.2.3` - -After some basic sanity checks this command creates a new release branch -`release/1.2.3`, runs the version-bumping script (`scripts/bump-version.sh`), -commits the changes made by the script, and then pushes the new branch to -GitHub. At this point CI systems kick in, and the results of those builds, as -well as built artifacts (binaries, NPM archives, Python wheels) are gradually -uploaded to GitHub. - -To publish the built artifacts we run `publish`: - -`$ craft publish 1.2.3` - -This command will find our release branch (`release/1.2.3`), check the build -status of the respective git revision in GitHub, and then publish available -artifacts to configured targets (for example, to GitHub and NPM in the case of -Craft). - -## Configuration File: `.craft.yml` - -Project configuration for `craft` is stored in `.craft.yml` configuration file, -located in the project root. - -### GitHub project - -Craft tries to determine the GitHub repo information from the local git repo and -its remotes configuration. However, since `publish` command does not require a -local git checkout, you may want to hard-code this information into the -configuration itself: - -```yaml -github: - owner: getsentry - repo: sentry-javascript -``` - -### Pre-release Command - -This command will run on your newly created release branch as part of `prepare` -command. By default, it is set to `bash scripts/bump-version.sh`. Please refer -to the [Pre-release version bumping script conventions section](#pre-release-version-bumping-script-conventions) -for more details. - -```yaml -preReleaseCommand: bash scripts/bump-version.sh -``` - -### Post-release Command - -This command will run after a successful `publish`. By default, it is set to -`bash scripts/post-release.sh`. It will _not_ error if the default script is -missing though, as this may not be needed by all projects. Please refer to the -[Post-release script conventions section](#post-release-script-conventions) -for more details. - -```yaml -postReleaseCommand: bash scripts/post-release.sh -``` - -### Release Branch Name - -This overrides the prefix for the release branch name. The full branch name used -for a release is `{releaseBranchPrefix}/{version}`. The prefix defaults to -`"release"`. - -```yaml -releaseBranchPrefix: publish -``` - -### Changelog Policies - -`craft` can help you to maintain change logs for your projects. At the moment, -`craft` supports two approaches: `simple`, and `auto` to changelog management. - -In `simple` mode, `craft prepare` will remind you to add a changelog entry to the -changelog file (`CHANGELOG.md` by default). - -In `auto` mode, `craft prepare` will use the following logic: - -1. If there's already an entry for the given version, use that -2. Else if there is an entry named `Unreleased`, rename that to the given - version -3. Else, create a new section for the version and populate it with the changes - since the last version. It uses `.github/release.yml` configuration to - categorize PRs by labels or commit title patterns. PRs are matched to - categories based on their labels first; if no label matches, the commit/PR - title is checked against `commit_patterns`. Any PRs that don't match a - category are listed under the "Other" section. The system supports custom - categories, exclusions (both global and per-category), and wildcard matching. - Check out [GitHub's release notes documentation](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuration-options) - for the base configuration format. - -**Custom Changelog Entries from PR Descriptions** - -By default, the changelog entry for a PR is generated from its title. However, -PR authors can override this by adding a "Changelog Entry" section to the PR -description. This allows for more detailed, user-facing changelog entries without -cluttering the PR title. - -To use this feature, add a markdown heading (level 2 or 3) titled "Changelog Entry" -to your PR description, followed by the desired changelog text: - -```markdown -### Description - -Add `foo` function, and add unit tests to thoroughly check all edge cases. - -### Changelog Entry - -Add a new function called `foo` which prints "Hello, world!" - -### Issues - -Closes #123 -``` - -The text under "Changelog Entry" will be used verbatim in the changelog instead -of the PR title. If no such section is present, the PR title is used as usual. - -**Advanced Features:** - -1. **Multiple Entries**: If you use multiple top-level bullet points in the - "Changelog Entry" section, each bullet will become a separate changelog entry: - - ```markdown - ### Changelog Entry - - - Add OAuth2 authentication - - Add two-factor authentication - - Add session management - ``` - -2. **Nested Content**: Indented bullets (4+ spaces or tabs) are preserved as - nested content under their parent entry: - - ```markdown - ### Changelog Entry - - - Add authentication system - - OAuth2 support - - Two-factor authentication - - Session management - ``` - - This will generate: - ```markdown - - Add authentication system by @user in [#123](url) - - OAuth2 support - - Two-factor authentication - - Session management - ``` - -3. **Plain Text**: If no bullets are used, the entire content is treated as a - single changelog entry. Multi-line text is supported. +# Auto-determine version from conventional commits +craft prepare auto -4. **Content Isolation**: Only content within the "Changelog Entry" section is - included in the changelog. Other sections (Description, Issues, etc.) are - ignored. +# Or specify a version explicitly +craft prepare 1.2.3 -**Default Conventional Commits Configuration** - -If `.github/release.yml` doesn't exist or has no `changelog` section, craft uses -a default configuration based on [Conventional Commits](https://www.conventionalcommits.org/): - -| Category | Pattern | -| ------------------------------- | ------------------------------------ | -| Breaking Changes | `^\w+(\(\w+\))?!:` (e.g., `feat!:`) | -| Build / dependencies / internal | `^(build\|ref\|chore\|ci)(\(\w+\))?:`| -| Bug Fixes | `^fix(\(\w+\))?:` | -| Documentation | `^docs?(\(\w+\))?:` | -| New Features | `^feat(\(\w+\))?:` | - -**Commit Log Patterns** - -In addition to GitHub labels, you can match commits to categories using -`commit_patterns`. This is an array of JavaScript regex strings that are -matched against the PR title (or commit message if no PR exists). Labels always -take precedence over patterns. - -```yaml -# .github/release.yml -changelog: - categories: - - title: Features - labels: - - enhancement - commit_patterns: - - "^feat(\\(\\w+\\))?:" - - title: Bug Fixes - labels: - - bug - commit_patterns: - - "^fix(\\(\\w+\\))?:" -``` - -Patterns are matched case-insensitively. You can use both `labels` and -`commit_patterns` in the same category - PRs will be matched by label first, -then by pattern if no label matches. - -**Section Ordering** - -Changelog sections are sorted according to the order in which categories are -defined in `.github/release.yml`. For example, if your config lists categories -as `Features`, `Bug Fixes`, `Documentation`, the generated changelog will -display sections in that exact order, regardless of which type of PR was -encountered first in the git history. The "Other" section (for uncategorized -changes) always appears last. - -**Scope Grouping** - -When using [Conventional Commits](https://www.conventionalcommits.org/) with -scopes (e.g., `feat(api): add endpoint`), changes within each category are -automatically grouped by scope. Scope names are formatted as title case -sub-headers (level 4, `####`), with dashes and underscores converted to spaces. - -For example, `feat(my-component): add feature` will appear under a -`#### My Component` sub-header within the Features category. - -Scopes are normalized to lowercase for grouping purposes, so `feat(API):` and -`feat(api):` will be grouped together under the same `#### Api` header. -Additionally, dashes and underscores are treated as equivalent, so `feat(my-component):` -and `feat(my_component):` will also be grouped together. - -Scope headers are only shown for scopes with more than one entry (single-entry -scope headers aren't useful). Entries without a scope (e.g., `feat: add feature`) -are listed at the bottom of each category section without a sub-header. - -**Example output with scope grouping:** - -```text -### New Features - -#### Api - -- feat(api): add user endpoint by @alice in [#1](https://github.com/...) -- feat(api): add auth endpoint by @bob in [#2](https://github.com/...) - -#### Ui - -- feat(ui): add dashboard by @charlie in [#3](https://github.com/...) - -- feat: general improvement by @dave in [#4](https://github.com/...) -``` - -**Configuration** - -The `changelog` option can be either a string (file path) or an object with more options: - -| Option | Description | -| ------------------------- | ------------------------------------------------------------------------------------------ | -| `changelog` | **optional**. Path to changelog file (string) OR configuration object (see below) | -| `changelog.filePath` | **optional**. Path to the changelog file. Defaults to `CHANGELOG.md` | -| `changelog.policy` | **optional**. Changelog management mode (`none`, `simple`, or `auto`). Defaults to `none`. | -| `changelog.scopeGrouping` | **optional**. Enable scope-based grouping within categories. Defaults to `true`. | -| `changelogPolicy` | **deprecated**. Use `changelog.policy` instead. | - -**Example (`simple` with file path only):** - -```yaml -changelog: CHANGES -``` - -**Example (`simple` with custom file path):** - -```yaml -changelog: - filePath: CHANGES.md - policy: simple +# Publish to all configured targets +craft publish 1.2.3 ``` -**Valid changelog example:** - -```text -## 1.3.5 - -* Removed something - -## 1.3.4 - -* Added something -``` +## Features -**Example (`auto`):** +- **Auto Versioning** - Automatically determine version bumps from conventional commits +- **Multiple Targets** - Publish to GitHub, NPM, PyPI, Docker, Crates.io, NuGet, and more +- **Changelog Management** - Auto-generate changelogs from commits or validate manual entries +- **Workspace Support** - Handle monorepos with NPM/Yarn workspaces +- **CI Integration** - Wait for CI to pass, download artifacts, and publish +- **GitHub Actions** - Built-in actions for release preparation and changelog previews -```yaml -changelog: - policy: auto -``` +## Configuration -**Example (disable scope grouping):** +Create a `.craft.yml` in your project root: ```yaml +minVersion: "2.0.0" changelog: policy: auto - scopeGrouping: false -``` - -**Changelog with staged changes example:** - -```text -## Unreleased - -* Removed something - -## 1.3.4 - -* Added something -``` - -Additionally, `.craft.yml` is used for listing targets where you want to -publish your new release. - -### Minimal Version - -It is possible to specify minimal `craft` version that is required to work with -your configuration. - -**Example:** - -```yaml -minVersion: '0.5.0' -``` - -### Required Files - -You can provide a list of patterns for files that _have to be_ available before -proceeding with publishing. In other words, for every pattern in the given list -there has to be a file present that matches that pattern. This might be helpful -to ensure that we're not trying to do an incomplete release. - -**Example:** - -```yaml -requireNames: - - /^sentry-craft.*\.tgz$/ - - /^gh-pages.zip$/ -``` - -## Status Provider - -You can configure which status providers `craft` will use to check for your build status. -By default, it will use GitHub but you can add more providers if needed. - -**Configuration** - -| Option | Description | -| -------- | -------------------------------------------------------------------------------------------------- | -| `name` | Name of the status provider: only `github` (default) for now. | -| `config` | In case of `github`: may include `contexts` key that contains a list of required contexts (checks) | - -**Example:** - -```yaml -statusProvider: - name: github - config: - contexts: - - Travis CI - Branch -``` - -## Artifact Provider - -You can configure which artifact providers `craft` will use to fetch artifacts from. -By default, GitHub is used, but in case you don't need use any artifacts in your -project, you can set it to `none`. - -**Configuration** - -| Option | Description | -| ------ | ------------------------------------------------------------------- | -| `name` | Name of the artifact provider: `github` (default), `gcs`, or `none` | - -**Example:** - -```yaml -artifactProvider: - name: none -``` - -## Target Configurations - -The configuration specifies which release targets to run for the repository. To -run more targets, list the target identifiers under the `targets` key in -`.craft.yml`. - -**Example:** - -```yaml targets: - - name: npm - name: github - - name: registry - id: browser - type: sdk - onlyIfPresent: /^sentry-browser-.*\.tgz$/ - includeNames: /\.js$/ - checksums: - - algorithm: sha384 - format: base64 - config: - canonical: 'npm:@sentry/browser' - - name: registry - id: node - type: sdk - onlyIfPresent: /^sentry-node-.*\.tgz$/ - config: - canonical: 'npm:@sentry/node' -``` - -### Per-target options - -The following options can be applied to every target individually: - -| Name | Description | -| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `includeNames` | **optional**. Regular expression: only matched files will be processed by the target. There is one special case that `includeNames` supports. | -| `excludeNames` | **optional**. Regular expression: the matched files will be skipped by the target. Matching is performed after testing for inclusion (via `includeNames`). | -| `id` | **optional**. A unique id for the target type so one can refer to that target individually with the `-t` option with the `publish` command like `-t registry[browser]`. (see the example config above) | - -If neither option is included, all artifacts for the release will be processed by the target. - -**Example:** - -```yaml -targets: - - name: github - includeNames: /^.*\.exe$/ - excludeNames: /^test.exe$/ -``` - -### GitHub (`github`) - -Create a release on Github. If a Markdown changelog is present in the -repository, this target tries to read the release name and description from the -changelog. Otherwise, defaults to the tag name and tag's commit message. - -If `previewReleases` is set to `true` (which is the default), the release -created on GitHub will be marked as a pre-release version if the release name -contains any one of [pre-release identifiers](#preview-releases-prerelease). - -**Environment** - -| Name | Description | -| -------------- | ------------------------------------------------------------------ | -| `GITHUB_TOKEN` | Personal GitHub API token (see ) | - -**Configuration** - -| Option | Description | -| ----------------- | ------------------------------------------------------------------------------------------------ | -| `tagPrefix` | **optional**. Prefix for new git tags (e.g. "v"). Empty by default. | -| `previewReleases` | **optional**. Automatically detect and create preview releases. `true` by default. | -| `tagOnly` | **optional**. If set to `true`, only create a tag (without a GitHub release).`false` by default. | - -**Example:** - -```yaml -targets: - - name: github - tagPrefix: v - previewReleases: false -``` - -### NPM (`npm`) - -Releases an NPM package to the public registry. This requires a package tarball -generated by `npm pack` in the artifacts. The file will be uploaded to the -registry with `npm publish`, or with `yarn publish` if `npm` is not found. This -requires NPM to be authenticated with sufficient permissions to publish the package. - -**Environment** - -The `npm` utility must be installed on the system. - -| Name | Description | -| ------------------- | ------------------------------------------------------------------- | -| `NPM_TOKEN` | An [automation token][npm-automation-token] allowed to publish. | -| `NPM_BIN` | **optional**. Path to the npm executable. Defaults to `npm` | -| `YARN_BIN` | **optional**. Path to the yarn executable. Defaults to `yarn` | -| `CRAFT_NPM_USE_OTP` | **optional**. If set to "1", you will be asked for an OTP (for 2FA) | - -[npm-automation-token]: https://docs.npmjs.com/creating-and-viewing-access-tokens - -**Configuration** - -| Option | Description | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------- | -| `access` | **optional**. Visibility for scoped packages: `restricted` (default) or `public` | -| `checkPackageName` | **optional**. If defined, check this package on the registry to get the current latest version to compare for the `latest` tag. The package(s) to be published will only be tagged with `latest` if the new version is greater than the checked package's version| - -**Example** - -```yaml -targets: - name: npm access: public ``` -### Python Package Index (`pypi`) - -Uploads source dists and wheels to the Python Package Index via [twine](https://pypi.org/project/twine/). -The source code bundles and/or wheels must be in the release assets. - -**Environment** - -The `twine` Python package must be installed on the system. - -| Name | Description | -| ---------------- | ----------------------------------------------------- | -| `TWINE_USERNAME` | User name for PyPI with access rights for the package | -| `TWINE_PASSWORD` | Password for the PyPI user | -| `TWINE_BIN` | **optional**. Path to twine. Defaults to `twine` | - -**Configuration** - -_none_ - -**Example** - -```yaml -targets: - - name: pypi -``` - -### Sentry internal PyPI (`sentry-pypi`) - -Creates a GitHub pull request to import the package into a repo set up -like [getsentry/pypi] - -[getsentry/pypi]: https://github.com/getsentry/pypi - -**Environment** - -| Name | Description | -| -------------- | ------------------------------------------------------------------ | -| `GITHUB_TOKEN` | Personal GitHub API token (see ) | - -**Configuration** - -| Option | Description | -| ------------------ | ------------------------------------ | -| `internalPypiRepo` | GitHub repo containing pypi metadata | - -**Example** - -```yaml -targets: - - name: pypi - - name: sentry-pypi - internalPypiRepo: getsentry/pypi -``` - -### Homebrew (`brew`) - -Pushes a new or updated homebrew formula to a brew tap repository. The formula -is committed directly to the master branch of the tap on GitHub, therefore the -bot needs rights to commit to `master` on that repository. Therefore, formulas -on `homebrew/core` are not supported, yet. - -The tap is configured with the mandatory `tap` parameter in the same format as -the `brew` utility. A tap `/` will expand to the GitHub repository -`github.com:/homebrew-`. - -The formula contents are given as configuration value and can be interpolated -with Mustache template syntax (`{{ variable }}`). The interpolation context -contains the following variables: - -- `version`: The new version -- `revision`: The tag's commit SHA -- `checksums`: A map containing sha256 checksums for every release asset. Use - the full filename to access the sha, e.g. `checksums.MyProgram-x86`. If the - filename contains dots (`.`), they are being replaced with `__`. If the - filename contains the currently released version, it is replaced with `__VERSION__`. - For example, `sentry-wizard-v3.9.3.tgz` checksums will be accessible by the key - `checksums.sentry-wizard-v__VERSION____tgz`. - -**Environment** - -| Name | Description | -| -------------- | ------------------------------------------------------------------ | -| `GITHUB_TOKEN` | Personal GitHub API token (seeh ttps://github.com/settings/tokens) | - -**Configuration** - -| Option | Description | -| ---------- | ------------------------------------------------------------------ | -| `tap` | The name of the homebrew tap used to access the GitHub repo | -| `template` | The template for contents of the formula file (ruby code) | -| `formula` | **optional**. Name of the formula. Defaults to the repository name | -| `path` | **optional**. Path to store the formula in. Defaults to `Formula` | - -**Example** - -```yaml -targets: - - name: brew - tap: octocat/tools # Expands to github.com:octocat/homebrew-tools - formula: myproject # Creates the file myproject.rb - path: HomebrewFormula # Creates the file in HomebrewFormula/ - template: > - class MyProject < Formula - desc "This is a test for homebrew formulae" - homepage "https://github.com/octocat/my-project" - url "https://github.com/octocat/my-project/releases/download/{{version}}/binary-darwin" - version "{{version}}" - sha256 "{{checksums.binary-darwin}}" - - def install - mv "binary-darwin", "myproject" - bin.install "myproject" - end - end -``` - -### NuGet (`nuget`) - -Uploads packages to [NuGet](https://www.nuget.org/) via [.NET Core](https://github.com/dotnet/core). -Normally, `craft` targets raise an exception when trying to release a version that already exists. *This target diverges from the norm and allows re-entrant publishing* as it can publish multiple packages at once and the processes might get interrupted. This behavior allows us to finalize half-finished releases without having to publish a new version and play cat & mouse with the flaky upstream package repository. - -**Environment** - -The `dotnet` tool must be available on the system. - -| Name | Description | -| ------------------ | ----------------------------------------------------------------- | -| `NUGET_API_TOKEN` | NuGet personal [API token](https://www.nuget.org/account/apikeys) | -| `NUGET_DOTNET_BIN` | **optional**. Path to .NET Core. Defaults to `dotnet` | - -**Configuration** - -_none_ - -**Example** - -```yaml -targets: - - name: nuget -``` - -### Rust Crates (`crates`) - -Publishes a single Rust package or entire workspace on the public crate registry -([crates.io](https://crates.io)). If the workspace contains multiple crates, -they are published in an order depending on their dependencies. - -**Environment** - -"cargo" must be installed and configured on the system. - -| Name | Description | -| ----------------- | ------------------------------------------------- | -| `CRATES_IO_TOKEN` | The access token to the crates.io account | -| `CARGO_BIN` | **optional**. Path to cargo. Defaults to `cargo`. | - -**Configuration** - -| Option | Description | -| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `noDevDeps` | **optional**. Strips `devDependencies` from crates before publishing. This is useful if a workspace crate uses circular dependencies for docs. Requires [`cargo-hack`](https://github.com/taiki-e/cargo-hack#readme) installed. Defaults to `false`. | - -**Example** - -```yaml -targets: - - name: crates - noDevDeps: false -``` - -### Google Cloud Storage (`gcs`) - -Uploads artifacts to a bucket in Google Cloud Storage. - -The bucket paths (`paths`) can be interpolated using Mustache syntax (`{{ variable }}`). The interpolation context contains the following variables: - -- `version`: The new project version -- `revision`: The SHA revision of the new version - -**Environment** - -Google Cloud credentials can be provided using either of the following two environment variables. - -| Name | Description | -| ----------------------------- | ------------------------------------------------------------------------ | -| `CRAFT_GCS_TARGET_CREDS_PATH` | Local filesystem path to Google Cloud credentials (service account file) | -| `CRAFT_GCS_TARGET_CREDS_JSON` | Full service account file contents, as a JSON string | - -If defined, `CRAFT_GCS_TARGET_CREDS_JSON` will be preferred over `CRAFT_GCS_TARGET_CREDS_PATH`. - -_Note:_ `CRAFT_GCS_TARGET_CREDS_JSON` and `CRAFT_GCS_TARGET_CREDS_PATH` were formerly called `CRAFT_GCS_CREDENTIALS_JSON` and `CRAFT_GCS_CREDENTIALS_PATH`, respectively. While those names will continue to work for the foreseeable future, you'll receive a warning encouraging you to switch to the new names. - -**Configuration** - -| Option | Description | -| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `bucket` | The name of the GCS bucket where artifacts are uploaded. | -| `paths` | A list of path objects that represent bucket paths. | -| `paths.path` | Template-aware bucket path, which can contain `{{ version }}` and/or `{{ revision }}`. | -| `paths.metadata` | **optional** [Metadata](https://cloud.google.com/storage/docs/json_api/v1/objects/insert#request_properties_JSON) for uploaded files. By default, it sets `Cache-Control` to `"public, max-age=300"`. | - -**Example** - -```yaml -targets: - - name: gcs - bucket: bucket-name - paths: - - path: release/{{version}}/download - metadata: - cacheControl: `public, max-age=3600` - - path: release/{{revision}}/platform/package -``` - -### GitHub Pages (`gh-pages`) - -Extracts an archive with static assets and pushes them to the specified git -branch (`gh-pages` by default). Thus, it can be used to publish documentation -or any other assets to [GitHub Pages](https://pages.github.com/), so they will be later automatically rendered -by GitHub. - -By default, this target will look for an artifact named `gh-pages.zip`, extract it, -and commit its contents to `gh-pages` branch. - -_WARNING!_ The destination branch will be completely overwritten by the contents -of the archive. - -**Environment** - -_none_ - -**Configuration** - -| Option | Description | -| ------------- | --------------------------------------------------------------------------------------- | -| `branch` | **optional** The name of the branch to push the changes to. `gh-pages` by default. | -| `githubOwner` | **optional** GitHub project owner, defaults to the value from the global configuration. | -| `githubRepo` | **optional** GitHub project name, defaults to the value from the global configuration. | - -**Example** - -```yaml -targets: - - name: gh-pages - branch: gh-pages -``` - -### Sentry Release Registry (`registry`) - -The target will update the Sentry release registry repo() with the latest version of the -project `craft` is used with. The release registry repository will be checked out -locally, and then the new version file will be created there, along with the necessary -symbolic links. - -Two package types are supported: "sdk" and "app". Type "sdk" means that the package -is uploaded to one of the public registries (PyPI, NPM, Nuget, etc.), and that -the corresponding package directory can be found inside "packages" directory of the -release regsitry. Type "app" indicates that the package's version files are located -in "apps" directory of the registry. - -It is strongly discouraged to have multiple `registry` targets in a config as it -supports grouping/batching multiple apps and SDKs in a single target. - -**Environment** - -_none_ - -**Configuration** - -| Option | Description | -| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `apps` | List of `app` configs as a dict, keyed by their canonical names (example: `app:craft`) | -| `sdks` | List of `sdk` configs as a dict, keyed by their canonical names (example: `maven:io.sentry:sentry`) | -| `(sdks\|apps).urlTemplate` | **optional** URL template that will be used to generate download links for "app" package type. | -| `(sdks\|apps).linkPrereleases` | **optional** Update package versions even if the release is a preview release, "false" by default. | -| `(sdks\|apps).checksums` | **optional** A list of checksums that will be computed for matched files (see `includeNames`). Every checksum entry is an object with two attributes: algorithm (one of `sha256`, `sha384`, and `sha512`) and format (`base64` and `hex`). | -| `(sdks\|apps).onlyIfPresent` | **optional** A file pattern. The target will be executed _only_ when the matched file is found. | - -**Example** - -```yaml -targets: - - name: registry - sdks: - 'npm:@sentry/browser': - apps: - 'npm:@sentry/browser': - urlTemplate: 'https://example.com/{{version}}/{{file}}' - checksums: - - algorithm: sha256 - format: hex -``` - -### Cocoapods (`cocoapods`) - -Pushes a new podspec to the central cocoapods repository. The Podspec is fetched -from the Github repository with the revision that is being released. No release -assets are required for this target. - -**Environment** - -The `cocoapods` gem must be installed on the system. - -| Name | Description | -| ----------------------- | ----------------------------------------- | -| `COCOAPODS_TRUNK_TOKEN` | The access token to the cocoapods account | -| `COCOAPODS_BIN` | **optional**. Path to `pod` executable. | - -**Configuration** - -| Option | Description | -| ---------- | ------------------------------------------ | -| `specPath` | Path to the Podspec file in the repository | - -**Example** - -```yaml -targets: - - name: cocoapods - specPath: MyProject.podspec -``` - -### Docker (`docker`) - -Copies an existing source image tagged with the revision SHA to a new target -tagged with the released version. No release assets are required for this target -except for the source image at the provided source image location so it would be -a good idea to add a status check that ensures the source image exists, otherwise -`craft publish` will fail at the copy step, causing an interrupted publish. -This is an issue for other, non-idempotent targets, not for the Docker target. - -**Environment** - -`docker` executable (or something equivalent) with BuildKit must be installed on the system. - -| Name | Description | -| ----------------- | ------------------------------------------ | -| `DOCKER_USERNAME` | The username for the Docker registry. | -| `DOCKER_PASSWORD` | The personal access token for the account. | -| `DOCKER_BIN` | **optional**. Path to `docker` executable. | - -**Configuration** - -| Option | Description | -| -------------- | ------------------------------------------------------------------------ | -| `source` | Path to the source Docker image to be pulled | -| `sourceFormat` | Format for the source image name. Default: `{{{source}}}:{{{revision}}}` | -| `target` | Path to the target Docker image to be pushed | -| `targetFormat` | Format for the target image name. Default: `{{{target}}}:{{{version}}}` | - -**Example** - -```yaml -targets: - - name: docker - source: us.gcr.io/sentryio/craft - target: getsentry/craft -# Optional but strongly recommended -statusProvider: - name: github - config: - contexts: - - Travis CI - Branch # or whatever builds and pushes your source image -``` - -### Ruby Gems Index (`gem`) +See the [configuration reference](https://getsentry.github.io/craft/configuration/) for all options. -Pushes a gem [Ruby Gems](https://rubygems.org). -It also requires you to be logged in with `gem login`. +## Supported Targets -**Environment** +| Target | Description | +|--------|-------------| +| `github` | GitHub releases and tags | +| `npm` | NPM registry (with workspace support) | +| `pypi` | Python Package Index | +| `crates` | Rust crates.io | +| `nuget` | .NET NuGet | +| `docker` | Docker registries | +| `brew` | Homebrew formulas | +| `gcs` | Google Cloud Storage | +| `gh-pages` | GitHub Pages | +| `cocoapods` | CocoaPods | +| `gem` | RubyGems | +| `maven` | Maven Central | +| `hex` | Elixir Hex | +| `pub-dev` | Dart/Flutter pub.dev | +| `aws-lambda-layer` | AWS Lambda layers | +| `powershell` | PowerShell Gallery | -`gem` must be installed on the system. +See the [targets documentation](https://getsentry.github.io/craft/targets/) for configuration details. -| Name | Description | -| --------- | --------------------------------------------------------- | -| `GEM_BIN` | **optional**. Path to "gem" executable. Defaults to `gem` | +## GitHub Actions -**Configuration** +Craft provides GitHub Actions for automating releases and previewing changelog entries. -_none_ +### Prepare Release Action -**Example** +Automates the `craft prepare` workflow in GitHub Actions: ```yaml -targets: - - name: gem -``` - -### AWS Lambda Layer (`aws-lambda-layer`) +name: Release +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release (or "auto")' + required: false -The target will create a new public lambda layer in each available region with -the extracted artifact from the artifact provider, and update the Sentry release -registry with the new layer versions afterwards. - -**Environment** - -| Name | Description | -| --------------------- | -------------------------------------------------------------------------- | -| AWS_ACCESS_KEY | The access key of the AWS account to create and publish the layers. | -| AWS_SECRET_ACCESS_KEY | The secret access key of the AWS account to create and publish the layers. | - -**Configuration** - -| Option | Description | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | -| linkPrereleases | **optional** Updates layer versions even if the release is a preview release, `false` by default. | -| includeNames | **optional** Exists for all targets, [see here](##per-target-options). It must filter exactly one artifact. | -| layerName | The name of the layer to be published. | -| compatibleRuntimes | A list of compatible runtimes for the layer. Each compatible runtime consists on the name of the runtime and a list of compatible versions. | -| license | The license of the layer. | - -**Example** - -```yaml -targets: - - name: aws-lambda-layer - includeNames: /^sentry-node-serverless-\d+(\.\d+)*\.zip$/ - layerName: SentryNodeServerlessSDK - compatibleRuntimes: - - name: node - versions: - - nodejs10.x - - nodejs12.x - license: MIT +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: getsentry/craft@v2 + with: + version: ${{ github.event.inputs.version }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` -### Unity Package Manager (`upm`) - -Pulls the package as a zipped artifact and pushes the unzipped content to the target repository, tagging it with the provided version. - -_WARNING!_ The destination repository will be completely overwritten. - -**Environment** - -_none_ - -**Configuration** - -| Option | Description | -| ------------------ | --------------------------------------- | -| `releaseRepoOwner` | Name of the owner of the release target | -| `releaseRepoName` | Name of the repo of the release target | - -**Example** - -```yaml -targets: - - name: upm - releaseRepoOwner: 'getsentry' - releaseRepoName: 'unity' -``` - -### Maven central (`maven`) - -PGP signs and publishes packages to Maven Central. - -Note: in order to see the output of the commands, set the [logging level](#logging-level) to `trace`. - -**Environment** +**Inputs:** -| Name | Description | -| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| `OSSRH_USERNAME` | Username of Sonatype repository. | -| `OSSRH_PASSWORD` | Password of Sonatype repository. | -| `GPG_PASSPHRASE` | Passphrase for your default GPG Private Key. | -| `GPG_PRIVATE_KEY` | **optional** GPG Private Key generated via `gpg --armor --export-secret-keys YOUR_ID`. If not provided, default key from your machine will be used. | +| Input | Description | Default | +|-------|-------------|---------| +| `version` | Version to release (semver, "auto", "major", "minor", "patch") | Uses `versioning.policy` from config | +| `merge_target` | Target branch to merge into | Default branch | +| `force` | Force release even with blockers | `false` | +| `blocker_label` | Label that blocks releases | `release-blocker` | +| `publish_repo` | Repository for publish issues | `{owner}/publish` | -**Configuration** +**Outputs:** -| Option | Description | -| ------------------- | -------------------------------------------------------------------- | -| `mavenCliPath` | Path to the Maven CLI. It must be executable by the calling process. | -| `mavenSettingsPath` | Path to the Maven `settings.xml` file. | -| `mavenRepoId` | ID of the Maven server in the `settings.xml`. | -| `mavenRepoUrl` | URL of the Maven repository. | -| `android` | Android configuration, see below. | -| `kmp` | Kotlin Multiplatform configuration, see below. | +| Output | Description | +|--------|-------------| +| `version` | The resolved version being released | +| `branch` | The release branch name | +| `sha` | The commit SHA on the release branch | +| `changelog` | The changelog for this release | -The Kotlin Multiplatform configuration is optional and `false` by default. -If your project isn't related to Android, you don't need this configuration and -can set the option to `false`. If not, set the following nested elements: +### Changelog Preview (Reusable Workflow) -- `distDirRegex`: pattern of distribution directory names. -- `fileReplaceeRegex`: pattern of substring of distribution module names to be replaced to get the Android distribution file. -- `fileReplacerStr`: string to be replaced in the module names to get the Android distribution file. - -**Example (without Android config)** - -```yaml -targets: - - name: maven - mavenCliPath: scripts/mvnw.cmd - mavenSettingsPath: scripts/settings.xml - mavenRepoId: ossrh - mavenRepoUrl: https://oss.sonatype.org/service/local/staging/deploy/maven2/ - android: false -``` - -**Example (with Android config)** +Posts a preview comment on PRs showing how they'll appear in the changelog: ```yaml -targets: - - name: maven - mavenCliPath: scripts/mvnw.cmd - mavenSettingsPath: scripts/settings.xml - mavenRepoId: ossrh - mavenRepoUrl: https://oss.sonatype.org/service/local/staging/deploy/maven2/ - android: - distDirRegex: /^sentry-android-.*$/ - fileReplaceeRegex: /\d\.\d\.\d(-SNAPSHOT)?/ - fileReplacerStr: release.aar -``` +name: Changelog Preview +on: + pull_request: + types: [opened, synchronize, reopened, edited, labeled] -**Example (with Kotlin Multiplatform config)** - -```yaml -targets: - - name: maven - mavenCliPath: scripts/mvnw.cmd - mavenSettingsPath: scripts/settings.xml - mavenRepoId: ossrh - mavenRepoUrl: https://oss.sonatype.org/service/local/staging/deploy/maven2/ - android: - distDirRegex: /^sentry-android-.*$/ - fileReplaceeRegex: /\d\.\d\.\d(-SNAPSHOT)?/ - fileReplacerStr: release.aar - kmp: - rootDistDirRegex: /sentry-kotlin-multiplatform-[0-9]+.*$/ - appleDistDirRegex: /sentry-kotlin-multiplatform-(macos|ios|tvos|watchos).*/ - klibDistDirRegex: /sentry-kotlin-multiplatform-(js|wasm-js).*/ +jobs: + changelog-preview: + uses: getsentry/craft/.github/workflows/changelog-preview.yml@v2 + secrets: inherit ``` -### Symbol Collector (`symbol-collector`) - -Using the [`symbol-collector`](https://github.com/getsentry/symbol-collector) client, uploads native symbols. -The `symbol-collector` needs to be available in the path. +The workflow will: +- Generate the upcoming changelog including the PR's changes +- Highlight entries from the PR using blockquote style (left border) +- Post a comment on the PR with the preview +- Automatically update when you update the PR (push, edit title/description, or change labels) -**Configuration** +## Contributing -| Option | Description | -| ---------------- | -------------------------------------------------------------------------------------------- | -| `serverEndpoint` | **optional** The server endpoint. Defaults to `https://symbol-collector.services.sentry.io`. | -| `batchType` | The batch type of the symbols to be uploaded. I.e: `Android`, `macOS`, `iOS`. | -| `bundleIdPrefix` | The prefix of the bundle ID. The new version will be appended to the end of this prefix. | - -**Example** - -```yaml -targets: - - name: symbol-collector - includeNames: /libsentry(-android)?\.so/ - batchType: Android - bundleIdPrefix: android-ndk- -``` - -### pub.dev (`pub-dev`) - -Pushes a new Dart or Flutter package to [pub.dev](https://pub.dev/). - -Because there is [no automated way](https://github.com/dart-lang/pub-dev/issues/5388) to login and obtain required tokens, you need to perform a valid release beforehand, for every package that you configure. This will open up your browser and use Google's OAuth to log you in, and generate an appropriate file with stored credentials. - -Based on your environment, you can find this file at either `$HOME/.pub-cache/credentials.json` or `$HOME/Library/Application\ Support/dart/pub-credentials.json` for OSX and `$HOME/.config/dart/pub-credentials.json` for Linux, depending on your setup. - -For this target to work correctly, either `dart` must be installed on the system or a valid `dartCliPath` must be provided. - -**Environment** - -| Name | Description | -| ---------------------- | ------------------------------------------------------------ | -| `PUBDEV_ACCESS_TOKEN` | Value of `accessToken` obtained from `pub-credentials.json` | -| `PUBDEV_REFRESH_TOKEN` | Value of `refreshToken` obtained from `pub-credentials.json` | - -**Configuration** - -| Option | Description | -| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `dartCliPath` | **optional** Path to the Dart CLI. It must be executable by the calling process. Defaults to `dart`. | -| `packages` | **optional** List of directories to be released, relative to the root. Useful when a single repository contains multiple packages. When skipped, root directory is assumed as the only package. | -| `skipValidation` | **optional** Publishes the package without going through validation steps, such as analyzer & dependency checks.
This is useful in particular situations when package maintainers know why the validation fails and wish to side step the issue. For example, there may be analyzer issues due to not following the current (latest) dart SDK recommendation because the package needs to maintain the package compatibility with an old SDK version.
This option should be used with caution and only after testing and verifying the reported issue shouldn't affect the package. It is advisable to do an alpha pre-release to further reduce the chance of a potential negative impact. | - -**Example** - -```yaml -targets: - - name: pub-dev - packages: - uno: - dos: - tres: -``` +See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines. -### Hex (`hex`) +## License -Pushes a package to the Elixir / Erlang package manager [Hex](https://hex.pm). - -**Environment** - -`mix` (bundled with the `elixir` language) must be installed on the system. - -| Name | Description | -| ------------- | --------------------------------------------------------- | -| `HEX_API_KEY` | API Key obtained from hex.pm account | -| `MIX_BIN` | **optional**. Path to "mix" executable. Defaults to `mix` | - -**Configuration** - -_none_ - -**Example** - -```yaml -targets: - - name: hex -``` - -### Commit on Git Repository (`commit-on-git-repository`) - -Takes a tarball and pushes the unpacked contents to a git repository. - -**Environment** - -| Name | Description | -| ------------------ | ------------------------------------------------------------------------------------------------ | -| `GITHUB_API_TOKEN` | GitHub PAT that will be used for authentication when a the `repositoryUrl` host is `github.com`. | - -**Configuration** - -| Option | Description | -| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `archive` | Regular expression to match a `.tgz` file in the build artifacts. The content of the found file will be pushed to the git repository. Needs to match exactly one file. | -| `repositoryUrl` | Url to the git remote git repository. Must use http or https protocol! (no `git@...`) | -| `branch` | Which repository branch to push to. | -| `stripComponents` | **optional**. How many leading path elements should be removed when unpacking the tarball. Default: 0 (see `tar --strip-components` option) | -| `createTag` | **optional**. Whether to attach a tag to the created commit. The content of the tag is gonna be equal to the release version passed to craft ("NEW-VERSION"). Default: `false` | - -**Example** - -```yaml -targets: - - name: commit-on-git-repository - archive: /^sentry-deno-\d.*\.tgz$/ - repositoryUrl: https://github.com/getsentry/sentry-deno - stripComponents: 1 - branch: main - createTag: true -``` - -### PowerShellGet (`powershell`) - -Uploads a module to [PowerShell Gallery](https://www.powershellgallery.com/) or another repository -supported by [PowerShellGet](https://learn.microsoft.com/en-us/powershell/module/powershellget)'s `Publish-Module`. - -The action looks for an artifact named `.zip` and extracts it to a temporary directory. -The extracted directory is then published as a module. - -#### Environment - -The `pwsh` executable [must be installed](https://github.com/powershell/powershell#get-powershell) on the system. - -| Name | Description | Default | -| -------------------- | ---------------------------------------------------- | --------- | -| `POWERSHELL_API_KEY` | **required** PowerShell Gallery API key | | -| `POWERSHELL_BIN` | **optional** Path to PowerShell binary | `pwsh` | - -#### Configuration - -| Option | Description | Default | -| -------------------- | ---------------------------------------------------- | --------- | -| `module` | **required** Module name. | | -| `repository` | **optional** Repository to publish the package to. | PSGallery | - -#### Example - -```yaml -targets: - - name: powershell - module: Sentry -``` - -## Integrating Your Project with `craft` - -Here is how you can integrate your GitHub project with `craft`: - -1. Set up a workflow that builds your assets and runs your tests. Allow building - release branches (their names follow `release/{VERSION}` by default, - configurable through `releaseBranchPrefix`). - - ```yaml - on: - push: - branches: - - 'release/**' - ``` - -2. Use the official `actions/upload-artifact@v2` action to upload your assets. - Here is an example config (step) of an archive job: - - ```yaml - - name: Archive Artifacts - uses: actions/upload-artifact@v2 - with: - name: ${{ github.sha }} - path: | - ${{ github.workspace }}/*.tgz - ${{ github.workspace }}/packages/tracing/build/** - ${{ github.workspace }}/packages/**/*.tgz - ``` - - A few important things to note: - - - The name of the artifacts is very important and needs to be `name: ${{ github.sha }}`. Craft uses this as a unique id to fetch the artifacts. - - Keep in mind that this action maintains the folder structure and zips everything together. Craft will download the zip and recursively walk it to find all assets. - -3. Add `.craft.yml` configuration file to your project - - - List there all the targets you want to publish to - - Configure additional options (changelog management policy, tag prefix, etc.) - -4. Add a [pre-release script](#pre-release-version-bumping-script-conventions) to your project. -5. Get various [configuration tokens](#global-configuration) -6. Run `craft prepare --publish` and profit! - -## Pre-release (Version-bumping) Script: Conventions - -Among other actions, `craft prepare` runs an external, project-specific command -or script that is responsible for version bumping. By default, this script -should be located at: `./scripts/bump-version.sh`. The command can be configured -by specifying the `preReleaseCommand` configuration option in `craft.yml`. - -The following requirements are on the script interface and functionality: - -- The script should accept at least two arguments. Craft will pass the old ("from") - version and the new ("to") version as the last two arguments, respectively. -- The script must replace all relevant occurrences of the old version string - with the new one. -- The script must not commit the changes made. -- The script must not change the state of the git repository (e.g. changing branches) - -**Example** - -```bash -#!/bin/bash -### Example of a version-bumping script for an NPM project. -### Located at: ./scripts/bump-version.sh -set -eux -OLD_VERSION="${1}" -NEW_VERSION="${2}" - -# Do not tag and commit changes made by "npm version" -export npm_config_git_tag_version=false -npm version "${NEW_VERSION}" -``` - -## Post-release Script: Conventions - -Among other actions, `craft publish` runs an external, project-specific command -or script that can do things like bumping the development version. By default, -this script should be located at: `./scripts/post-release.sh`. Unlike the -pre-release command, this script is not mandatory so if the file does not exist, -`craft` will report this fact and then move along as usual. This command can be -configured by specifying `postReleaseCommand` configuration option in `craft.yml`. - -The following requirements are on the script interface and functionality: - -- The script should accept at least two arguments. Craft will pass the old ("from") - version and the new ("to") version as the last two arguments, respectively. -- The script is responsible for any and all `git` state management as `craft` will - simply exit after running this script as the final step. This means the script - is responsible for committing and pushing any changes that it may have made. - -**Example** - -```bash -#!/bin/bash -### Example of a dev-version-bumping script for a Python project -### Located at: ./scripts/post-release.sh -set -eux -OLD_VERSION="${1}" -NEW_VERSION="${2}" - -# Ensure master branch -git checkout master -# Advance the CalVer release by one-month and add the `.dev0` suffix -./scripts/bump-version.sh '' $(date -d "$(echo $NEW_VERSION | sed -e 's/^\([0-9]\{2\}\)\.\([0-9]\{1,2\}\)\.[0-9]\+$/20\1-\2-1/') 1 month" +%y.%-m.0.dev0) -# Only commit if there are changes, make sure to `pull --rebase` before pushing to avoid conflicts -git diff --quiet || git commit -anm 'meta: Bump new development version' && git pull --rebase && git push -``` +MIT diff --git a/action.yml b/action.yml new file mode 100644 index 00000000..e0aa73f7 --- /dev/null +++ b/action.yml @@ -0,0 +1,208 @@ +name: "Craft Prepare Release" +description: "Prepare a new release using Craft" + +inputs: + version: + description: > + Version to release. Can be a semver string (e.g., "1.2.3"), + a bump type ("major", "minor", "patch"), or "auto" for automatic detection. + required: false + merge_target: + description: Target branch to merge into. Uses the default branch as a fallback. + required: false + force: + description: Force a release even when there are release-blockers + required: false + default: "false" + blocker_label: + description: Label that blocks releases + required: false + default: "release-blocker" + publish_repo: + description: Repository for publish issues (owner/repo format) + required: false + git_user_name: + description: Git committer name + required: false + git_user_email: + description: Git committer email + required: false + path: + description: The path that Craft will run inside + required: false + default: "." + craft_config_from_merge_target: + description: Use the craft config from the merge target branch + required: false + default: "false" + +outputs: + version: + description: The resolved version being released + value: ${{ steps.craft.outputs.version }} + branch: + description: The release branch name + value: ${{ steps.craft.outputs.branch }} + sha: + description: The commit SHA on the release branch + value: ${{ steps.craft.outputs.sha }} + previous_tag: + description: The tag before this release (for diff links) + value: ${{ steps.craft.outputs.previous_tag }} + changelog: + description: The changelog for this release + value: ${{ steps.craft.outputs.changelog }} + +runs: + using: "composite" + steps: + - id: killswitch + name: Check release blockers + shell: bash + run: | + if [[ '${{ inputs.force }}' != 'true' ]] && gh issue list -l '${{ inputs.blocker_label }}' -s open | grep -q '^[0-9]\+[[:space:]]'; then + echo "::error::Open release-blocking issues found (label: ${{ inputs.blocker_label }}), cancelling release..." + gh api -X POST repos/:owner/:repo/actions/runs/$GITHUB_RUN_ID/cancel + fi + + - name: Set git user + shell: bash + run: | + # Use provided values or fall back to triggering actor + GIT_USER_NAME='${{ inputs.git_user_name }}' + GIT_USER_EMAIL='${{ inputs.git_user_email }}' + + if [[ -z "$GIT_USER_NAME" ]]; then + GIT_USER_NAME="${GITHUB_ACTOR}" + fi + if [[ -z "$GIT_USER_EMAIL" ]]; then + GIT_USER_EMAIL="${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com" + fi + + echo "GIT_COMMITTER_NAME=${GIT_USER_NAME}" >> $GITHUB_ENV + echo "GIT_AUTHOR_NAME=${GIT_USER_NAME}" >> $GITHUB_ENV + echo "EMAIL=${GIT_USER_EMAIL}" >> $GITHUB_ENV + + - name: Install Craft + uses: ./install + + - name: Craft Prepare + id: craft + shell: bash + env: + CRAFT_LOG_LEVEL: Debug + working-directory: ${{ inputs.path }} + run: | + # Ensure we have origin/HEAD set + git remote set-head origin --auto + + # Build command with optional flags + CRAFT_ARGS="" + if [[ '${{ inputs.craft_config_from_merge_target }}' == 'true' && -n '${{ inputs.merge_target }}' ]]; then + CRAFT_ARGS="--config-from ${{ inputs.merge_target }}" + fi + + # Version is optional - if not provided, Craft uses versioning.policy from config + VERSION_ARG="" + if [[ -n '${{ inputs.version }}' ]]; then + VERSION_ARG="${{ inputs.version }}" + fi + + craft prepare $VERSION_ARG $CRAFT_ARGS + + - name: Read Craft Targets + id: craft-targets + shell: bash + working-directory: ${{ inputs.path }} + env: + CRAFT_LOG_LEVEL: Warn + run: | + targets=$(craft targets | jq -r '.[]|" - [ ] \(.)"') + + # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings + echo "targets<> "$GITHUB_OUTPUT" + echo "$targets" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + - name: Request publish + shell: bash + run: | + if [[ '${{ inputs.path }}' == '.' ]]; then + subdirectory='' + else + subdirectory='/${{ inputs.path }}' + fi + + if [[ -n '${{ inputs.merge_target }}' ]]; then + merge_target='${{ inputs.merge_target }}' + else + merge_target='(default)' + fi + + # Use resolved version from Craft output + RESOLVED_VERSION="${{ steps.craft.outputs.version }}" + if [[ -z "$RESOLVED_VERSION" ]]; then + echo "::error::Craft did not output a version. This is unexpected." + exit 1 + fi + + title="publish: ${GITHUB_REPOSITORY}${subdirectory}@${RESOLVED_VERSION}" + + # Determine publish repo + PUBLISH_REPO='${{ inputs.publish_repo }}' + if [[ -z "$PUBLISH_REPO" ]]; then + PUBLISH_REPO="${GITHUB_REPOSITORY_OWNER}/publish" + fi + + # Check if issue already exists + # GitHub only allows search with the "in" operator and this issue search can + # return non-exact matches. We extract the titles and check with grep -xF + if gh -R "$PUBLISH_REPO" issue list -S "'$title' in:title" --json title -q '.[] | .title' | grep -qxF -- "$title"; then + echo "There's already an open publish request, skipped issue creation." + exit 0 + fi + + # Use Craft outputs for git info + RELEASE_BRANCH="${{ steps.craft.outputs.branch }}" + RELEASE_SHA="${{ steps.craft.outputs.sha }}" + PREVIOUS_TAG="${{ steps.craft.outputs.previous_tag }}" + + # Fall back to HEAD if no previous tag + if [[ -z "$PREVIOUS_TAG" ]]; then + PREVIOUS_TAG="HEAD" + fi + + # Build changelog section if available + CHANGELOG='${{ steps.craft.outputs.changelog }}' + if [[ -n "$CHANGELOG" ]]; then + CHANGELOG_SECTION=" + --- + +
+ 📋 Changelog + + ${CHANGELOG} + +
" + else + CHANGELOG_SECTION="" + fi + + body="Requested by: @${GITHUB_ACTOR} + + Merge target: ${merge_target} + + Quick links: + - [View changes](https://github.com/${GITHUB_REPOSITORY}/compare/${PREVIOUS_TAG}...${RELEASE_BRANCH}) + - [View check runs](https://github.com/${GITHUB_REPOSITORY}/commit/${RELEASE_SHA}/checks/) + + Assign the **accepted** label to this issue to approve the release. + To retract the release, the person requesting it must leave a comment containing \`#retract\` on a line by itself under this issue. + + ### Targets + + ${{ steps.craft-targets.outputs.targets }} + + Checked targets will be skipped (either already published or user-requested skip). Uncheck to retry a target. + ${CHANGELOG_SECTION}" + gh issue create -R "$PUBLISH_REPO" --title "$title" --body "$body" diff --git a/docs/_site/assets/main.css b/docs/_site/assets/main.css deleted file mode 100644 index 84804805..00000000 --- a/docs/_site/assets/main.css +++ /dev/null @@ -1,158 +0,0 @@ -body { - font-family: 'Oxygen', serif; - color: #46433a; - background-color: #fcfcfc; -} - -header, -main { - padding: 0 20px; -} - -/* ** wrapper div for both header and main ** */ -.wrapper { - margin-top: 10%; -} - -/* ** anchor tags ** */ -a:link, -a:visited, -a:hover, -a:active { - color: #ce534d; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -/* ** main content list ** */ -.main-list-item { - font-weight: bold; - font-size: 1.2em; - margin: 0.8em 0; -} - -/* override the left margin added by font awesome for the main content list, -since it must be aligned with the content */ -.fa-ul.main-list { - margin-left: 0; -} - -/* list icons */ -.main-list-item-icon { - width: 36px; - color: #46433a; -} - -/* ** logo ** */ -.logo-container { - text-align: center; -} - -.logo { - width: 160px; - height: 160px; - display: inline-block; - background-size: cover; - border: 2px solid #fcfcfc; -} - -/* ** author ** */ -.author-container h1 { - font-size: 1.6em; - margin-top: 0; - margin-bottom: 0; - text-align: center; -} - -/* ** tagline ** */ -.tagline-container p { - font-size: 1.3em; - text-align: center; - margin-bottom: 2em; -} - -/* **** */ -hr { - border: 0; - height: 1px; - background-image: -webkit-linear-gradient( - left, - rgba(0, 0, 0, 0), - #46433a, - rgba(0, 0, 0, 0) - ); - background-image: -moz-linear-gradient( - left, - rgba(0, 0, 0, 0), - #46433a, - rgba(0, 0, 0, 0) - ); - background-image: -ms-linear-gradient( - left, - rgba(0, 0, 0, 0), - #46433a, - rgba(0, 0, 0, 0) - ); - background-image: -o-linear-gradient( - left, - rgba(0, 0, 0, 0), - #46433a, - rgba(0, 0, 0, 0) - ); -} - -/* ** footer ** */ -footer { - position: fixed; - bottom: 0; - right: 0; - height: 20px; -} - -.poweredby { - font-family: 'Arial Narrow', Arial; - font-size: 0.6em; - line-height: 0.6em; - padding: 0 5px; -} - -/* ** media queries ** */ -/* X-Small devices (phones, 480px and up) */ -@media (min-width: 480px) { - /* wrapper stays 480px wide past 480px wide and is kept centered */ - .wrapper { - width: 480px; - margin: 10% auto 0 auto; - } -} -/* All other devices (768px and up) */ -@media (min-width: 768px) { - /* past 768px the layout is changed and the wrapper has a fixed width of 760px - to accomodate both the header column and the content column */ - .wrapper { - width: 760px; - } - - /* the header column stays left and has a dynamic width with all contents - aligned right */ - header { - float: left; - width: 46%; - text-align: right; - } - - .author-container h1, - .logo-container, - .tagline-container p { - text-align: right; - } - - main { - width: 46%; - margin-left: 54%; - padding: 0; - } -} diff --git a/docs/_site/assets/normalize.css b/docs/_site/assets/normalize.css deleted file mode 100644 index d9f54fd9..00000000 --- a/docs/_site/assets/normalize.css +++ /dev/null @@ -1,427 +0,0 @@ -/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ - -/** - * 1. Set default font family to sans-serif. - * 2. Prevent iOS text size adjust after orientation change, without disabling - * user zoom. - */ - -html { - font-family: sans-serif; /* 1 */ - -ms-text-size-adjust: 100%; /* 2 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/** - * Remove default margin. - */ - -body { - margin: 0; -} - -/* HTML5 display definitions - ========================================================================== */ - -/** - * Correct `block` display not defined for any HTML5 element in IE 8/9. - * Correct `block` display not defined for `details` or `summary` in IE 10/11 - * and Firefox. - * Correct `block` display not defined for `main` in IE 11. - */ - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -menu, -nav, -section, -summary { - display: block; -} - -/** - * 1. Correct `inline-block` display not defined in IE 8/9. - * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. - */ - -audio, -canvas, -progress, -video { - display: inline-block; /* 1 */ - vertical-align: baseline; /* 2 */ -} - -/** - * Prevent modern browsers from displaying `audio` without controls. - * Remove excess height in iOS 5 devices. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/** - * Address `[hidden]` styling not present in IE 8/9/10. - * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. - */ - -[hidden], -template { - display: none; -} - -/* Links - ========================================================================== */ - -/** - * Remove the gray background color from active links in IE 10. - */ - -a { - background-color: transparent; -} - -/** - * Improve readability when focused and also mouse hovered in all browsers. - */ - -a:active, -a:hover { - outline: 0; -} - -/* Text-level semantics - ========================================================================== */ - -/** - * Address styling not present in IE 8/9/10/11, Safari, and Chrome. - */ - -abbr[title] { - border-bottom: 1px dotted; -} - -/** - * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. - */ - -b, -strong { - font-weight: bold; -} - -/** - * Address styling not present in Safari and Chrome. - */ - -dfn { - font-style: italic; -} - -/** - * Address variable `h1` font-size and margin within `section` and `article` - * contexts in Firefox 4+, Safari, and Chrome. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/** - * Address styling not present in IE 8/9. - */ - -mark { - background: #ff0; - color: #000; -} - -/** - * Address inconsistent and variable font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` affecting `line-height` in all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove border when inside `a` element in IE 8/9/10. - */ - -img { - border: 0; -} - -/** - * Correct overflow not hidden in IE 9/10/11. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* Grouping content - ========================================================================== */ - -/** - * Address margin not present in IE 8/9 and Safari. - */ - -figure { - margin: 1em 40px; -} - -/** - * Address differences between Firefox and other browsers. - */ - -hr { - -moz-box-sizing: content-box; - box-sizing: content-box; - height: 0; -} - -/** - * Contain overflow in all browsers. - */ - -pre { - overflow: auto; -} - -/** - * Address odd `em`-unit font size rendering in all browsers. - */ - -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; -} - -/* Forms - ========================================================================== */ - -/** - * Known limitation: by default, Chrome and Safari on OS X allow very limited - * styling of `select`, unless a `border` property is set. - */ - -/** - * 1. Correct color not being inherited. - * Known issue: affects color of disabled elements. - * 2. Correct font properties not being inherited. - * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. - */ - -button, -input, -optgroup, -select, -textarea { - color: inherit; /* 1 */ - font: inherit; /* 2 */ - margin: 0; /* 3 */ -} - -/** - * Address `overflow` set to `hidden` in IE 8/9/10/11. - */ - -button { - overflow: visible; -} - -/** - * Address inconsistent `text-transform` inheritance for `button` and `select`. - * All other form control elements do not inherit `text-transform` values. - * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. - * Correct `select` style inheritance in Firefox. - */ - -button, -select { - text-transform: none; -} - -/** - * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` - * and `video` controls. - * 2. Correct inability to style clickable `input` types in iOS. - * 3. Improve usability and consistency of cursor style between image-type - * `input` and others. - */ - -button, -html input[type="button"], /* 1 */ -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; /* 2 */ - cursor: pointer; /* 3 */ -} - -/** - * Re-set default cursor for disabled elements. - */ - -button[disabled], -html input[disabled] { - cursor: default; -} - -/** - * Remove inner padding and border in Firefox 4+. - */ - -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -/** - * Address Firefox 4+ setting `line-height` on `input` using `!important` in - * the UA stylesheet. - */ - -input { - line-height: normal; -} - -/** - * It's recommended that you don't attempt to style these elements. - * Firefox's implementation doesn't respect box-sizing, padding, or width. - * - * 1. Address box sizing set to `content-box` in IE 8/9/10. - * 2. Remove excess padding in IE 8/9/10. - */ - -input[type='checkbox'], -input[type='radio'] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Fix the cursor style for Chrome's increment/decrement buttons. For certain - * `font-size` values of the `input`, it causes the cursor style of the - * decrement button to change from `default` to `text`. - */ - -input[type='number']::-webkit-inner-spin-button, -input[type='number']::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Address `appearance` set to `searchfield` in Safari and Chrome. - * 2. Address `box-sizing` set to `border-box` in Safari and Chrome - * (include `-moz` to future-proof). - */ - -input[type='search'] { - -webkit-appearance: textfield; /* 1 */ - -moz-box-sizing: content-box; - -webkit-box-sizing: content-box; /* 2 */ - box-sizing: content-box; -} - -/** - * Remove inner padding and search cancel button in Safari and Chrome on OS X. - * Safari (but not Chrome) clips the cancel button when the search input has - * padding (and `textfield` appearance). - */ - -input[type='search']::-webkit-search-cancel-button, -input[type='search']::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * Define consistent border, margin, and padding. - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/** - * 1. Correct `color` not being inherited in IE 8/9/10/11. - * 2. Remove padding so people aren't caught out if they zero out fieldsets. - */ - -legend { - border: 0; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Remove default vertical scrollbar in IE 8/9/10/11. - */ - -textarea { - overflow: auto; -} - -/** - * Don't inherit the `font-weight` (applied by a rule above). - * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. - */ - -optgroup { - font-weight: bold; -} - -/* Tables - ========================================================================== */ - -/** - * Remove most spacing between table cells. - */ - -table { - border-collapse: collapse; - border-spacing: 0; -} - -td, -th { - padding: 0; -} diff --git a/docs/_site/favicon.ico b/docs/_site/favicon.ico deleted file mode 100644 index d90a3de2..00000000 Binary files a/docs/_site/favicon.ico and /dev/null differ diff --git a/docs/_site/images/sentry-glyph-black.png b/docs/_site/images/sentry-glyph-black.png deleted file mode 100644 index 787c55fe..00000000 Binary files a/docs/_site/images/sentry-glyph-black.png and /dev/null differ diff --git a/docs/_site/index.html b/docs/_site/index.html deleted file mode 100644 index 020b0e19..00000000 --- a/docs/_site/index.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - • Sentry Craft - - - - - - - - - - - - - - - - - - - - - - -
-
-
- -
- -
-

-
-
-
-
-

Sentry Craft

-

- "Craft" is a command line tool that helps to automate and pipeline - package releases. -

-
- -
-
-
- - diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs new file mode 100644 index 00000000..9046114c --- /dev/null +++ b/docs/astro.config.mjs @@ -0,0 +1,43 @@ +import { defineConfig } from 'astro/config'; +import starlight from '@astrojs/starlight'; + +// Allow base path override via environment variable for PR previews +const base = process.env.DOCS_BASE_PATH || '/craft'; + +export default defineConfig({ + site: 'https://getsentry.github.io', + base: base, + integrations: [ + starlight({ + title: 'Craft', + logo: { + src: './src/assets/logo.svg', + }, + social: { + github: 'https://github.com/getsentry/craft', + }, + sidebar: [ + { + label: 'Getting Started', + items: [ + { label: 'Introduction', slug: '' }, + { label: 'Installation', slug: 'getting-started' }, + { label: 'Configuration', slug: 'configuration' }, + { label: 'GitHub Actions', slug: 'github-actions' }, + ], + }, + { + label: 'Targets', + autogenerate: { directory: 'targets' }, + }, + { + label: 'Resources', + items: [ + { label: 'Contributing', slug: 'contributing' }, + ], + }, + ], + customCss: [], + }), + ], +}); diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..2cc74a4e --- /dev/null +++ b/docs/package.json @@ -0,0 +1,15 @@ +{ + "name": "craft-docs", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview" + }, + "dependencies": { + "@astrojs/starlight": "^0.31.1", + "astro": "^5.1.1", + "sharp": "^0.33.5" + } +} diff --git a/docs/public/favicon.svg b/docs/public/favicon.svg new file mode 100644 index 00000000..e839048d --- /dev/null +++ b/docs/public/favicon.svg @@ -0,0 +1 @@ + diff --git a/docs/src/assets/logo.svg b/docs/src/assets/logo.svg new file mode 100644 index 00000000..b67cc92f --- /dev/null +++ b/docs/src/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/docs/src/content/config.ts b/docs/src/content/config.ts new file mode 100644 index 00000000..31b74762 --- /dev/null +++ b/docs/src/content/config.ts @@ -0,0 +1,6 @@ +import { defineCollection } from 'astro:content'; +import { docsSchema } from '@astrojs/starlight/schema'; + +export const collections = { + docs: defineCollection({ schema: docsSchema() }), +}; diff --git a/docs/src/content/docs/configuration.md b/docs/src/content/docs/configuration.md new file mode 100644 index 00000000..18e6c4f2 --- /dev/null +++ b/docs/src/content/docs/configuration.md @@ -0,0 +1,330 @@ +--- +title: Configuration +description: Complete reference for .craft.yml configuration +--- + +Project configuration for Craft is stored in `.craft.yml` in the project root. + +## GitHub Project + +Craft tries to determine GitHub repo information from the local git repo. You can also hard-code it: + +```yaml +github: + owner: getsentry + repo: sentry-javascript +``` + +## Pre-release Command + +This command runs on your release branch as part of `craft prepare`. Default: `bash scripts/bump-version.sh`. + +```yaml +preReleaseCommand: bash scripts/bump-version.sh +``` + +The script should: +- Accept old and new version as the last two arguments +- Replace version occurrences +- Not commit changes +- Not change git state + +Example script: + +```bash +#!/bin/bash +set -eux +OLD_VERSION="${1}" +NEW_VERSION="${2}" + +export npm_config_git_tag_version=false +npm version "${NEW_VERSION}" +``` + +## Post-release Command + +This command runs after a successful `craft publish`. Default: `bash scripts/post-release.sh`. + +```yaml +postReleaseCommand: bash scripts/post-release.sh +``` + +## Release Branch Name + +Override the release branch prefix. Default: `release`. + +```yaml +releaseBranchPrefix: publish +``` + +Full branch name: `{releaseBranchPrefix}/{version}` + +## Changelog Policies + +Craft supports `simple` and `auto` changelog management modes. + +### Simple Mode + +Reminds you to add a changelog entry: + +```yaml +changelog: CHANGES +``` + +Or with options: + +```yaml +changelog: + filePath: CHANGES.md + policy: simple +``` + +### Auto Mode + +Automatically generates changelog from commits: + +```yaml +changelog: + policy: auto +``` + +Auto mode uses `.github/release.yml` to categorize PRs by labels or commit patterns. If not present, it uses default [Conventional Commits](https://www.conventionalcommits.org/) patterns: + +| Category | Pattern | +|----------|---------| +| Breaking Changes | `^\w+(\(\w+\))?!:` | +| Build / dependencies | `^(build\|ref\|chore\|ci)(\(\w+\))?:` | +| Bug Fixes | `^fix(\(\w+\))?:` | +| Documentation | `^docs?(\(\w+\))?:` | +| New Features | `^feat(\(\w+\))?:` | + +Example `.github/release.yml`: + +```yaml +changelog: + categories: + - title: Features + labels: + - enhancement + commit_patterns: + - "^feat(\\(\\w+\\))?:" + - title: Bug Fixes + labels: + - bug + commit_patterns: + - "^fix(\\(\\w+\\))?:" +``` + +### Custom Changelog Entries from PR Descriptions + +By default, the changelog entry for a PR is generated from its title. However, +PR authors can override this by adding a "Changelog Entry" section to the PR +description. This allows for more detailed, user-facing changelog entries without +cluttering the PR title. + +To use this feature, add a markdown heading (level 2 or 3) titled "Changelog Entry" +to your PR description, followed by the desired changelog text: + +```markdown +### Description + +Add `foo` function, and add unit tests to thoroughly check all edge cases. + +### Changelog Entry + +Add a new function called `foo` which prints "Hello, world!" + +### Issues + +Closes #123 +``` + +The text under "Changelog Entry" will be used verbatim in the changelog instead +of the PR title. If no such section is present, the PR title is used as usual. + +#### Advanced Features + +1. **Multiple Entries**: If you use multiple top-level bullet points in the + "Changelog Entry" section, each bullet will become a separate changelog entry: + + ```markdown + ### Changelog Entry + + - Add OAuth2 authentication + - Add two-factor authentication + - Add session management + ``` + +2. **Nested Content**: Indented bullets (4+ spaces or tabs) are preserved as + nested content under their parent entry: + + ```markdown + ### Changelog Entry + + - Add authentication system + - OAuth2 support + - Two-factor authentication + - Session management + ``` + + This will generate: + ```markdown + - Add authentication system by @user in [#123](url) + - OAuth2 support + - Two-factor authentication + - Session management + ``` + +3. **Plain Text**: If no bullets are used, the entire content is treated as a + single changelog entry. Multi-line text is supported. + +4. **Content Isolation**: Only content within the "Changelog Entry" section is + included in the changelog. Other sections (Description, Issues, etc.) are + ignored. + +### Scope Grouping + +Changes are automatically grouped by scope (e.g., `feat(api):` groups under "Api"): + +```yaml +changelog: + policy: auto + scopeGrouping: true # default +``` + +Scope headers are only shown for scopes with more than one entry. Entries without +a scope are listed at the bottom of each category section without a sub-header. + +Example output with scope grouping: + +```text +### New Features + +#### Api + +- feat(api): add user endpoint by @alice in [#1](https://github.com/...) +- feat(api): add auth endpoint by @bob in [#2](https://github.com/...) + +#### Ui + +- feat(ui): add dashboard by @charlie in [#3](https://github.com/...) + +- feat: general improvement by @dave in [#4](https://github.com/...) +``` + +### Configuration Options + +| Option | Description | +|--------|-------------| +| `changelog` | Path to changelog file (string) OR configuration object | +| `changelog.filePath` | Path to changelog file. Default: `CHANGELOG.md` | +| `changelog.policy` | Mode: `none`, `simple`, or `auto`. Default: `none` | +| `changelog.scopeGrouping` | Enable scope-based grouping. Default: `true` | + +## Versioning + +Configure default versioning behavior: + +```yaml +versioning: + policy: auto # auto, manual, or calver +``` + +### Versioning Policies + +| Policy | Description | +|--------|-------------| +| `auto` | Analyze commits to determine version bump (default when using `craft prepare auto`) | +| `manual` | Require explicit version argument | +| `calver` | Use calendar-based versioning | + +### Calendar Versioning (CalVer) + +For projects using calendar-based versions: + +```yaml +versioning: + policy: calver + calver: + format: "%y.%-m" # e.g., 24.12 for December 2024 + offset: 14 # Days to look back for date calculation +``` + +Format supports: +- `%y` - 2-digit year +- `%m` - Zero-padded month +- `%-m` - Month without padding + +## Minimal Version + +Require a minimum Craft version: + +```yaml +minVersion: '0.5.0' +``` + +## Required Files + +Ensure specific artifacts exist before publishing: + +```yaml +requireNames: + - /^sentry-craft.*\.tgz$/ + - /^gh-pages.zip$/ +``` + +## Status Provider + +Configure build status checks: + +```yaml +statusProvider: + name: github + config: + contexts: + - Travis CI - Branch +``` + +## Artifact Provider + +Configure where to fetch artifacts from: + +```yaml +artifactProvider: + name: github # or 'gcs' or 'none' +``` + +## Targets + +List release targets in your configuration: + +```yaml +targets: + - name: npm + - name: github + - name: registry + id: browser + type: sdk + onlyIfPresent: /^sentry-browser-.*\.tgz$/ +``` + +See [Target Configurations](./targets/) for details on each target. + +### Per-target Options + +These options apply to all targets: + +| Option | Description | +|--------|-------------| +| `includeNames` | Regex: only matched files are processed | +| `excludeNames` | Regex: matched files are skipped | +| `id` | Unique ID for the target (use with `-t target[id]`) | + +Example: + +```yaml +targets: + - name: github + includeNames: /^.*\.exe$/ + excludeNames: /^test.exe$/ +``` diff --git a/docs/src/content/docs/contributing.md b/docs/src/content/docs/contributing.md new file mode 100644 index 00000000..0f537a00 --- /dev/null +++ b/docs/src/content/docs/contributing.md @@ -0,0 +1,129 @@ +--- +title: Contributing +description: How to contribute to Craft +--- + +Thank you for your interest in contributing to Craft! This guide will help you get started. + +## Development Setup + +### Prerequisites + +- Node.js v22+ (managed by [Volta](https://volta.sh/)) +- Yarn v1 + +### Installation + +```bash +# Clone the repository +git clone https://github.com/getsentry/craft.git +cd craft + +# Install dependencies +yarn install --frozen-lockfile +``` + +## Development Commands + +| Command | Description | +|---------|-------------| +| `yarn build` | Build the project (outputs to `dist/craft`) | +| `yarn test` | Run tests | +| `yarn lint` | Run ESLint | +| `yarn fix` | Auto-fix lint issues | + +### Manual Testing + +To test your changes locally: + +```bash +yarn build && ./dist/craft +``` + +## Project Structure + +``` +src/ +├── __mocks__/ # Test mocks +├── __tests__/ # Test files (*.test.ts) +├── artifact_providers/ # Artifact provider implementations +├── commands/ # CLI command implementations +├── schemas/ # JSON schema and TypeScript types +├── status_providers/ # Status provider implementations +├── targets/ # Release target implementations +├── types/ # Shared TypeScript types +├── utils/ # Utility functions +├── config.ts # Configuration loading +├── index.ts # CLI entry point +└── logger.ts # Logging utilities +``` + +## Code Style + +- **TypeScript** throughout the codebase +- **Prettier** for formatting (single quotes, no arrow parens) +- **ESLint** with `@typescript-eslint/recommended` +- Unused variables prefixed with `_` are allowed + +## Testing + +- Tests use **Jest** with `ts-jest` +- Test files are in `src/__tests__/` with the `*.test.ts` pattern + +```bash +# Run all tests +yarn test + +# Run tests in watch mode +yarn test:watch +``` + +## Adding a New Target + +1. Create a new file in `src/targets/` (e.g., `myTarget.ts`) +2. Implement the `BaseTarget` interface +3. Register the target in `src/targets/index.ts` +4. Add configuration schema in `src/schemas/` +5. Write tests in `src/__tests__/` +6. Document the target in the docs + +## Pull Requests + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Run tests and linting +5. Submit a pull request + +## Pre-release Script Conventions + +The pre-release script (`scripts/bump-version.sh`) should: + +- Accept old and new version as the last two arguments +- Replace version occurrences in project files +- **Not** commit changes +- **Not** change git state + +Example: + +```bash +#!/bin/bash +set -eux +OLD_VERSION="${1}" +NEW_VERSION="${2}" + +export npm_config_git_tag_version=false +npm version "${NEW_VERSION}" +``` + +## Post-release Script Conventions + +The post-release script (`scripts/post-release.sh`) runs after successful publish and should: + +- Accept old and new version as arguments +- Handle its own git operations (commit, push) + +## Questions? + +- Open an issue on [GitHub](https://github.com/getsentry/craft/issues) +- Check existing issues and pull requests diff --git a/docs/src/content/docs/getting-started.md b/docs/src/content/docs/getting-started.md new file mode 100644 index 00000000..bdfb5e17 --- /dev/null +++ b/docs/src/content/docs/getting-started.md @@ -0,0 +1,228 @@ +--- +title: Getting Started +description: How to install and use Craft +--- + +## Installation + +### Binary + +Craft is [distributed as a minified single JS binary](https://github.com/getsentry/craft/releases/latest). Download the latest release and add it to your PATH. + +### npm (not recommended) + +While the recommended approach is to use the binary directly, you can also install Craft as an [NPM package](https://yarn.pm/@sentry/craft): + +```shell +yarn global add @sentry/craft +``` + +```shell +npm install -g @sentry/craft +``` + +## Usage + +```shell +$ craft -h +craft + +Commands: + craft prepare NEW-VERSION 🚢 Prepare a new release branch + [aliases: p, prerelease, prepublish, prepare, release] + craft publish NEW-VERSION 🛫 Publish artifacts [aliases: pp, publish] + craft targets List defined targets as JSON array + craft config Print the parsed, processed, and validated Craft + config for the current project in pretty-JSON. + craft artifacts 📦 Manage artifacts [aliases: a, artifact] + +Options: + --no-input Suppresses all user prompts [default: false] + --dry-run Dry run mode: do not perform any real actions + --log-level Logging level + [choices: "Fatal", "Error", "Warn", "Log", "Info", "Success", "Debug", + "Trace", "Silent", "Verbose"] [default: "Info"] + -v, --version Show version number [boolean] + -h, --help Show help [boolean] +``` + +## Workflow + +### `craft prepare`: Preparing a New Release + +This command creates a new release branch, checks the changelog entries, runs a version-bumping script, and pushes this branch to GitHub. CI triggered by pushing this branch will build release artifacts and upload them to your artifact provider. + +**Version Specification** + +The `NEW-VERSION` argument can be specified in three ways: + +1. **Explicit version** (e.g., `1.2.3`): Release with the specified version +2. **Bump type** (`major`, `minor`, or `patch`): Automatically increment the latest tag +3. **Auto** (`auto`): Analyze commits since the last tag and determine bump type from conventional commit patterns + +```shell +craft prepare NEW-VERSION + +🚢 Prepare a new release branch + +Positionals: + NEW-VERSION The new version to release. Can be: a semver string (e.g., + "1.2.3"), a bump type ("major", "minor", or "patch"), or "auto" + to determine automatically from conventional commits. + [string] [required] + +Options: + --no-input Suppresses all user prompts [default: false] + --dry-run Dry run mode: do not perform any real actions + --rev, -r Source revision (git SHA or tag) to prepare from + --no-push Do not push the release branch [boolean] [default: false] + --no-git-checks Ignore local git changes and unsynchronized remotes + --no-changelog Do not check for changelog entries [boolean] [default: false] + --publish Run "publish" right after "release"[boolean] [default: false] + --remote The git remote to use when pushing [string] [default: "origin"] + -v, --version Show version number [boolean] + -h, --help Show help [boolean] +``` + +### `craft publish`: Publishing the Release + +This command finds a release branch for the provided version, checks the build status, downloads release artifacts, and uploads them to configured targets. + +```shell +craft publish NEW-VERSION + +🛫 Publish artifacts + +Positionals: + NEW-VERSION Version to publish [string] [required] + +Options: + --no-input Suppresses all user prompts [default: false] + --dry-run Dry run mode: do not perform any real actions + --target, -t Publish to this target [default: "all"] + --rev, -r Source revision (git SHA or tag) to publish + --no-merge Do not merge the release branch after publishing + --keep-branch Do not remove release branch after merging it + --keep-downloads Keep all downloaded files [boolean] [default: false] + --no-status-check Do not check for build status [boolean] [default: false] + -v, --version Show version number [boolean] + -h, --help Show help [boolean] +``` + +### Example + +Let's release version `1.2.3`: + +```shell +# Prepare the release +$ craft prepare 1.2.3 +``` + +This creates a release branch `release/1.2.3`, runs the version-bumping script, commits changes, and pushes to GitHub. CI builds artifacts and uploads them. + +```shell +# Publish the release +$ craft publish 1.2.3 +``` + +This finds the release branch, waits for CI to pass, downloads artifacts, and publishes to configured targets (e.g., GitHub and NPM). + +## Version Naming Conventions + +Craft supports [semantic versioning (semver)](https://semver.org)-like versions: + +```txt +..(-)?(-)? +``` + +- The ``, ``, and `` numbers are required +- The `` and `` identifiers are optional + +### Preview Releases + +Preview or pre-release identifiers **must** include one of: + +```txt +preview|pre|rc|dev|alpha|beta|unstable|a|b +``` + +Examples: +- `1.0.0-preview` +- `1.0.0-alpha.0` +- `1.0.0-beta.1` +- `1.0.0-rc.20` + +### Build Identifiers + +Add a build identifier for platform-specific releases: + +```txt +1.0.0+x86_64 +1.0.0-rc.1+x86_64 +``` + +## Global Configuration + +Configure Craft using environment variables or configuration files. + +All command line flags can be set through environment variables by prefixing them with `CRAFT_`: + +```shell +CRAFT_LOG_LEVEL=Debug +CRAFT_DRY_RUN=1 +CRAFT_NO_INPUT=0 +``` + +Since Craft relies heavily on GitHub, set the `GITHUB_TOKEN` environment variable to a [GitHub Personal Access Token](https://github.com/settings/tokens) with `repo` scope. + +### Environment Files + +Craft reads configuration from these locations (in order of precedence): + +1. `$HOME/.craft.env` +2. `$PROJECT_DIR/.craft.env` +3. Shell environment + +Example `.craft.env`: + +```shell +# ~/.craft.env +GITHUB_TOKEN=token123 +export NUGET_API_TOKEN=abcdefgh +``` + +## Caveats + +- When interacting with remote GitHub repositories, Craft uses the remote `origin` by default. Set `CRAFT_REMOTE` or use the `--remote` option to change this. + +## Integrating Your Project + +1. **Set up a workflow** that builds assets and runs tests. Allow building release branches: + + ```yaml + on: + push: + branches: + - 'release/**' + ``` + +2. **Upload artifacts** using `actions/upload-artifact@v2`: + + ```yaml + - name: Archive Artifacts + uses: actions/upload-artifact@v2 + with: + name: ${{ github.sha }} + path: | + ${{ github.workspace }}/*.tgz + ``` + + Note: The artifact name must be `${{ github.sha }}`. + +3. **Add `.craft.yml`** to your project with targets and options. + +4. **Add a pre-release script** (default: `scripts/bump-version.sh`). + +5. **Configure environment variables** for your targets. + +6. **Run** `craft prepare --publish`! diff --git a/docs/src/content/docs/github-actions.md b/docs/src/content/docs/github-actions.md new file mode 100644 index 00000000..2cffed2b --- /dev/null +++ b/docs/src/content/docs/github-actions.md @@ -0,0 +1,250 @@ +--- +title: GitHub Actions +description: Automate releases and changelog previews with Craft GitHub Actions +--- + +Craft provides GitHub Actions for automating releases and previewing changelog entries in pull requests. + +For a real-world example of using Craft's GitHub Actions, see the [getsentry/publish](https://github.com/getsentry/publish) repository. + +## Prepare Release Action + +The main Craft action automates the `craft prepare` workflow in GitHub Actions. It creates a release branch, updates the changelog, and opens a publish request issue. + +### Basic Usage + +```yaml +name: Release +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release (or "auto")' + required: false + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: getsentry/craft@v2 + with: + version: ${{ github.event.inputs.version }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +### Inputs + +| Input | Description | Default | +|-------|-------------|---------| +| `version` | Version to release. Can be a semver string (e.g., "1.2.3"), a bump type ("major", "minor", "patch"), or "auto" for automatic detection. | Uses `versioning.policy` from config | +| `merge_target` | Target branch to merge into. | Default branch | +| `force` | Force a release even when there are release-blockers. | `false` | +| `blocker_label` | Label that blocks releases. | `release-blocker` | +| `publish_repo` | Repository for publish issues (owner/repo format). | `{owner}/publish` | +| `git_user_name` | Git committer name. | GitHub actor | +| `git_user_email` | Git committer email. | Actor's noreply email | +| `path` | The path that Craft will run inside. | `.` | +| `craft_config_from_merge_target` | Use the craft config from the merge target branch. | `false` | + +### Outputs + +| Output | Description | +|--------|-------------| +| `version` | The resolved version being released | +| `branch` | The release branch name | +| `sha` | The commit SHA on the release branch | +| `previous_tag` | The tag before this release (for diff links) | +| `changelog` | The changelog for this release | + +### Auto-versioning Example + +When using auto-versioning, Craft analyzes conventional commits to determine the version bump: + +```yaml +name: Auto Release +on: + schedule: + - cron: '0 10 * * 1' # Every Monday at 10 AM + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: getsentry/craft@v2 + with: + version: auto + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +## Changelog Preview (Reusable Workflow) + +The changelog preview workflow posts a comment on pull requests showing how they will appear in the changelog. This helps contributors understand the impact of their changes. + +### Basic Usage + +Call the reusable workflow from your repository: + +```yaml +name: Changelog Preview +on: + pull_request: + types: [opened, synchronize, reopened, edited, labeled] + +jobs: + changelog-preview: + uses: getsentry/craft/.github/workflows/changelog-preview.yml@v2 + secrets: inherit +``` + +### Inputs + +| Input | Description | Default | +|-------|-------------|---------| +| `craft-version` | Version of Craft to use (tag or "latest") | `latest` | + +### Pinning a Specific Version + +```yaml +jobs: + changelog-preview: + uses: getsentry/craft/.github/workflows/changelog-preview.yml@v2 + with: + craft-version: "2.15.0" + secrets: inherit +``` + +### How It Works + +1. **Generates the changelog** - Runs `craft changelog --pr --format json` to generate the upcoming changelog with metadata +2. **Fetches PR info** - Gets PR title, body, labels, and base branch from GitHub API +3. **Categorizes the PR** - Matches the PR to changelog categories based on labels and commit patterns +4. **Suggests version bump** - Based on matched categories with semver fields (major/minor/patch) +5. **Highlights PR entries** - The current PR is rendered with blockquote style (displayed with a left border in GitHub) +6. **Posts a comment** - Creates or updates a comment on the PR with the changelog preview +7. **Auto-updates** - The comment is automatically updated when you update the PR (push commits, edit title/description, or change labels) + +### Example Comment + +The workflow posts a comment like this: + +```markdown +## Suggested Version Bump + +🟡 **Minor** (new features) + +## 📋 Changelog Preview + +This is how your changes will appear in the changelog. +Entries from this PR are highlighted with a left border (blockquote style). + +--- + +### New Features ✨ + +> - feat(api): Add new endpoint by @you in #123 + +- feat(core): Existing feature by @other in #100 + +### Bug Fixes 🐛 + +- fix(ui): Resolve crash by @other in #99 + +--- + +🤖 This preview updates automatically when you update the PR. +``` + +### PR Trigger Types + +The workflow supports these PR event types: +- `opened` - When a PR is created +- `synchronize` - When new commits are pushed +- `reopened` - When a closed PR is reopened +- `edited` - When the PR title or description is changed +- `labeled` - When labels are added or removed + +### Requirements + +- Use `secrets: inherit` to pass the GitHub token +- The repository should have a git history with tags for the changelog to be meaningful + +## Skipping Changelog Entries + +### Using Magic Words + +Use `#skip-changelog` in your commit message or PR body to exclude a commit from the changelog: + +``` +chore: Update dependencies + +#skip-changelog +``` + +### Using Labels + +You can configure labels to exclude PRs from the changelog. In your `.craft.yml`: + +```yaml +changelog: + categories: + - title: "New Features ✨" + labels: ["feature", "enhancement"] + - title: "Bug Fixes 🐛" + labels: ["bug", "fix"] + exclude: + labels: ["skip-changelog", "dependencies"] + authors: ["dependabot[bot]", "renovate[bot]"] +``` + +PRs with the `skip-changelog` label or from excluded authors will not appear in the changelog. + +## Tips + +### Combining Both Actions + +You can use both the changelog preview and release actions together for a complete release workflow. See the [getsentry/publish](https://github.com/getsentry/publish) repository for a real-world example. + +```yaml +# .github/workflows/changelog-preview.yml +name: Changelog Preview +on: + pull_request: + types: [opened, synchronize, reopened, edited, labeled] + +jobs: + changelog-preview: + uses: getsentry/craft/.github/workflows/changelog-preview.yml@v2 + secrets: inherit +``` + +```yaml +# .github/workflows/release.yml +name: Release +on: + workflow_dispatch: + inputs: + version: + description: 'Version (leave empty for auto)' + required: false + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: getsentry/craft@v2 + with: + version: ${{ github.event.inputs.version || 'auto' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx new file mode 100644 index 00000000..8052b47c --- /dev/null +++ b/docs/src/content/docs/index.mdx @@ -0,0 +1,67 @@ +--- +title: Craft +description: Universal Release Tool (And More) +template: splash +hero: + tagline: A command line tool that helps to automate and pipeline package releases. + image: + file: ../../assets/logo.svg + actions: + - text: Get Started + link: ./getting-started/ + icon: right-arrow + - text: View on GitHub + link: https://github.com/getsentry/craft + icon: external + variant: minimal +--- + +import { Card, CardGrid } from '@astrojs/starlight/components'; + +## Features + + + + Prepare and publish releases with a single command. Craft handles version bumping, changelog management, and artifact publishing. + + + Automatically determine version bumps from conventional commits. Just run `craft prepare auto` and let Craft figure out the rest. + + + Publish to GitHub, NPM, PyPI, Docker, NuGet, Crates.io, and many more registries from a single configuration. + + + Automatic changelog generation using conventional commits or manual changelog policies. + + + Works seamlessly with GitHub Actions and other CI systems. Fetch artifacts and publish them to your targets. + + + +## Quick Example + +```bash +# Auto-determine version from conventional commits +craft prepare auto + +# Or specify a bump type +craft prepare minor + +# Or use an explicit version +craft prepare 1.2.3 + +# Then publish to all configured targets +craft publish 1.2.3 +``` + +## Why Craft? + +Craft enforces a specific workflow for managing release branches, changelogs, and artifact publishing. It: + +- Creates release branches automatically +- Validates changelog entries +- Runs version-bumping scripts +- Waits for CI to complete +- Downloads build artifacts +- Publishes to configured targets +- Merges release branches back to main diff --git a/docs/src/content/docs/targets/aws-lambda-layer.md b/docs/src/content/docs/targets/aws-lambda-layer.md new file mode 100644 index 00000000..835f1d37 --- /dev/null +++ b/docs/src/content/docs/targets/aws-lambda-layer.md @@ -0,0 +1,48 @@ +--- +title: AWS Lambda Layer +description: Publish Lambda layers to all AWS regions +--- + +Creates a new public Lambda layer in each available AWS region and updates the Sentry release registry. + +## Configuration + +| Option | Description | +|--------|-------------| +| `layerName` | Name of the Lambda layer | +| `compatibleRuntimes` | List of runtime configurations | +| `license` | Layer license | +| `linkPrereleases` | Update for preview releases. Default: `false` | +| `includeNames` | Must filter to exactly one artifact | + +### Runtime Configuration + +```yaml +compatibleRuntimes: + - name: node + versions: + - nodejs10.x + - nodejs12.x +``` + +## Environment Variables + +| Name | Description | +|------|-------------| +| `AWS_ACCESS_KEY` | AWS account access key | +| `AWS_SECRET_ACCESS_KEY` | AWS account secret key | + +## Example + +```yaml +targets: + - name: aws-lambda-layer + includeNames: /^sentry-node-serverless-\d+(\.\d+)*\.zip$/ + layerName: SentryNodeServerlessSDK + compatibleRuntimes: + - name: node + versions: + - nodejs10.x + - nodejs12.x + license: MIT +``` diff --git a/docs/src/content/docs/targets/brew.md b/docs/src/content/docs/targets/brew.md new file mode 100644 index 00000000..dd62567f --- /dev/null +++ b/docs/src/content/docs/targets/brew.md @@ -0,0 +1,54 @@ +--- +title: Homebrew +description: Update Homebrew formulas +--- + +Pushes a new or updated Homebrew formula to a tap repository. The formula is committed directly to the master branch. + +:::note +Formulas on `homebrew/core` are not supported. Use your own tap repository. +::: + +## Configuration + +| Option | Description | +|--------|-------------| +| `tap` | Homebrew tap name (e.g., `octocat/tools` → `github.com:octocat/homebrew-tools`) | +| `template` | Formula template (Ruby code) with Mustache interpolation | +| `formula` | Formula name. Default: repository name | +| `path` | Path to store formula. Default: `Formula` | + +### Template Variables + +- `version`: The new version +- `revision`: The tag's commit SHA +- `checksums`: Map of sha256 checksums by filename (dots replaced with `__`, version with `__VERSION__`) + +## Environment Variables + +| Name | Description | +|------|-------------| +| `GITHUB_TOKEN` | GitHub API token | + +## Example + +```yaml +targets: + - name: brew + tap: octocat/tools + formula: myproject + path: HomebrewFormula + template: > + class MyProject < Formula + desc "This is a test for homebrew formulae" + homepage "https://github.com/octocat/my-project" + url "https://github.com/octocat/my-project/releases/download/{{version}}/binary-darwin" + version "{{version}}" + sha256 "{{checksums.binary-darwin}}" + + def install + mv "binary-darwin", "myproject" + bin.install "myproject" + end + end +``` diff --git a/docs/src/content/docs/targets/cocoapods.md b/docs/src/content/docs/targets/cocoapods.md new file mode 100644 index 00000000..a0c12d2a --- /dev/null +++ b/docs/src/content/docs/targets/cocoapods.md @@ -0,0 +1,32 @@ +--- +title: CocoaPods +description: Publish pods to CocoaPods +--- + +Pushes a new podspec to the central CocoaPods repository. The podspec is fetched from the GitHub repository at the release revision. + +## Configuration + +| Option | Description | +|--------|-------------| +| `specPath` | Path to the Podspec file in the repository | + +## Environment Variables + +| Name | Description | +|------|-------------| +| `COCOAPODS_TRUNK_TOKEN` | Access token for CocoaPods account | +| `COCOAPODS_BIN` | Path to `pod` executable | + +## Example + +```yaml +targets: + - name: cocoapods + specPath: MyProject.podspec +``` + +## Notes + +- The `cocoapods` gem must be installed on the system +- No release artifacts are required for this target diff --git a/docs/src/content/docs/targets/commit-on-git-repository.md b/docs/src/content/docs/targets/commit-on-git-repository.md new file mode 100644 index 00000000..518af8b6 --- /dev/null +++ b/docs/src/content/docs/targets/commit-on-git-repository.md @@ -0,0 +1,39 @@ +--- +title: Commit on Git Repository +description: Push unpacked tarball contents to a git repository +--- + +Takes a tarball and pushes the unpacked contents to a git repository. + +## Configuration + +| Option | Description | +|--------|-------------| +| `archive` | Regex to match a `.tgz` file in artifacts (must match exactly one) | +| `repositoryUrl` | Git remote URL (must use http or https, not `git@...`) | +| `branch` | Target branch | +| `stripComponents` | Leading path elements to remove when unpacking. Default: `0` | +| `createTag` | Create a tag with the release version. Default: `false` | + +## Environment Variables + +| Name | Description | +|------|-------------| +| `GITHUB_API_TOKEN` | GitHub PAT for authentication (when host is `github.com`) | + +## Example + +```yaml +targets: + - name: commit-on-git-repository + archive: /^sentry-deno-\d.*\.tgz$/ + repositoryUrl: https://github.com/getsentry/sentry-deno + stripComponents: 1 + branch: main + createTag: true +``` + +## Notes + +- The repository URL must use HTTP or HTTPS protocol +- For GitHub repos, authentication uses `GITHUB_API_TOKEN` diff --git a/docs/src/content/docs/targets/crates.md b/docs/src/content/docs/targets/crates.md new file mode 100644 index 00000000..0de6e093 --- /dev/null +++ b/docs/src/content/docs/targets/crates.md @@ -0,0 +1,32 @@ +--- +title: Crates +description: Publish Rust packages to crates.io +--- + +Publishes a single Rust package or entire workspace to [crates.io](https://crates.io). If the workspace contains multiple crates, they are published in dependency order. + +## Configuration + +| Option | Description | +|--------|-------------| +| `noDevDeps` | Strip `devDependencies` before publishing. Requires [`cargo-hack`](https://github.com/taiki-e/cargo-hack). Default: `false` | + +## Environment Variables + +| Name | Description | +|------|-------------| +| `CRATES_IO_TOKEN` | Access token for crates.io | +| `CARGO_BIN` | Path to cargo. Default: `cargo` | + +## Example + +```yaml +targets: + - name: crates + noDevDeps: false +``` + +## Notes + +- `cargo` must be installed and configured on the system +- For workspaces, crates are published in topological order based on dependencies diff --git a/docs/src/content/docs/targets/docker.md b/docs/src/content/docs/targets/docker.md new file mode 100644 index 00000000..40b663e8 --- /dev/null +++ b/docs/src/content/docs/targets/docker.md @@ -0,0 +1,104 @@ +--- +title: Docker +description: Tag and push Docker images +--- + +Copies an existing source image tagged with the revision SHA to a new target tagged with the released version. Supports Docker Hub, GitHub Container Registry (ghcr.io), Google Container Registry (gcr.io), and other OCI-compliant registries. + +## Configuration + +| Option | Description | +|--------|-------------| +| `source` | Source image: string or object with `image`, `registry`, `format`, `usernameVar`, `passwordVar`, `skipLogin` | +| `target` | Target image: string or object (same options as source) | + +### Image Object Options + +| Property | Description | +|----------|-------------| +| `image` | Docker image path (e.g., `ghcr.io/org/image`) | +| `registry` | Override the registry (auto-detected from `image`) | +| `format` | Format template. Default: `{{{source}}}:{{{revision}}}` for source | +| `usernameVar` | Env var name for username | +| `passwordVar` | Env var name for password | +| `skipLogin` | Skip `docker login` for this registry | + +## Environment Variables + +**Target Registry Credentials** (resolved in order): + +1. Explicit `usernameVar`/`passwordVar` from config +2. Registry-derived: `DOCKER__USERNAME/PASSWORD` (e.g., `DOCKER_GHCR_IO_USERNAME`) +3. Built-in defaults for `ghcr.io`: `GITHUB_ACTOR` and `GITHUB_TOKEN` +4. Fallback: `DOCKER_USERNAME` and `DOCKER_PASSWORD` + +| Name | Description | +|------|-------------| +| `DOCKER_USERNAME` | Default username for target registry | +| `DOCKER_PASSWORD` | Default password/token for target registry | +| `DOCKER_BIN` | Path to `docker` executable | + +## Examples + +### Docker Hub + +```yaml +targets: + - name: docker + source: ghcr.io/getsentry/craft + target: getsentry/craft +``` + +### GitHub Container Registry (zero-config in GitHub Actions) + +```yaml +targets: + - name: docker + source: ghcr.io/getsentry/craft + target: ghcr.io/getsentry/craft +``` + +### Multiple Registries + +```yaml +targets: + # Docker Hub + - name: docker + source: ghcr.io/getsentry/craft + target: getsentry/craft + + # GHCR + - name: docker + source: ghcr.io/getsentry/craft + target: ghcr.io/getsentry/craft + + # GCR with shared credentials + - name: docker + source: ghcr.io/getsentry/craft + target: us.gcr.io/my-project/craft + registry: gcr.io +``` + +### Cross-registry with Explicit Credentials + +```yaml +targets: + - name: docker + source: + image: private.registry.io/image + usernameVar: PRIVATE_REGISTRY_USER + passwordVar: PRIVATE_REGISTRY_PASS + target: getsentry/craft +``` + +### Google Cloud Registries + +Craft auto-detects Google Cloud registries and uses `gcloud auth configure-docker`: + +```yaml +# Works with google-github-actions/auth +targets: + - name: docker + source: ghcr.io/myorg/image + target: us-docker.pkg.dev/my-project/repo/image +``` diff --git a/docs/src/content/docs/targets/gcs.md b/docs/src/content/docs/targets/gcs.md new file mode 100644 index 00000000..a4950022 --- /dev/null +++ b/docs/src/content/docs/targets/gcs.md @@ -0,0 +1,45 @@ +--- +title: Google Cloud Storage +description: Upload artifacts to GCS buckets +--- + +Uploads artifacts to a bucket in Google Cloud Storage. + +## Configuration + +| Option | Description | +|--------|-------------| +| `bucket` | GCS bucket name | +| `paths` | List of path objects | +| `paths.path` | Bucket path with `{{ version }}` and/or `{{ revision }}` templates | +| `paths.metadata` | Optional metadata for uploaded files | + +## Environment Variables + +| Name | Description | +|------|-------------| +| `CRAFT_GCS_TARGET_CREDS_PATH` | Path to Google Cloud credentials file | +| `CRAFT_GCS_TARGET_CREDS_JSON` | Service account file contents as JSON string | + +If both are set, `CRAFT_GCS_TARGET_CREDS_JSON` takes precedence. + +## Example + +```yaml +targets: + - name: gcs + bucket: bucket-name + paths: + - path: release/{{version}}/download + metadata: + cacheControl: 'public, max-age=3600' + - path: release/{{revision}}/platform/package +``` + +## Default Metadata + +By default, files are uploaded with: + +```yaml +cacheControl: 'public, max-age=300' +``` diff --git a/docs/src/content/docs/targets/gem.md b/docs/src/content/docs/targets/gem.md new file mode 100644 index 00000000..cda80732 --- /dev/null +++ b/docs/src/content/docs/targets/gem.md @@ -0,0 +1,28 @@ +--- +title: Ruby Gems +description: Publish gems to RubyGems +--- + +Pushes a gem to [RubyGems](https://rubygems.org). + +## Configuration + +No additional configuration options. + +## Environment Variables + +| Name | Description | +|------|-------------| +| `GEM_BIN` | Path to `gem` executable. Default: `gem` | + +## Example + +```yaml +targets: + - name: gem +``` + +## Notes + +- `gem` must be installed on the system +- You must be logged in with `gem login` diff --git a/docs/src/content/docs/targets/gh-pages.md b/docs/src/content/docs/targets/gh-pages.md new file mode 100644 index 00000000..791d0f13 --- /dev/null +++ b/docs/src/content/docs/targets/gh-pages.md @@ -0,0 +1,39 @@ +--- +title: GitHub Pages +description: Deploy static sites to GitHub Pages +--- + +Extracts an archive with static assets and pushes them to a git branch for GitHub Pages deployment. + +:::caution +The destination branch will be completely overwritten by the archive contents. +::: + +## Configuration + +| Option | Description | +|--------|-------------| +| `branch` | Branch to push to. Default: `gh-pages` | +| `githubOwner` | GitHub project owner. Default: from global config | +| `githubRepo` | GitHub project name. Default: from global config | + +## Default Behavior + +By default, this target: +1. Looks for an artifact named `gh-pages.zip` +2. Extracts its contents +3. Commits to the `gh-pages` branch + +## Example + +```yaml +targets: + - name: gh-pages + branch: gh-pages +``` + +## Workflow + +1. Create a `gh-pages.zip` artifact in your CI workflow +2. Configure the target in `.craft.yml` +3. Enable GitHub Pages in repository settings to serve from the `gh-pages` branch diff --git a/docs/src/content/docs/targets/github.md b/docs/src/content/docs/targets/github.md new file mode 100644 index 00000000..6f681e4e --- /dev/null +++ b/docs/src/content/docs/targets/github.md @@ -0,0 +1,48 @@ +--- +title: GitHub +description: Create GitHub releases and tags +--- + +Creates a release on GitHub. If a Markdown changelog is present, this target reads the release name and description from it. + +## Configuration + +| Option | Description | +|--------|-------------| +| `tagPrefix` | Prefix for new git tags (e.g., `v`). Empty by default. | +| `previewReleases` | Automatically detect and create preview releases. Default: `true` | +| `tagOnly` | Only create a tag (without a GitHub release). Default: `false` | +| `floatingTags` | List of floating tags to create/update. Supports `{major}`, `{minor}`, `{patch}` placeholders. | + +## Environment Variables + +| Name | Description | +|------|-------------| +| `GITHUB_TOKEN` | Personal GitHub API token ([create one](https://github.com/settings/tokens)) | + +## Example + +```yaml +targets: + - name: github + tagPrefix: v + previewReleases: true +``` + +## Floating Tags + +Use `floatingTags` to maintain "latest major version" tags that always point to the most recent release: + +```yaml +targets: + - name: github + floatingTags: + - "v{major}" # Creates v2 for version 2.15.0 + - "v{major}.{minor}" # Creates v2.15 for version 2.15.0 +``` + +This is useful for users who want to pin to a major version while automatically receiving updates. + +## Preview Releases + +If `previewReleases` is `true` (default), releases containing pre-release identifiers like `alpha`, `beta`, `rc`, etc. are marked as pre-releases on GitHub. diff --git a/docs/src/content/docs/targets/hex.md b/docs/src/content/docs/targets/hex.md new file mode 100644 index 00000000..8eddc8c1 --- /dev/null +++ b/docs/src/content/docs/targets/hex.md @@ -0,0 +1,28 @@ +--- +title: Hex +description: Publish Elixir/Erlang packages to Hex +--- + +Pushes a package to [Hex](https://hex.pm), the package manager for Elixir and Erlang. + +## Configuration + +No additional configuration options. + +## Environment Variables + +| Name | Description | +|------|-------------| +| `HEX_API_KEY` | API key from hex.pm account | +| `MIX_BIN` | Path to `mix` executable. Default: `mix` | + +## Example + +```yaml +targets: + - name: hex +``` + +## Notes + +- `mix` (bundled with Elixir) must be installed on the system diff --git a/docs/src/content/docs/targets/index.md b/docs/src/content/docs/targets/index.md new file mode 100644 index 00000000..13758a21 --- /dev/null +++ b/docs/src/content/docs/targets/index.md @@ -0,0 +1,80 @@ +--- +title: Targets Overview +description: Overview of all available release targets +--- + +Targets define where Craft publishes your release artifacts. Configure them in `.craft.yml` under the `targets` key. + +## Available Targets + +| Target | Description | +|--------|-------------| +| [GitHub](./github/) | Create GitHub releases and tags | +| [NPM](./npm/) | Publish to NPM registry | +| [PyPI](./pypi/) | Publish to Python Package Index | +| [Crates](./crates/) | Publish Rust crates | +| [NuGet](./nuget/) | Publish .NET packages | +| [Docker](./docker/) | Tag and push Docker images | +| [Homebrew](./brew/) | Update Homebrew formulas | +| [GCS](./gcs/) | Upload to Google Cloud Storage | +| [GitHub Pages](./gh-pages/) | Deploy static sites | +| [CocoaPods](./cocoapods/) | Publish iOS/macOS pods | +| [Ruby Gems](./gem/) | Publish Ruby gems | +| [Maven](./maven/) | Publish to Maven Central | +| [Hex](./hex/) | Publish Elixir packages | +| [pub.dev](./pub-dev/) | Publish Dart/Flutter packages | +| [AWS Lambda Layer](./aws-lambda-layer/) | Publish Lambda layers | +| [Registry](./registry/) | Update Sentry release registry | +| [UPM](./upm/) | Publish Unity packages | +| [Symbol Collector](./symbol-collector/) | Upload native symbols | +| [PowerShell](./powershell/) | Publish PowerShell modules | +| [Commit on Git Repository](./commit-on-git-repository/) | Push to a git repository | + +## Basic Configuration + +```yaml +targets: + - name: npm + - name: github +``` + +## Per-target Options + +These options can be applied to any target: + +| Option | Description | +|--------|-------------| +| `includeNames` | Regex pattern: only matched files are processed | +| `excludeNames` | Regex pattern: matched files are skipped | +| `id` | Unique ID to reference this target with `-t target[id]` | +| `onlyIfPresent` | Only run if a file matching this pattern exists | + +Example: + +```yaml +targets: + - name: github + includeNames: /^.*\.exe$/ + excludeNames: /^test.exe$/ + - name: registry + id: browser + onlyIfPresent: /^sentry-browser-.*\.tgz$/ +``` + +## Running Specific Targets + +Use the `-t` flag with `craft publish`: + +```shell +# Publish to all targets +craft publish 1.2.3 + +# Publish to specific target +craft publish 1.2.3 -t npm + +# Publish to target with ID +craft publish 1.2.3 -t registry[browser] + +# Skip publishing (just merge branch) +craft publish 1.2.3 -t none +``` diff --git a/docs/src/content/docs/targets/maven.md b/docs/src/content/docs/targets/maven.md new file mode 100644 index 00000000..994846d0 --- /dev/null +++ b/docs/src/content/docs/targets/maven.md @@ -0,0 +1,75 @@ +--- +title: Maven +description: Publish packages to Maven Central +--- + +PGP signs and publishes packages to Maven Central. + +:::tip +Set the logging level to `trace` to see command output. +::: + +## Configuration + +| Option | Description | +|--------|-------------| +| `mavenCliPath` | Path to Maven CLI (must be executable) | +| `mavenSettingsPath` | Path to Maven `settings.xml` | +| `mavenRepoId` | Maven server ID in `settings.xml` | +| `mavenRepoUrl` | Maven repository URL | +| `android` | Android configuration object or `false` | +| `kmp` | Kotlin Multiplatform configuration or `false` | + +### Android Configuration + +| Option | Description | +|--------|-------------| +| `distDirRegex` | Pattern for distribution directory names | +| `fileReplaceeRegex` | Pattern for module name substring to replace | +| `fileReplacerStr` | Replacement string for Android distribution file | + +### KMP Configuration + +| Option | Description | +|--------|-------------| +| `rootDistDirRegex` | Pattern for root distribution directory | +| `appleDistDirRegex` | Pattern for Apple platform directories | +| `klibDistDirRegex` | Pattern for JS/WASM directories | + +## Environment Variables + +| Name | Description | +|------|-------------| +| `OSSRH_USERNAME` | Sonatype repository username | +| `OSSRH_PASSWORD` | Sonatype repository password | +| `GPG_PASSPHRASE` | Passphrase for GPG private key | +| `GPG_PRIVATE_KEY` | GPG private key (optional, uses default if not set) | + +## Examples + +### Without Android + +```yaml +targets: + - name: maven + mavenCliPath: scripts/mvnw.cmd + mavenSettingsPath: scripts/settings.xml + mavenRepoId: ossrh + mavenRepoUrl: https://oss.sonatype.org/service/local/staging/deploy/maven2/ + android: false +``` + +### With Android + +```yaml +targets: + - name: maven + mavenCliPath: scripts/mvnw.cmd + mavenSettingsPath: scripts/settings.xml + mavenRepoId: ossrh + mavenRepoUrl: https://oss.sonatype.org/service/local/staging/deploy/maven2/ + android: + distDirRegex: /^sentry-android-.*$/ + fileReplaceeRegex: /\d\.\d\.\d(-SNAPSHOT)?/ + fileReplacerStr: release.aar +``` diff --git a/docs/src/content/docs/targets/npm.md b/docs/src/content/docs/targets/npm.md new file mode 100644 index 00000000..9c5d425f --- /dev/null +++ b/docs/src/content/docs/targets/npm.md @@ -0,0 +1,83 @@ +--- +title: NPM +description: Publish packages to NPM registry +--- + +Releases an NPM package to the public registry. Requires a package tarball generated by `npm pack` in the artifacts. + +## Configuration + +| Option | Description | +|--------|-------------| +| `access` | Visibility for scoped packages: `restricted` (default) or `public` | +| `checkPackageName` | Package to check for current version when determining `latest` tag | + +## Environment Variables + +| Name | Description | +|------|-------------| +| `NPM_TOKEN` | An [automation token](https://docs.npmjs.com/creating-and-viewing-access-tokens) allowed to publish | +| `NPM_BIN` | Path to npm executable. Default: `npm` | +| `YARN_BIN` | Path to yarn executable. Default: `yarn` | +| `CRAFT_NPM_USE_OTP` | If `1`, prompts for OTP (for 2FA) | + +## Example + +```yaml +targets: + - name: npm + access: public +``` + +## Workspaces Support + +Craft supports automatic discovery and publishing of NPM/Yarn workspace packages. When enabled, the npm target automatically expands into multiple targets—one per workspace package—published in dependency order. + +### Workspace Configuration + +| Option | Description | +|--------|-------------| +| `workspaces` | Enable workspace discovery. Default: `false` | +| `includeWorkspaces` | Regex pattern to filter which packages to include (e.g., `/^@sentry\//`) | +| `excludeWorkspaces` | Regex pattern to filter which packages to exclude (e.g., `/^@sentry-internal\//`) | +| `artifactTemplate` | Template for artifact filenames. Variables: `{{name}}`, `{{simpleName}}`, `{{version}}` | + +### Workspace Example + +```yaml +targets: + - name: npm + access: public + workspaces: true + includeWorkspaces: /^@sentry\// + excludeWorkspaces: /^@sentry-internal\// +``` + +### Workspace Features + +- **Auto-discovery**: Detects packages from `package.json` workspaces field (npm/yarn workspaces) +- **Dependency ordering**: Publishes packages in topological order (dependencies before dependents) +- **Private package filtering**: Automatically excludes packages marked as `private: true` +- **Validation**: Errors if public packages depend on private workspace packages +- **Scoped package warnings**: Warns if scoped packages don't have `publishConfig.access: 'public'` + +### Artifact Naming + +By default, Craft expects artifacts named like: +- `@sentry/browser` → `sentry-browser-{version}.tgz` + +Use `artifactTemplate` for custom naming: + +```yaml +targets: + - name: npm + workspaces: true + artifactTemplate: "{{simpleName}}-{{version}}.tgz" +``` + +## Notes + +- The `npm` utility must be installed on the system +- If `npm` is not found, Craft falls back to `yarn publish` +- For scoped packages (`@org/package`), set `access: public` to publish publicly +- Pre-release versions are automatically tagged as `next` instead of `latest` diff --git a/docs/src/content/docs/targets/nuget.md b/docs/src/content/docs/targets/nuget.md new file mode 100644 index 00000000..74a89bc3 --- /dev/null +++ b/docs/src/content/docs/targets/nuget.md @@ -0,0 +1,28 @@ +--- +title: NuGet +description: Publish .NET packages to NuGet +--- + +Uploads packages to [NuGet](https://www.nuget.org/) via .NET Core. + +:::note +This target allows re-entrant publishing to handle interrupted releases when publishing multiple packages. +::: + +## Configuration + +No additional configuration options. + +## Environment Variables + +| Name | Description | +|------|-------------| +| `NUGET_API_TOKEN` | NuGet [API token](https://www.nuget.org/account/apikeys) | +| `NUGET_DOTNET_BIN` | Path to .NET Core. Default: `dotnet` | + +## Example + +```yaml +targets: + - name: nuget +``` diff --git a/docs/src/content/docs/targets/powershell.md b/docs/src/content/docs/targets/powershell.md new file mode 100644 index 00000000..0b5a4ae2 --- /dev/null +++ b/docs/src/content/docs/targets/powershell.md @@ -0,0 +1,34 @@ +--- +title: PowerShell +description: Publish PowerShell modules to PowerShell Gallery +--- + +Uploads a module to [PowerShell Gallery](https://www.powershellgallery.com/) or another repository supported by PowerShellGet's `Publish-Module`. + +The action looks for an artifact named `.zip` and extracts it to a temporary directory for publishing. + +## Configuration + +| Option | Description | +|--------|-------------| +| `module` | Module name (required) | +| `repository` | Repository to publish to. Default: `PSGallery` | + +## Environment Variables + +| Name | Description | +|------|-------------| +| `POWERSHELL_API_KEY` | PowerShell Gallery API key (required) | +| `POWERSHELL_BIN` | Path to PowerShell binary. Default: `pwsh` | + +## Example + +```yaml +targets: + - name: powershell + module: Sentry +``` + +## Notes + +- `pwsh` must be [installed](https://github.com/powershell/powershell#get-powershell) on the system diff --git a/docs/src/content/docs/targets/pub-dev.md b/docs/src/content/docs/targets/pub-dev.md new file mode 100644 index 00000000..632ef6e4 --- /dev/null +++ b/docs/src/content/docs/targets/pub-dev.md @@ -0,0 +1,59 @@ +--- +title: pub.dev +description: Publish Dart/Flutter packages to pub.dev +--- + +Pushes a new Dart or Flutter package to [pub.dev](https://pub.dev/). + +## Setup + +Because there is [no automated way](https://github.com/dart-lang/pub-dev/issues/5388) to obtain tokens, you must perform a valid release manually first for each package. This generates credentials at: + +- macOS: `$HOME/Library/Application Support/dart/pub-credentials.json` +- Linux: `$HOME/.config/dart/pub-credentials.json` +- Or: `$HOME/.pub-cache/credentials.json` + +## Configuration + +| Option | Description | +|--------|-------------| +| `dartCliPath` | Path to Dart CLI. Default: `dart` | +| `packages` | List of package directories (for monorepos) | +| `skipValidation` | Skip analyzer and dependency checks | + +## Environment Variables + +| Name | Description | +|------|-------------| +| `PUBDEV_ACCESS_TOKEN` | Value of `accessToken` from credentials file | +| `PUBDEV_REFRESH_TOKEN` | Value of `refreshToken` from credentials file | + +## Examples + +### Single Package + +```yaml +targets: + - name: pub-dev +``` + +### Multiple Packages (Monorepo) + +```yaml +targets: + - name: pub-dev + packages: + uno: + dos: + tres: +``` + +### Skip Validation + +Use cautiously—this skips analyzer checks: + +```yaml +targets: + - name: pub-dev + skipValidation: true +``` diff --git a/docs/src/content/docs/targets/pypi.md b/docs/src/content/docs/targets/pypi.md new file mode 100644 index 00000000..222b3e6e --- /dev/null +++ b/docs/src/content/docs/targets/pypi.md @@ -0,0 +1,48 @@ +--- +title: PyPI +description: Publish packages to Python Package Index +--- + +Uploads source distributions and wheels to the Python Package Index via [twine](https://pypi.org/project/twine/). + +## Configuration + +No additional configuration options. + +## Environment Variables + +| Name | Description | +|------|-------------| +| `TWINE_USERNAME` | PyPI username with access rights | +| `TWINE_PASSWORD` | PyPI password | +| `TWINE_BIN` | Path to twine. Default: `twine` | + +## Example + +```yaml +targets: + - name: pypi +``` + +## Sentry Internal PyPI + +For Sentry's internal PyPI, use the `sentry-pypi` target which creates a PR to import the package: + +```yaml +targets: + - name: pypi + - name: sentry-pypi + internalPypiRepo: getsentry/pypi +``` + +### Sentry PyPI Configuration + +| Option | Description | +|--------|-------------| +| `internalPypiRepo` | GitHub repo containing pypi metadata | + +### Sentry PyPI Environment + +| Name | Description | +|------|-------------| +| `GITHUB_TOKEN` | GitHub API token | diff --git a/docs/src/content/docs/targets/registry.md b/docs/src/content/docs/targets/registry.md new file mode 100644 index 00000000..bb524152 --- /dev/null +++ b/docs/src/content/docs/targets/registry.md @@ -0,0 +1,54 @@ +--- +title: Sentry Release Registry +description: Update Sentry's release registry +--- + +Updates the [Sentry release registry](https://github.com/getsentry/sentry-release-registry/) with the latest version. + +:::tip +Avoid having multiple `registry` targets—it supports batching multiple apps and SDKs in a single target. +::: + +## Configuration + +| Option | Description | +|--------|-------------| +| `apps` | Dict of app configs keyed by canonical name (e.g., `app:craft`) | +| `sdks` | Dict of SDK configs keyed by canonical name (e.g., `maven:io.sentry:sentry`) | + +### App/SDK Options + +| Option | Description | +|--------|-------------| +| `urlTemplate` | URL template for download links | +| `linkPrereleases` | Update for preview releases. Default: `false` | +| `checksums` | List of checksum configs | +| `onlyIfPresent` | Only run if matching file exists | + +### Checksum Configuration + +```yaml +checksums: + - algorithm: sha256 # or sha384, sha512 + format: hex # or base64 +``` + +## Example + +```yaml +targets: + - name: registry + sdks: + 'npm:@sentry/browser': + apps: + 'app:craft': + urlTemplate: 'https://example.com/{{version}}/{{file}}' + checksums: + - algorithm: sha256 + format: hex +``` + +## Package Types + +- **sdk**: Package uploaded to public registries (PyPI, NPM, etc.) +- **app**: Standalone application with version files in the registry diff --git a/docs/src/content/docs/targets/symbol-collector.md b/docs/src/content/docs/targets/symbol-collector.md new file mode 100644 index 00000000..5d1d96fd --- /dev/null +++ b/docs/src/content/docs/targets/symbol-collector.md @@ -0,0 +1,28 @@ +--- +title: Symbol Collector +description: Upload native symbols to Symbol Collector +--- + +Uses the [`symbol-collector`](https://github.com/getsentry/symbol-collector) client to upload native symbols. + +## Configuration + +| Option | Description | +|--------|-------------| +| `serverEndpoint` | Server endpoint. Default: `https://symbol-collector.services.sentry.io` | +| `batchType` | Symbol batch type: `Android`, `macOS`, `iOS` | +| `bundleIdPrefix` | Prefix for bundle ID (version is appended) | + +## Example + +```yaml +targets: + - name: symbol-collector + includeNames: /libsentry(-android)?\.so/ + batchType: Android + bundleIdPrefix: android-ndk- +``` + +## Notes + +- The `symbol-collector` CLI must be available in PATH diff --git a/docs/src/content/docs/targets/upm.md b/docs/src/content/docs/targets/upm.md new file mode 100644 index 00000000..acb78882 --- /dev/null +++ b/docs/src/content/docs/targets/upm.md @@ -0,0 +1,26 @@ +--- +title: Unity Package Manager +description: Publish Unity packages +--- + +Pulls a package as a zipped artifact and pushes the unzipped content to a target repository, tagging it with the release version. + +:::caution +The destination repository will be completely overwritten. +::: + +## Configuration + +| Option | Description | +|--------|-------------| +| `releaseRepoOwner` | Owner of the release target repository | +| `releaseRepoName` | Name of the release target repository | + +## Example + +```yaml +targets: + - name: upm + releaseRepoOwner: 'getsentry' + releaseRepoName: 'unity' +``` diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 00000000..e9c3ffbe --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/docs/yarn.lock b/docs/yarn.lock new file mode 100644 index 00000000..ebfd2168 --- /dev/null +++ b/docs/yarn.lock @@ -0,0 +1,3752 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@astrojs/compiler@^2.13.0": + version "2.13.0" + resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-2.13.0.tgz#a40bef3106fff808bd91b41680275a7e28996d63" + integrity sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw== + +"@astrojs/internal-helpers@0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@astrojs/internal-helpers/-/internal-helpers-0.7.5.tgz#c1491b70a7ac00efbd0de650f3a4058e07112a31" + integrity sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA== + +"@astrojs/markdown-remark@6.3.10": + version "6.3.10" + resolved "https://registry.yarnpkg.com/@astrojs/markdown-remark/-/markdown-remark-6.3.10.tgz#75b8ce7398eec483006de8d76c604cc6ff841606" + integrity sha512-kk4HeYR6AcnzC4QV8iSlOfh+N8TZ3MEStxPyenyCtemqn8IpEATBFMTJcfrNW32dgpt6MY3oCkMM/Tv3/I4G3A== + dependencies: + "@astrojs/internal-helpers" "0.7.5" + "@astrojs/prism" "3.3.0" + github-slugger "^2.0.0" + hast-util-from-html "^2.0.3" + hast-util-to-text "^4.0.2" + import-meta-resolve "^4.2.0" + js-yaml "^4.1.1" + mdast-util-definitions "^6.0.0" + rehype-raw "^7.0.0" + rehype-stringify "^10.0.1" + remark-gfm "^4.0.1" + remark-parse "^11.0.0" + remark-rehype "^11.1.2" + remark-smartypants "^3.0.2" + shiki "^3.19.0" + smol-toml "^1.5.2" + unified "^11.0.5" + unist-util-remove-position "^5.0.0" + unist-util-visit "^5.0.0" + unist-util-visit-parents "^6.0.2" + vfile "^6.0.3" + +"@astrojs/mdx@^4.0.5": + version "4.3.13" + resolved "https://registry.yarnpkg.com/@astrojs/mdx/-/mdx-4.3.13.tgz#a7f6ad87df991ac7f5f9a2d26d1ad07eb3426476" + integrity sha512-IHDHVKz0JfKBy3//52JSiyWv089b7GVSChIXLrlUOoTLWowG3wr2/8hkaEgEyd/vysvNQvGk+QhysXpJW5ve6Q== + dependencies: + "@astrojs/markdown-remark" "6.3.10" + "@mdx-js/mdx" "^3.1.1" + acorn "^8.15.0" + es-module-lexer "^1.7.0" + estree-util-visit "^2.0.0" + hast-util-to-html "^9.0.5" + piccolore "^0.1.3" + rehype-raw "^7.0.0" + remark-gfm "^4.0.1" + remark-smartypants "^3.0.2" + source-map "^0.7.6" + unist-util-visit "^5.0.0" + vfile "^6.0.3" + +"@astrojs/prism@3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@astrojs/prism/-/prism-3.3.0.tgz#5888fcd5665d416450a4fe55b1b7b701b8d586d9" + integrity sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ== + dependencies: + prismjs "^1.30.0" + +"@astrojs/sitemap@^3.2.1": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@astrojs/sitemap/-/sitemap-3.6.0.tgz#e9a83abb96df6c7e89be301b07adce032e49a96f" + integrity sha512-4aHkvcOZBWJigRmMIAJwRQXBS+ayoP5z40OklTXYXhUDhwusz+DyDl+nSshY6y9DvkVEavwNcFO8FD81iGhXjg== + dependencies: + sitemap "^8.0.0" + stream-replace-string "^2.0.0" + zod "^3.25.76" + +"@astrojs/starlight@^0.31.1": + version "0.31.1" + resolved "https://registry.yarnpkg.com/@astrojs/starlight/-/starlight-0.31.1.tgz#617835c93c466d3d7d4c71d8e270d08ae0d83bce" + integrity sha512-VIVkHugwgtEqJPiRH8+ouP0UqUfdmpBO9C64R+6QaQ2qmADNkI/BA3/YAJHTBZYlMQQGEEuLJwD9qpaUovi52Q== + dependencies: + "@astrojs/mdx" "^4.0.5" + "@astrojs/sitemap" "^3.2.1" + "@pagefind/default-ui" "^1.3.0" + "@types/hast" "^3.0.4" + "@types/js-yaml" "^4.0.9" + "@types/mdast" "^4.0.4" + astro-expressive-code "^0.40.0" + bcp-47 "^2.1.0" + hast-util-from-html "^2.0.1" + hast-util-select "^6.0.2" + hast-util-to-string "^3.0.0" + hastscript "^9.0.0" + i18next "^23.11.5" + js-yaml "^4.1.0" + mdast-util-directive "^3.0.0" + mdast-util-to-markdown "^2.1.0" + mdast-util-to-string "^4.0.0" + pagefind "^1.3.0" + rehype "^13.0.1" + rehype-format "^5.0.0" + remark-directive "^3.0.0" + unified "^11.0.5" + unist-util-visit "^5.0.0" + vfile "^6.0.2" + +"@astrojs/telemetry@3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@astrojs/telemetry/-/telemetry-3.3.0.tgz#397dc1f3ab123470571d80c9b4c1335195d30417" + integrity sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ== + dependencies: + ci-info "^4.2.0" + debug "^4.4.0" + dlv "^1.1.3" + dset "^3.1.4" + is-docker "^3.0.0" + is-wsl "^3.1.0" + which-pm-runs "^1.1.0" + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + +"@babel/parser@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08" + integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ== + dependencies: + "@babel/types" "^7.28.5" + +"@babel/runtime@^7.23.2": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" + integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== + +"@babel/types@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b" + integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + +"@capsizecss/unpack@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@capsizecss/unpack/-/unpack-3.0.1.tgz#d40cd7fded06110a3d6198dd1e7a9bbcded52880" + integrity sha512-8XqW8xGn++Eqqbz3e9wKuK7mxryeRjs4LOHLxbh2lwKeSbuNR4NFifDZT4KzvjU6HMOPbiNTsWpniK5EJfTWkg== + dependencies: + fontkit "^2.0.2" + +"@ctrl/tinycolor@^4.0.4": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz#ba5d0b917303c0b3d3c14c4865cdc6ded25ac05f" + integrity sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A== + +"@emnapi/runtime@^1.2.0", "@emnapi/runtime@^1.7.0": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.1.tgz#a73784e23f5d57287369c808197288b52276b791" + integrity sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA== + dependencies: + tslib "^2.4.0" + +"@esbuild/aix-ppc64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz#80fcbe36130e58b7670511e888b8e88a259ed76c" + integrity sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA== + +"@esbuild/android-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz#8aa4965f8d0a7982dc21734bf6601323a66da752" + integrity sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg== + +"@esbuild/android-arm@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz#300712101f7f50f1d2627a162e6e09b109b6767a" + integrity sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg== + +"@esbuild/android-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz#87dfb27161202bdc958ef48bb61b09c758faee16" + integrity sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg== + +"@esbuild/darwin-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz#79197898ec1ff745d21c071e1c7cc3c802f0c1fd" + integrity sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg== + +"@esbuild/darwin-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz#146400a8562133f45c4d2eadcf37ddd09718079e" + integrity sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA== + +"@esbuild/freebsd-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz#1c5f9ba7206e158fd2b24c59fa2d2c8bb47ca0fe" + integrity sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg== + +"@esbuild/freebsd-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz#ea631f4a36beaac4b9279fa0fcc6ca29eaeeb2b3" + integrity sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ== + +"@esbuild/linux-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz#e1066bce58394f1b1141deec8557a5f0a22f5977" + integrity sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ== + +"@esbuild/linux-arm@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz#452cd66b20932d08bdc53a8b61c0e30baf4348b9" + integrity sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw== + +"@esbuild/linux-ia32@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz#b24f8acc45bcf54192c7f2f3be1b53e6551eafe0" + integrity sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA== + +"@esbuild/linux-loong64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz#f9cfffa7fc8322571fbc4c8b3268caf15bd81ad0" + integrity sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng== + +"@esbuild/linux-mips64el@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz#575a14bd74644ffab891adc7d7e60d275296f2cd" + integrity sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw== + +"@esbuild/linux-ppc64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz#75b99c70a95fbd5f7739d7692befe60601591869" + integrity sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA== + +"@esbuild/linux-riscv64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz#2e3259440321a44e79ddf7535c325057da875cd6" + integrity sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w== + +"@esbuild/linux-s390x@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz#17676cabbfe5928da5b2a0d6df5d58cd08db2663" + integrity sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg== + +"@esbuild/linux-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz#0583775685ca82066d04c3507f09524d3cd7a306" + integrity sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw== + +"@esbuild/netbsd-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz#f04c4049cb2e252fe96b16fed90f70746b13f4a4" + integrity sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg== + +"@esbuild/netbsd-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz#77da0d0a0d826d7c921eea3d40292548b258a076" + integrity sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ== + +"@esbuild/openbsd-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz#6296f5867aedef28a81b22ab2009c786a952dccd" + integrity sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A== + +"@esbuild/openbsd-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz#f8d23303360e27b16cf065b23bbff43c14142679" + integrity sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw== + +"@esbuild/openharmony-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz#49e0b768744a3924be0d7fd97dd6ce9b2923d88d" + integrity sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg== + +"@esbuild/sunos-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz#a6ed7d6778d67e528c81fb165b23f4911b9b13d6" + integrity sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w== + +"@esbuild/win32-arm64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz#9ac14c378e1b653af17d08e7d3ce34caef587323" + integrity sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg== + +"@esbuild/win32-ia32@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz#918942dcbbb35cc14fca39afb91b5e6a3d127267" + integrity sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ== + +"@esbuild/win32-x64@0.25.12": + version "0.25.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5" + integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA== + +"@expressive-code/core@^0.40.2": + version "0.40.2" + resolved "https://registry.yarnpkg.com/@expressive-code/core/-/core-0.40.2.tgz#4bf2ffd3879e7adc6177bf240182d153b68b5c6d" + integrity sha512-gXY3v7jbgz6nWKvRpoDxK4AHUPkZRuJsM79vHX/5uhV9/qX6Qnctp/U/dMHog/LCVXcuOps+5nRmf1uxQVPb3w== + dependencies: + "@ctrl/tinycolor" "^4.0.4" + hast-util-select "^6.0.2" + hast-util-to-html "^9.0.1" + hast-util-to-text "^4.0.1" + hastscript "^9.0.0" + postcss "^8.4.38" + postcss-nested "^6.0.1" + unist-util-visit "^5.0.0" + unist-util-visit-parents "^6.0.1" + +"@expressive-code/plugin-frames@^0.40.2": + version "0.40.2" + resolved "https://registry.yarnpkg.com/@expressive-code/plugin-frames/-/plugin-frames-0.40.2.tgz#8df2936519de7fde4d70e221b72199fdf038887c" + integrity sha512-aLw5IlDlZWb10Jo/TTDCVsmJhKfZ7FJI83Zo9VDrV0OBlmHAg7klZqw68VDz7FlftIBVAmMby53/MNXPnMjTSQ== + dependencies: + "@expressive-code/core" "^0.40.2" + +"@expressive-code/plugin-shiki@^0.40.2": + version "0.40.2" + resolved "https://registry.yarnpkg.com/@expressive-code/plugin-shiki/-/plugin-shiki-0.40.2.tgz#afeff8b98b24cf3b79ec1839825b804fc825bd44" + integrity sha512-t2HMR5BO6GdDW1c1ISBTk66xO503e/Z8ecZdNcr6E4NpUfvY+MRje+LtrcvbBqMwWBBO8RpVKcam/Uy+1GxwKQ== + dependencies: + "@expressive-code/core" "^0.40.2" + shiki "^1.26.1" + +"@expressive-code/plugin-text-markers@^0.40.2": + version "0.40.2" + resolved "https://registry.yarnpkg.com/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.40.2.tgz#62b9f267634f5574306985b0d73f41da4a3b8395" + integrity sha512-/XoLjD67K9nfM4TgDlXAExzMJp6ewFKxNpfUw4F7q5Ecy+IU3/9zQQG/O70Zy+RxYTwKGw2MA9kd7yelsxnSmw== + dependencies: + "@expressive-code/core" "^0.40.2" + +"@img/colour@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@img/colour/-/colour-1.0.0.tgz#d2fabb223455a793bf3bf9c70de3d28526aa8311" + integrity sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw== + +"@img/sharp-darwin-arm64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08" + integrity sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ== + optionalDependencies: + "@img/sharp-libvips-darwin-arm64" "1.0.4" + +"@img/sharp-darwin-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz#6e0732dcade126b6670af7aa17060b926835ea86" + integrity sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w== + optionalDependencies: + "@img/sharp-libvips-darwin-arm64" "1.2.4" + +"@img/sharp-darwin-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz#e03d3451cd9e664faa72948cc70a403ea4063d61" + integrity sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q== + optionalDependencies: + "@img/sharp-libvips-darwin-x64" "1.0.4" + +"@img/sharp-darwin-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz#19bc1dd6eba6d5a96283498b9c9f401180ee9c7b" + integrity sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw== + optionalDependencies: + "@img/sharp-libvips-darwin-x64" "1.2.4" + +"@img/sharp-libvips-darwin-arm64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz#447c5026700c01a993c7804eb8af5f6e9868c07f" + integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg== + +"@img/sharp-libvips-darwin-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz#2894c0cb87d42276c3889942e8e2db517a492c43" + integrity sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g== + +"@img/sharp-libvips-darwin-x64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz#e0456f8f7c623f9dbfbdc77383caa72281d86062" + integrity sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ== + +"@img/sharp-libvips-darwin-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz#e63681f4539a94af9cd17246ed8881734386f8cc" + integrity sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg== + +"@img/sharp-libvips-linux-arm64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz#979b1c66c9a91f7ff2893556ef267f90ebe51704" + integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA== + +"@img/sharp-libvips-linux-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz#b1b288b36864b3bce545ad91fa6dadcf1a4ad318" + integrity sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw== + +"@img/sharp-libvips-linux-arm@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz#99f922d4e15216ec205dcb6891b721bfd2884197" + integrity sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g== + +"@img/sharp-libvips-linux-arm@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz#b9260dd1ebe6f9e3bdbcbdcac9d2ac125f35852d" + integrity sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A== + +"@img/sharp-libvips-linux-ppc64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz#4b83ecf2a829057222b38848c7b022e7b4d07aa7" + integrity sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA== + +"@img/sharp-libvips-linux-riscv64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz#880b4678009e5a2080af192332b00b0aaf8a48de" + integrity sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA== + +"@img/sharp-libvips-linux-s390x@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz#f8a5eb1f374a082f72b3f45e2fb25b8118a8a5ce" + integrity sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA== + +"@img/sharp-libvips-linux-s390x@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz#74f343c8e10fad821b38f75ced30488939dc59ec" + integrity sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ== + +"@img/sharp-libvips-linux-x64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz#d4c4619cdd157774906e15770ee119931c7ef5e0" + integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw== + +"@img/sharp-libvips-linux-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz#df4183e8bd8410f7d61b66859a35edeab0a531ce" + integrity sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw== + +"@img/sharp-libvips-linuxmusl-arm64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz#166778da0f48dd2bded1fa3033cee6b588f0d5d5" + integrity sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA== + +"@img/sharp-libvips-linuxmusl-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz#c8d6b48211df67137541007ee8d1b7b1f8ca8e06" + integrity sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw== + +"@img/sharp-libvips-linuxmusl-x64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff" + integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw== + +"@img/sharp-libvips-linuxmusl-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz#be11c75bee5b080cbee31a153a8779448f919f75" + integrity sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg== + +"@img/sharp-linux-arm64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz#edb0697e7a8279c9fc829a60fc35644c4839bb22" + integrity sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA== + optionalDependencies: + "@img/sharp-libvips-linux-arm64" "1.0.4" + +"@img/sharp-linux-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz#7aa7764ef9c001f15e610546d42fce56911790cc" + integrity sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg== + optionalDependencies: + "@img/sharp-libvips-linux-arm64" "1.2.4" + +"@img/sharp-linux-arm@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz#422c1a352e7b5832842577dc51602bcd5b6f5eff" + integrity sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ== + optionalDependencies: + "@img/sharp-libvips-linux-arm" "1.0.5" + +"@img/sharp-linux-arm@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz#5fb0c3695dd12522d39c3ff7a6bc816461780a0d" + integrity sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw== + optionalDependencies: + "@img/sharp-libvips-linux-arm" "1.2.4" + +"@img/sharp-linux-ppc64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz#9c213a81520a20caf66978f3d4c07456ff2e0813" + integrity sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA== + optionalDependencies: + "@img/sharp-libvips-linux-ppc64" "1.2.4" + +"@img/sharp-linux-riscv64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz#cdd28182774eadbe04f62675a16aabbccb833f60" + integrity sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw== + optionalDependencies: + "@img/sharp-libvips-linux-riscv64" "1.2.4" + +"@img/sharp-linux-s390x@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz#f5c077926b48e97e4a04d004dfaf175972059667" + integrity sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q== + optionalDependencies: + "@img/sharp-libvips-linux-s390x" "1.0.4" + +"@img/sharp-linux-s390x@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz#93eac601b9f329bb27917e0e19098c722d630df7" + integrity sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg== + optionalDependencies: + "@img/sharp-libvips-linux-s390x" "1.2.4" + +"@img/sharp-linux-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz#d806e0afd71ae6775cc87f0da8f2d03a7c2209cb" + integrity sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA== + optionalDependencies: + "@img/sharp-libvips-linux-x64" "1.0.4" + +"@img/sharp-linux-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz#55abc7cd754ffca5002b6c2b719abdfc846819a8" + integrity sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ== + optionalDependencies: + "@img/sharp-libvips-linux-x64" "1.2.4" + +"@img/sharp-linuxmusl-arm64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz#252975b915894fb315af5deea174651e208d3d6b" + integrity sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" + +"@img/sharp-linuxmusl-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz#d6515ee971bb62f73001a4829b9d865a11b77086" + integrity sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64" "1.2.4" + +"@img/sharp-linuxmusl-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48" + integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64" "1.0.4" + +"@img/sharp-linuxmusl-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz#d97978aec7c5212f999714f2f5b736457e12ee9f" + integrity sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64" "1.2.4" + +"@img/sharp-wasm32@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz#6f44f3283069d935bb5ca5813153572f3e6f61a1" + integrity sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg== + dependencies: + "@emnapi/runtime" "^1.2.0" + +"@img/sharp-wasm32@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz#2f15803aa626f8c59dd7c9d0bbc766f1ab52cfa0" + integrity sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw== + dependencies: + "@emnapi/runtime" "^1.7.0" + +"@img/sharp-win32-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz#3706e9e3ac35fddfc1c87f94e849f1b75307ce0a" + integrity sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g== + +"@img/sharp-win32-ia32@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz#1a0c839a40c5351e9885628c85f2e5dfd02b52a9" + integrity sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ== + +"@img/sharp-win32-ia32@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz#0b71166599b049e032f085fb9263e02f4e4788de" + integrity sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg== + +"@img/sharp-win32-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342" + integrity sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg== + +"@img/sharp-win32-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz#a81ffb00e69267cd0a1d626eaedb8a8430b2b2f8" + integrity sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw== + +"@jridgewell/sourcemap-codec@^1.5.5": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@mdx-js/mdx@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-3.1.1.tgz#c5ffd991a7536b149e17175eee57a1a2a511c6d1" + integrity sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ== + dependencies: + "@types/estree" "^1.0.0" + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdx" "^2.0.0" + acorn "^8.0.0" + collapse-white-space "^2.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + estree-util-scope "^1.0.0" + estree-walker "^3.0.0" + hast-util-to-jsx-runtime "^2.0.0" + markdown-extensions "^2.0.0" + recma-build-jsx "^1.0.0" + recma-jsx "^1.0.0" + recma-stringify "^1.0.0" + rehype-recma "^1.0.0" + remark-mdx "^3.0.0" + remark-parse "^11.0.0" + remark-rehype "^11.0.0" + source-map "^0.7.0" + unified "^11.0.0" + unist-util-position-from-estree "^2.0.0" + unist-util-stringify-position "^4.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +"@oslojs/encoding@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@oslojs/encoding/-/encoding-1.1.0.tgz#55f3d9a597430a01f2a5ef63c6b42f769f9ce34e" + integrity sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ== + +"@pagefind/darwin-arm64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@pagefind/darwin-arm64/-/darwin-arm64-1.4.0.tgz#0315030e6a89bec3121273b1851f7aadf0b12425" + integrity sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ== + +"@pagefind/darwin-x64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@pagefind/darwin-x64/-/darwin-x64-1.4.0.tgz#671e1fe0f0733350a3eb244ace2675166186793e" + integrity sha512-e7JPIS6L9/cJfow+/IAqknsGqEPjJnVXGjpGm25bnq+NPdoD3c/7fAwr1OXkG4Ocjx6ZGSCijXEV4ryMcH2E3A== + +"@pagefind/default-ui@^1.3.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@pagefind/default-ui/-/default-ui-1.4.0.tgz#036017ba6ed40e9f34ff5652b9caed11113f7bcc" + integrity sha512-wie82VWn3cnGEdIjh4YwNESyS1G6vRHwL6cNjy9CFgNnWW/PGRjsLq300xjVH5sfPFK3iK36UxvIBymtQIEiSQ== + +"@pagefind/freebsd-x64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@pagefind/freebsd-x64/-/freebsd-x64-1.4.0.tgz#3419701ce810e7ec050bbf4786b1c3bee78ec51b" + integrity sha512-WcJVypXSZ+9HpiqZjFXMUobfFfZZ6NzIYtkhQ9eOhZrQpeY5uQFqNWLCk7w9RkMUwBv1HAMDW3YJQl/8OqsV0Q== + +"@pagefind/linux-arm64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@pagefind/linux-arm64/-/linux-arm64-1.4.0.tgz#ba2a5c8d10d5273fe61a8d230b546b08d22cb676" + integrity sha512-PIt8dkqt4W06KGmQjONw7EZbhDF+uXI7i0XtRLN1vjCUxM9vGPdtJc2mUyVPevjomrGz5M86M8bqTr6cgDp1Uw== + +"@pagefind/linux-x64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@pagefind/linux-x64/-/linux-x64-1.4.0.tgz#1e56bb3c91fd0128be84e98897c9785c489fbbb7" + integrity sha512-z4oddcWwQ0UHrTHR8psLnVlz6USGJ/eOlDPTDYZ4cI8TK8PgwRUPQZp9D2iJPNIPcS6Qx/E4TebjuGJOyK8Mmg== + +"@pagefind/windows-x64@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@pagefind/windows-x64/-/windows-x64-1.4.0.tgz#ba68fd609621132e8e314a89e2d2d52516f61723" + integrity sha512-NkT+YAdgS2FPCn8mIA9bQhiBs+xmniMGq1LFPDhcFn0+2yIUEiIG06t7bsZlhdjknEQRTSdT7YitP6fC5qwP0g== + +"@rollup/pluginutils@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz#57ba1b0cbda8e7a3c597a4853c807b156e21a7b4" + integrity sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^4.0.2" + +"@rollup/rollup-android-arm-eabi@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz#f3ff5dbde305c4fa994d49aeb0a5db5305eff03b" + integrity sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng== + +"@rollup/rollup-android-arm64@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz#c97d6ee47846a7ab1cd38e968adce25444a90a19" + integrity sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw== + +"@rollup/rollup-darwin-arm64@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz#a13fc2d82e01eaf8ac823634a3f5f76fd9d0f938" + integrity sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw== + +"@rollup/rollup-darwin-x64@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz#db4fa8b2b76d86f7e9b68ce4661fafe9767adf9b" + integrity sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A== + +"@rollup/rollup-freebsd-arm64@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz#b2c6039de4b75efd3f29417fcb1a795c75a4e3ee" + integrity sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA== + +"@rollup/rollup-freebsd-x64@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz#9ae2a216c94f87912a596a3b3a2ec5199a689ba5" + integrity sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ== + +"@rollup/rollup-linux-arm-gnueabihf@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz#69d5de7f781132f138514f2b900c523e38e2461f" + integrity sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ== + +"@rollup/rollup-linux-arm-musleabihf@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz#b6431e5699747f285306ffe8c1194d7af74f801f" + integrity sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA== + +"@rollup/rollup-linux-arm64-gnu@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz#a32931baec8a0fa7b3288afb72d400ae735112c2" + integrity sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng== + +"@rollup/rollup-linux-arm64-musl@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz#0ad72572b01eb946c0b1a7a6f17ab3be6689a963" + integrity sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg== + +"@rollup/rollup-linux-loong64-gnu@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz#05681f000310906512279944b5bef38c0cd4d326" + integrity sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw== + +"@rollup/rollup-linux-ppc64-gnu@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz#9847a8c9dd76d687c3bdbe38d7f5f32c6b2743c8" + integrity sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA== + +"@rollup/rollup-linux-riscv64-gnu@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz#173f20c278ac770ae3e969663a27d172a4545e87" + integrity sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ== + +"@rollup/rollup-linux-riscv64-musl@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz#db70c2377ae1ef61ef8673354d107ecb3fa7ffed" + integrity sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A== + +"@rollup/rollup-linux-s390x-gnu@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz#b2c461778add1c2ee70ec07d1788611548647962" + integrity sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ== + +"@rollup/rollup-linux-x64-gnu@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz#ab140b356569601f57ab8727bd7306463841894f" + integrity sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ== + +"@rollup/rollup-linux-x64-musl@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz#810134b4a9d0d88576938f2eed38999a653814a1" + integrity sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw== + +"@rollup/rollup-openharmony-arm64@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz#0182bae7a54e748be806acef7a7f726f6949213c" + integrity sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg== + +"@rollup/rollup-win32-arm64-msvc@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz#1f19349bd1c5e454d03e4508a9277b6354985b9d" + integrity sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw== + +"@rollup/rollup-win32-ia32-msvc@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz#234ff739993539f64efac6c2e59704a691a309c2" + integrity sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ== + +"@rollup/rollup-win32-x64-gnu@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz#a4df0507c3be09c152a795cfc0c4f0c225765c5c" + integrity sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ== + +"@rollup/rollup-win32-x64-msvc@4.54.0": + version "4.54.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz#beacb356412eef5dc0164e9edfee51c563732054" + integrity sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg== + +"@shikijs/core@1.29.2": + version "1.29.2" + resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.29.2.tgz#9c051d3ac99dd06ae46bd96536380c916e552bf3" + integrity sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ== + dependencies: + "@shikijs/engine-javascript" "1.29.2" + "@shikijs/engine-oniguruma" "1.29.2" + "@shikijs/types" "1.29.2" + "@shikijs/vscode-textmate" "^10.0.1" + "@types/hast" "^3.0.4" + hast-util-to-html "^9.0.4" + +"@shikijs/core@3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-3.20.0.tgz#ccb9f687de1a236247d8f306cc193dde35f51688" + integrity sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g== + dependencies: + "@shikijs/types" "3.20.0" + "@shikijs/vscode-textmate" "^10.0.2" + "@types/hast" "^3.0.4" + hast-util-to-html "^9.0.5" + +"@shikijs/engine-javascript@1.29.2": + version "1.29.2" + resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz#a821ad713a3e0b7798a1926fd9e80116e38a1d64" + integrity sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A== + dependencies: + "@shikijs/types" "1.29.2" + "@shikijs/vscode-textmate" "^10.0.1" + oniguruma-to-es "^2.2.0" + +"@shikijs/engine-javascript@3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-3.20.0.tgz#b0a40ea401b2dc167b14ed924979081c7f920650" + integrity sha512-OFx8fHAZuk7I42Z9YAdZ95To6jDePQ9Rnfbw9uSRTSbBhYBp1kEOKv/3jOimcj3VRUKusDYM6DswLauwfhboLg== + dependencies: + "@shikijs/types" "3.20.0" + "@shikijs/vscode-textmate" "^10.0.2" + oniguruma-to-es "^4.3.4" + +"@shikijs/engine-oniguruma@1.29.2": + version "1.29.2" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz#d879717ced61d44e78feab16f701f6edd75434f1" + integrity sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA== + dependencies: + "@shikijs/types" "1.29.2" + "@shikijs/vscode-textmate" "^10.0.1" + +"@shikijs/engine-oniguruma@3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.20.0.tgz#4b476a8dff29561dfd9af1ba2edb4c378d3bee06" + integrity sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ== + dependencies: + "@shikijs/types" "3.20.0" + "@shikijs/vscode-textmate" "^10.0.2" + +"@shikijs/langs@1.29.2": + version "1.29.2" + resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-1.29.2.tgz#4f1de46fde8991468c5a68fa4a67dd2875d643cd" + integrity sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ== + dependencies: + "@shikijs/types" "1.29.2" + +"@shikijs/langs@3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-3.20.0.tgz#5dcfdeb9eb2d5f811144ca606553a4d8a6a667d5" + integrity sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA== + dependencies: + "@shikijs/types" "3.20.0" + +"@shikijs/themes@1.29.2": + version "1.29.2" + resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-1.29.2.tgz#293cc5c83dd7df3fdc8efa25cec8223f3a6acb0d" + integrity sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g== + dependencies: + "@shikijs/types" "1.29.2" + +"@shikijs/themes@3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-3.20.0.tgz#9b030fe81fcd0a8b7941131ef14c274b4c6451a8" + integrity sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ== + dependencies: + "@shikijs/types" "3.20.0" + +"@shikijs/types@1.29.2": + version "1.29.2" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.29.2.tgz#a93fdb410d1af8360c67bf5fc1d1a68d58e21c4f" + integrity sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw== + dependencies: + "@shikijs/vscode-textmate" "^10.0.1" + "@types/hast" "^3.0.4" + +"@shikijs/types@3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-3.20.0.tgz#b1fbacba2e1e38d31e3f869309fff216a5d27126" + integrity sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw== + dependencies: + "@shikijs/vscode-textmate" "^10.0.2" + "@types/hast" "^3.0.4" + +"@shikijs/vscode-textmate@^10.0.1", "@shikijs/vscode-textmate@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224" + integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg== + +"@swc/helpers@^0.5.12": + version "0.5.17" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.17.tgz#5a7be95ac0f0bf186e7e6e890e7a6f6cda6ce971" + integrity sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A== + dependencies: + tslib "^2.8.0" + +"@types/debug@^4.0.0": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + +"@types/estree-jsx@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18" + integrity sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg== + dependencies: + "@types/estree" "*" + +"@types/estree@*", "@types/estree@1.0.8", "@types/estree@^1.0.0": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/fontkit@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@types/fontkit/-/fontkit-2.0.8.tgz#59725be650e68acbbff6df9f3fccbd54d9ef7f4c" + integrity sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew== + dependencies: + "@types/node" "*" + +"@types/hast@^3.0.0", "@types/hast@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + +"@types/js-yaml@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" + integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== + +"@types/mdast@^4.0.0", "@types/mdast@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" + integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== + dependencies: + "@types/unist" "*" + +"@types/mdx@^2.0.0": + version "2.0.13" + resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.13.tgz#68f6877043d377092890ff5b298152b0a21671bd" + integrity sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw== + +"@types/ms@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + +"@types/nlcst@^2.0.0": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/nlcst/-/nlcst-2.0.3.tgz#31cad346eaab48a9a8a58465d3d05e2530dda762" + integrity sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA== + dependencies: + "@types/unist" "*" + +"@types/node@*": + version "25.0.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.3.tgz#79b9ac8318f373fbfaaf6e2784893efa9701f269" + integrity sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA== + dependencies: + undici-types "~7.16.0" + +"@types/node@^17.0.5": + version "17.0.45" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" + integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== + +"@types/sax@^1.2.1": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.7.tgz#ba5fe7df9aa9c89b6dff7688a19023dd2963091d" + integrity sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A== + dependencies: + "@types/node" "*" + +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + +"@types/unist@^2.0.0": + version "2.0.11" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" + integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== + +"@ungap/structured-clone@^1.0.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== + +acorn-jsx@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.0.0, acorn@^8.15.0: + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + +ansi-align@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" + integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== + +ansi-styles@^6.2.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" + integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== + +anymatch@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +aria-query@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" + integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== + +array-iterate@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/array-iterate/-/array-iterate-2.0.1.tgz#6efd43f8295b3fee06251d3d62ead4bd9805dd24" + integrity sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg== + +astring@^1.8.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/astring/-/astring-1.9.0.tgz#cc73e6062a7eb03e7d19c22d8b0b3451fd9bfeef" + integrity sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg== + +astro-expressive-code@^0.40.0: + version "0.40.2" + resolved "https://registry.yarnpkg.com/astro-expressive-code/-/astro-expressive-code-0.40.2.tgz#caee511b873c5c93f22cbca469dcd6f7ad3d86ee" + integrity sha512-yJMQId0yXSAbW9I6yqvJ3FcjKzJ8zRL7elbJbllkv1ZJPlsI0NI83Pxn1YL1IapEM347EvOOkSW2GL+2+NO61w== + dependencies: + rehype-expressive-code "^0.40.2" + +astro@^5.1.1: + version "5.16.6" + resolved "https://registry.yarnpkg.com/astro/-/astro-5.16.6.tgz#9f2d8a7bbb7e69fc7db4718cca82233b20b76d08" + integrity sha512-6mF/YrvwwRxLTu+aMEa5pwzKUNl5ZetWbTyZCs9Um0F12HUmxUiF5UHiZPy4rifzU3gtpM3xP2DfdmkNX9eZRg== + dependencies: + "@astrojs/compiler" "^2.13.0" + "@astrojs/internal-helpers" "0.7.5" + "@astrojs/markdown-remark" "6.3.10" + "@astrojs/telemetry" "3.3.0" + "@capsizecss/unpack" "^3.0.1" + "@oslojs/encoding" "^1.1.0" + "@rollup/pluginutils" "^5.3.0" + acorn "^8.15.0" + aria-query "^5.3.2" + axobject-query "^4.1.0" + boxen "8.0.1" + ci-info "^4.3.1" + clsx "^2.1.1" + common-ancestor-path "^1.0.1" + cookie "^1.0.2" + cssesc "^3.0.0" + debug "^4.4.3" + deterministic-object-hash "^2.0.2" + devalue "^5.5.0" + diff "^5.2.0" + dlv "^1.1.3" + dset "^3.1.4" + es-module-lexer "^1.7.0" + esbuild "^0.25.0" + estree-walker "^3.0.3" + flattie "^1.1.1" + fontace "~0.3.1" + github-slugger "^2.0.0" + html-escaper "3.0.3" + http-cache-semantics "^4.2.0" + import-meta-resolve "^4.2.0" + js-yaml "^4.1.1" + magic-string "^0.30.21" + magicast "^0.5.1" + mrmime "^2.0.1" + neotraverse "^0.6.18" + p-limit "^6.2.0" + p-queue "^8.1.1" + package-manager-detector "^1.5.0" + piccolore "^0.1.3" + picomatch "^4.0.3" + prompts "^2.4.2" + rehype "^13.0.2" + semver "^7.7.3" + shiki "^3.15.0" + smol-toml "^1.5.2" + svgo "^4.0.0" + tinyexec "^1.0.2" + tinyglobby "^0.2.15" + tsconfck "^3.1.6" + ultrahtml "^1.6.0" + unifont "~0.6.0" + unist-util-visit "^5.0.0" + unstorage "^1.17.3" + vfile "^6.0.3" + vite "^6.4.1" + vitefu "^1.1.1" + xxhash-wasm "^1.1.0" + yargs-parser "^21.1.1" + yocto-spinner "^0.2.3" + zod "^3.25.76" + zod-to-json-schema "^3.25.0" + zod-to-ts "^1.2.0" + optionalDependencies: + sharp "^0.34.0" + +axobject-query@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" + integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== + +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== + +base-64@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/base-64/-/base-64-1.0.0.tgz#09d0f2084e32a3fd08c2475b973788eee6ae8f4a" + integrity sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg== + +base64-js@^1.1.2, base64-js@^1.3.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bcp-47-match@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/bcp-47-match/-/bcp-47-match-2.0.3.tgz#603226f6e5d3914a581408be33b28a53144b09d0" + integrity sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ== + +bcp-47@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/bcp-47/-/bcp-47-2.1.0.tgz#7e80734c3338fe8320894981dccf4968c3092df6" + integrity sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w== + dependencies: + is-alphabetical "^2.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + +boxen@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-8.0.1.tgz#7e9fcbb45e11a2d7e6daa8fdcebfc3242fc19fe3" + integrity sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw== + dependencies: + ansi-align "^3.0.1" + camelcase "^8.0.0" + chalk "^5.3.0" + cli-boxes "^3.0.0" + string-width "^7.2.0" + type-fest "^4.21.0" + widest-line "^5.0.0" + wrap-ansi "^9.0.0" + +brotli@^1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/brotli/-/brotli-1.3.3.tgz#7365d8cc00f12cf765d2b2c898716bcf4b604d48" + integrity sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg== + dependencies: + base64-js "^1.1.2" + +camelcase@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-8.0.0.tgz#c0d36d418753fb6ad9c5e0437579745c1c14a534" + integrity sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA== + +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + +chalk@^5.3.0: + version "5.6.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea" + integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== + +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + +character-reference-invalid@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" + integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== + +chokidar@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + +ci-info@^4.2.0, ci-info@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.3.1.tgz#355ad571920810b5623e11d40232f443f16f1daa" + integrity sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA== + +cli-boxes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145" + integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g== + +clone@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== + +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + +collapse-white-space@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-2.1.0.tgz#640257174f9f42c740b40f3b55ee752924feefca" + integrity sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw== + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + +commander@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" + integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== + +common-ancestor-path@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz#4f7d2d1394d91b7abdf51871c62f71eadb0182a7" + integrity sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w== + +cookie-es@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-es/-/cookie-es-1.2.2.tgz#18ceef9eb513cac1cb6c14bcbf8bdb2679b34821" + integrity sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg== + +cookie@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.1.1.tgz#3bb9bdfc82369db9c2f69c93c9c3ceb310c88b3c" + integrity sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ== + +crossws@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/crossws/-/crossws-0.3.5.tgz#daad331d44148ea6500098bc858869f3a5ab81a6" + integrity sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA== + dependencies: + uncrypto "^0.1.3" + +css-select@^5.1.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.2.2.tgz#01b6e8d163637bb2dd6c982ca4ed65863682786e" + integrity sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-selector-parser@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-3.3.0.tgz#1a34220d76762c929ae99993df5a60721f505082" + integrity sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g== + +css-tree@^3.0.0, css-tree@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-3.1.0.tgz#7aabc035f4e66b5c86f54570d55e05b1346eb0fd" + integrity sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w== + dependencies: + mdn-data "2.12.2" + source-map-js "^1.0.1" + +css-tree@~2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.2.1.tgz#36115d382d60afd271e377f9c5f67d02bd48c032" + integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA== + dependencies: + mdn-data "2.0.28" + source-map-js "^1.0.1" + +css-what@^6.1.0: + version "6.2.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.2.2.tgz#cdcc8f9b6977719fdfbd1de7aec24abf756b9dea" + integrity sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +csso@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" + integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ== + dependencies: + css-tree "~2.2.0" + +debug@^4.0.0, debug@^4.4.0, debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +decode-named-character-reference@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz#25c32ae6dd5e21889549d40f676030e9514cc0ed" + integrity sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q== + dependencies: + character-entities "^2.0.0" + +defu@^6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" + integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== + +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +destr@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.5.tgz#7d112ff1b925fb8d2079fac5bdb4a90973b51fdb" + integrity sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA== + +detect-libc@^2.0.3, detect-libc@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + +deterministic-object-hash@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/deterministic-object-hash/-/deterministic-object-hash-2.0.2.tgz#b251ddc801443905f0e9fef08816a46bc9fe3807" + integrity sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ== + dependencies: + base-64 "^1.0.0" + +devalue@^5.5.0: + version "5.6.1" + resolved "https://registry.yarnpkg.com/devalue/-/devalue-5.6.1.tgz#f4c0a6e71d1a2bc50c02f9ca3c54ecafeb6a0445" + integrity sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A== + +devlop@^1.0.0, devlop@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + +dfa@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/dfa/-/dfa-1.2.0.tgz#96ac3204e2d29c49ea5b57af8d92c2ae12790657" + integrity sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q== + +diff@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== + +direction@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/direction/-/direction-2.0.1.tgz#71800dd3c4fa102406502905d3866e65bdebb985" + integrity sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA== + +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== + +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^3.0.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" + integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + +dset@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.4.tgz#f8eaf5f023f068a036d08cd07dc9ffb7d0065248" + integrity sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA== + +emoji-regex-xs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz#e8af22e5d9dbd7f7f22d280af3d19d2aab5b0724" + integrity sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg== + +emoji-regex@^10.3.0: + version "10.6.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.6.0.tgz#bf3d6e8f7f8fd22a65d9703475bc0147357a6b0d" + integrity sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +entities@^4.2.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +entities@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694" + integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== + +es-module-lexer@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" + integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + +esast-util-from-estree@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz#8d1cfb51ad534d2f159dc250e604f3478a79f1ad" + integrity sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ== + dependencies: + "@types/estree-jsx" "^1.0.0" + devlop "^1.0.0" + estree-util-visit "^2.0.0" + unist-util-position-from-estree "^2.0.0" + +esast-util-from-js@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz#5147bec34cc9da44accf52f87f239a40ac3e8225" + integrity sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw== + dependencies: + "@types/estree-jsx" "^1.0.0" + acorn "^8.0.0" + esast-util-from-estree "^2.0.0" + vfile-message "^4.0.0" + +esbuild@^0.25.0: + version "0.25.12" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.12.tgz#97a1d041f4ab00c2fce2f838d2b9969a2d2a97a5" + integrity sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.12" + "@esbuild/android-arm" "0.25.12" + "@esbuild/android-arm64" "0.25.12" + "@esbuild/android-x64" "0.25.12" + "@esbuild/darwin-arm64" "0.25.12" + "@esbuild/darwin-x64" "0.25.12" + "@esbuild/freebsd-arm64" "0.25.12" + "@esbuild/freebsd-x64" "0.25.12" + "@esbuild/linux-arm" "0.25.12" + "@esbuild/linux-arm64" "0.25.12" + "@esbuild/linux-ia32" "0.25.12" + "@esbuild/linux-loong64" "0.25.12" + "@esbuild/linux-mips64el" "0.25.12" + "@esbuild/linux-ppc64" "0.25.12" + "@esbuild/linux-riscv64" "0.25.12" + "@esbuild/linux-s390x" "0.25.12" + "@esbuild/linux-x64" "0.25.12" + "@esbuild/netbsd-arm64" "0.25.12" + "@esbuild/netbsd-x64" "0.25.12" + "@esbuild/openbsd-arm64" "0.25.12" + "@esbuild/openbsd-x64" "0.25.12" + "@esbuild/openharmony-arm64" "0.25.12" + "@esbuild/sunos-x64" "0.25.12" + "@esbuild/win32-arm64" "0.25.12" + "@esbuild/win32-ia32" "0.25.12" + "@esbuild/win32-x64" "0.25.12" + +escape-string-regexp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + +estree-util-attach-comments@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz#344bde6a64c8a31d15231e5ee9e297566a691c2d" + integrity sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw== + dependencies: + "@types/estree" "^1.0.0" + +estree-util-build-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz#b6d0bced1dcc4f06f25cf0ceda2b2dcaf98168f1" + integrity sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ== + dependencies: + "@types/estree-jsx" "^1.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + estree-walker "^3.0.0" + +estree-util-is-identifier-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz#0b5ef4c4ff13508b34dcd01ecfa945f61fce5dbd" + integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== + +estree-util-scope@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/estree-util-scope/-/estree-util-scope-1.0.0.tgz#9cbdfc77f5cb51e3d9ed4ad9c4adbff22d43e585" + integrity sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + +estree-util-to-js@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz#10a6fb924814e6abb62becf0d2bc4dea51d04f17" + integrity sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg== + dependencies: + "@types/estree-jsx" "^1.0.0" + astring "^1.8.0" + source-map "^0.7.0" + +estree-util-visit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/estree-util-visit/-/estree-util-visit-2.0.0.tgz#13a9a9f40ff50ed0c022f831ddf4b58d05446feb" + integrity sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/unist" "^3.0.0" + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +estree-walker@^3.0.0, estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + +expressive-code@^0.40.2: + version "0.40.2" + resolved "https://registry.yarnpkg.com/expressive-code/-/expressive-code-0.40.2.tgz#34eca86bcfa54716c6887a860ca59682b2d983e6" + integrity sha512-1zIda2rB0qiDZACawzw2rbdBQiWHBT56uBctS+ezFe5XMAaFaHLnnSYND/Kd+dVzO9HfCXRDpzH3d+3fvOWRcw== + dependencies: + "@expressive-code/core" "^0.40.2" + "@expressive-code/plugin-frames" "^0.40.2" + "@expressive-code/plugin-shiki" "^0.40.2" + "@expressive-code/plugin-text-markers" "^0.40.2" + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fdir@^6.4.4, fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + +flattie@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/flattie/-/flattie-1.1.1.tgz#88182235723113667d36217fec55359275d6fe3d" + integrity sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ== + +fontace@~0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/fontace/-/fontace-0.3.1.tgz#2325007b9784695103e630f865b3ac8d771547c8" + integrity sha512-9f5g4feWT1jWT8+SbL85aLIRLIXUaDygaM2xPXRmzPYxrOMNok79Lr3FGJoKVNKibE0WCunNiEVG2mwuE+2qEg== + dependencies: + "@types/fontkit" "^2.0.8" + fontkit "^2.0.4" + +fontkit@^2.0.2, fontkit@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/fontkit/-/fontkit-2.0.4.tgz#4765d664c68b49b5d6feb6bd1051ee49d8ec5ab0" + integrity sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g== + dependencies: + "@swc/helpers" "^0.5.12" + brotli "^1.3.2" + clone "^2.1.2" + dfa "^1.2.0" + fast-deep-equal "^3.1.3" + restructure "^3.0.0" + tiny-inflate "^1.0.3" + unicode-properties "^1.4.0" + unicode-trie "^2.0.0" + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +get-east-asian-width@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz#9bc4caa131702b4b61729cb7e42735bc550c9ee6" + integrity sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q== + +github-slugger@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-2.0.0.tgz#52cf2f9279a21eb6c59dd385b410f0c0adda8f1a" + integrity sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw== + +h3@^1.15.4: + version "1.15.4" + resolved "https://registry.yarnpkg.com/h3/-/h3-1.15.4.tgz#022ab3563bbaf2108c25375c40460f3e54a5fe02" + integrity sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ== + dependencies: + cookie-es "^1.2.2" + crossws "^0.3.5" + defu "^6.1.4" + destr "^2.0.5" + iron-webcrypto "^1.2.1" + node-mock-http "^1.0.2" + radix3 "^1.1.2" + ufo "^1.6.1" + uncrypto "^0.1.3" + +hast-util-embedded@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz#be4477780fbbe079cdba22982e357a0de4ba853e" + integrity sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA== + dependencies: + "@types/hast" "^3.0.0" + hast-util-is-element "^3.0.0" + +hast-util-format@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/hast-util-format/-/hast-util-format-1.1.0.tgz#373e77382e07deb04f6676f1b4437e7d8549d985" + integrity sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA== + dependencies: + "@types/hast" "^3.0.0" + hast-util-embedded "^3.0.0" + hast-util-minify-whitespace "^1.0.0" + hast-util-phrasing "^3.0.0" + hast-util-whitespace "^3.0.0" + html-whitespace-sensitive-tag-names "^3.0.0" + unist-util-visit-parents "^6.0.0" + +hast-util-from-html@^2.0.0, hast-util-from-html@^2.0.1, hast-util-from-html@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz#485c74785358beb80c4ba6346299311ac4c49c82" + integrity sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw== + dependencies: + "@types/hast" "^3.0.0" + devlop "^1.1.0" + hast-util-from-parse5 "^8.0.0" + parse5 "^7.0.0" + vfile "^6.0.0" + vfile-message "^4.0.0" + +hast-util-from-parse5@^8.0.0: + version "8.0.3" + resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz#830a35022fff28c3fea3697a98c2f4cc6b835a2e" + integrity sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + hastscript "^9.0.0" + property-information "^7.0.0" + vfile "^6.0.0" + vfile-location "^5.0.0" + web-namespaces "^2.0.0" + +hast-util-has-property@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz#4e595e3cddb8ce530ea92f6fc4111a818d8e7f93" + integrity sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-is-body-ok-link@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz#ef63cb2f14f04ecf775139cd92bda5026380d8b4" + integrity sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-is-element@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz#6e31a6532c217e5b533848c7e52c9d9369ca0932" + integrity sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-minify-whitespace@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hast-util-minify-whitespace/-/hast-util-minify-whitespace-1.0.1.tgz#7588fd1a53f48f1d30406b81959dffc3650daf55" + integrity sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw== + dependencies: + "@types/hast" "^3.0.0" + hast-util-embedded "^3.0.0" + hast-util-is-element "^3.0.0" + hast-util-whitespace "^3.0.0" + unist-util-is "^6.0.0" + +hast-util-parse-selector@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27" + integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-phrasing@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz#fa284c0cd4a82a0dd6020de8300a7b1ebffa1690" + integrity sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ== + dependencies: + "@types/hast" "^3.0.0" + hast-util-embedded "^3.0.0" + hast-util-has-property "^3.0.0" + hast-util-is-body-ok-link "^3.0.0" + hast-util-is-element "^3.0.0" + +hast-util-raw@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-9.1.0.tgz#79b66b26f6f68fb50dfb4716b2cdca90d92adf2e" + integrity sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + "@ungap/structured-clone" "^1.0.0" + hast-util-from-parse5 "^8.0.0" + hast-util-to-parse5 "^8.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + parse5 "^7.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + +hast-util-select@^6.0.2: + version "6.0.4" + resolved "https://registry.yarnpkg.com/hast-util-select/-/hast-util-select-6.0.4.tgz#1d8f69657a57441d0ce0ade35887874d3e65a303" + integrity sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + bcp-47-match "^2.0.0" + comma-separated-tokens "^2.0.0" + css-selector-parser "^3.0.0" + devlop "^1.0.0" + direction "^2.0.0" + hast-util-has-property "^3.0.0" + hast-util-to-string "^3.0.0" + hast-util-whitespace "^3.0.0" + nth-check "^2.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + unist-util-visit "^5.0.0" + zwitch "^2.0.0" + +hast-util-to-estree@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz#e654c1c9374645135695cc0ab9f70b8fcaf733d7" + integrity sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w== + dependencies: + "@types/estree" "^1.0.0" + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + estree-util-attach-comments "^3.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + style-to-js "^1.0.0" + unist-util-position "^5.0.0" + zwitch "^2.0.0" + +hast-util-to-html@^9.0.0, hast-util-to-html@^9.0.1, hast-util-to-html@^9.0.4, hast-util-to-html@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz#ccc673a55bb8e85775b08ac28380f72d47167005" + integrity sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-whitespace "^3.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + stringify-entities "^4.0.0" + zwitch "^2.0.4" + +hast-util-to-jsx-runtime@^2.0.0: + version "2.3.6" + resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz#ff31897aae59f62232e21594eac7ef6b63333e98" + integrity sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg== + dependencies: + "@types/estree" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + style-to-js "^1.0.0" + unist-util-position "^5.0.0" + vfile-message "^4.0.0" + +hast-util-to-parse5@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz#95aa391cc0514b4951418d01c883d1038af42f5d" + integrity sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + +hast-util-to-string@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz#a4f15e682849326dd211c97129c94b0c3e76527c" + integrity sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-to-text@^4.0.1, hast-util-to-text@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz#57b676931e71bf9cb852453678495b3080bfae3e" + integrity sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + hast-util-is-element "^3.0.0" + unist-util-find-after "^5.0.0" + +hast-util-whitespace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" + integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== + dependencies: + "@types/hast" "^3.0.0" + +hastscript@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-9.0.1.tgz#dbc84bef6051d40084342c229c451cd9dc567dff" + integrity sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + hast-util-parse-selector "^4.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + +html-escaper@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-3.0.3.tgz#4d336674652beb1dcbc29ef6b6ba7f6be6fdfed6" + integrity sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ== + +html-void-elements@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" + integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + +html-whitespace-sensitive-tag-names@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz#c35edd28205f3bf8c1fd03274608d60b923de5b2" + integrity sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA== + +http-cache-semantics@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5" + integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ== + +i18next@^23.11.5: + version "23.16.8" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.16.8.tgz#3ae1373d344c2393f465556f394aba5a9233b93a" + integrity sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg== + dependencies: + "@babel/runtime" "^7.23.2" + +import-meta-resolve@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz#08cb85b5bd37ecc8eb1e0f670dc2767002d43734" + integrity sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg== + +inline-style-parser@0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.7.tgz#b1fc68bfc0313b8685745e4464e37f9376b9c909" + integrity sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA== + +iron-webcrypto@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz#aa60ff2aa10550630f4c0b11fd2442becdb35a6f" + integrity sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg== + +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== + dependencies: + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" + +is-arrayish@^0.3.1: + version "0.3.4" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.4.tgz#1ee5553818511915685d33bb13d31bf854e5059d" + integrity sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA== + +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== + +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-hexadecimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" + integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== + +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + +is-wsl@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2" + integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== + dependencies: + is-inside-container "^1.0.0" + +js-yaml@^4.1.0, js-yaml@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== + dependencies: + argparse "^2.0.1" + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== + +lru-cache@^10.4.3: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +magic-string@^0.30.21: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + +magicast@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.5.1.tgz#518959aea78851cd35d4bb0da92f780db3f606d3" + integrity sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw== + dependencies: + "@babel/parser" "^7.28.5" + "@babel/types" "^7.28.5" + source-map-js "^1.2.1" + +markdown-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz#34bebc83e9938cae16e0e017e4a9814a8330d3c4" + integrity sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q== + +markdown-table@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.4.tgz#fe44d6d410ff9d6f2ea1797a3f60aa4d2b631c2a" + integrity sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw== + +mdast-util-definitions@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz#c1bb706e5e76bb93f9a09dd7af174002ae69ac24" + integrity sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + unist-util-visit "^5.0.0" + +mdast-util-directive@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz#f3656f4aab6ae3767d3c72cfab5e8055572ccba1" + integrity sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + parse-entities "^4.0.0" + stringify-entities "^4.0.0" + unist-util-visit-parents "^6.0.0" + +mdast-util-find-and-replace@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz#70a3174c894e14df722abf43bc250cbae44b11df" + integrity sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg== + dependencies: + "@types/mdast" "^4.0.0" + escape-string-regexp "^5.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +mdast-util-from-markdown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz#4850390ca7cf17413a9b9a0fbefcd1bc0eb4160a" + integrity sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + mdast-util-to-string "^4.0.0" + micromark "^4.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-decode-string "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-stringify-position "^4.0.0" + +mdast-util-gfm-autolink-literal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz#abd557630337bd30a6d5a4bd8252e1c2dc0875d5" + integrity sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ== + dependencies: + "@types/mdast" "^4.0.0" + ccount "^2.0.0" + devlop "^1.0.0" + mdast-util-find-and-replace "^3.0.0" + micromark-util-character "^2.0.0" + +mdast-util-gfm-footnote@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz#7778e9d9ca3df7238cc2bd3fa2b1bf6a65b19403" + integrity sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + +mdast-util-gfm-strikethrough@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz#d44ef9e8ed283ac8c1165ab0d0dfd058c2764c16" + integrity sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-gfm-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz#7a435fb6223a72b0862b33afbd712b6dae878d38" + integrity sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + markdown-table "^3.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-gfm-task-list-item@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz#e68095d2f8a4303ef24094ab642e1047b991a936" + integrity sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-gfm@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz#2cdf63b92c2a331406b0fb0db4c077c1b0331751" + integrity sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ== + dependencies: + mdast-util-from-markdown "^2.0.0" + mdast-util-gfm-autolink-literal "^2.0.0" + mdast-util-gfm-footnote "^2.0.0" + mdast-util-gfm-strikethrough "^2.0.0" + mdast-util-gfm-table "^2.0.0" + mdast-util-gfm-task-list-item "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdx-expression@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz#43f0abac9adc756e2086f63822a38c8d3c3a5096" + integrity sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdx-jsx@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz#fd04c67a2a7499efb905a8a5c578dddc9fdada0d" + integrity sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + parse-entities "^4.0.0" + stringify-entities "^4.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + +mdast-util-mdx@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz#792f9cf0361b46bee1fdf1ef36beac424a099c41" + integrity sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w== + dependencies: + mdast-util-from-markdown "^2.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdxjs-esm@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz#019cfbe757ad62dd557db35a695e7314bcc9fa97" + integrity sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-phrasing@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3" + integrity sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w== + dependencies: + "@types/mdast" "^4.0.0" + unist-util-is "^6.0.0" + +mdast-util-to-hast@^13.0.0: + version "13.2.1" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz#d7ff84ca499a57e2c060ae67548ad950e689a053" + integrity sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@ungap/structured-clone" "^1.0.0" + devlop "^1.0.0" + micromark-util-sanitize-uri "^2.0.0" + trim-lines "^3.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +mdast-util-to-markdown@^2.0.0, mdast-util-to-markdown@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz#f910ffe60897f04bb4b7e7ee434486f76288361b" + integrity sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^4.0.0" + mdast-util-to-string "^4.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-decode-string "^2.0.0" + unist-util-visit "^5.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" + integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== + dependencies: + "@types/mdast" "^4.0.0" + +mdn-data@2.0.28: + version "2.0.28" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" + integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g== + +mdn-data@2.12.2: + version "2.12.2" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.12.2.tgz#9ae6c41a9e65adf61318b32bff7b64fbfb13f8cf" + integrity sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA== + +micromark-core-commonmark@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz#c691630e485021a68cf28dbc2b2ca27ebf678cd4" + integrity sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg== + dependencies: + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-factory-destination "^2.0.0" + micromark-factory-label "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-title "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-html-tag-name "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-directive@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz#2eb61985d1995a7c1ff7621676a4f32af29409e8" + integrity sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + parse-entities "^4.0.0" + +micromark-extension-gfm-autolink-literal@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz#6286aee9686c4462c1e3552a9d505feddceeb935" + integrity sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-footnote@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz#4dab56d4e398b9853f6fe4efac4fc9361f3e0750" + integrity sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw== + dependencies: + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-strikethrough@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz#86106df8b3a692b5f6a92280d3879be6be46d923" + integrity sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-table@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz#fac70bcbf51fe65f5f44033118d39be8a9b5940b" + integrity sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-tagfilter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz#f26d8a7807b5985fba13cf61465b58ca5ff7dc57" + integrity sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg== + dependencies: + micromark-util-types "^2.0.0" + +micromark-extension-gfm-task-list-item@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz#bcc34d805639829990ec175c3eea12bb5b781f2c" + integrity sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz#3e13376ab95dd7a5cfd0e29560dfe999657b3c5b" + integrity sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w== + dependencies: + micromark-extension-gfm-autolink-literal "^2.0.0" + micromark-extension-gfm-footnote "^2.0.0" + micromark-extension-gfm-strikethrough "^2.0.0" + micromark-extension-gfm-table "^2.0.0" + micromark-extension-gfm-tagfilter "^2.0.0" + micromark-extension-gfm-task-list-item "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-mdx-expression@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz#43d058d999532fb3041195a3c3c05c46fa84543b" + integrity sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + micromark-factory-mdx-expression "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-mdx-jsx@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz#ffc98bdb649798902fa9fc5689f67f9c1c902044" + integrity sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + micromark-factory-mdx-expression "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + vfile-message "^4.0.0" + +micromark-extension-mdx-md@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz#1d252881ea35d74698423ab44917e1f5b197b92d" + integrity sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ== + dependencies: + micromark-util-types "^2.0.0" + +micromark-extension-mdxjs-esm@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz#de21b2b045fd2059bd00d36746081de38390d54a" + integrity sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-position-from-estree "^2.0.0" + vfile-message "^4.0.0" + +micromark-extension-mdxjs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz#b5a2e0ed449288f3f6f6c544358159557549de18" + integrity sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ== + dependencies: + acorn "^8.0.0" + acorn-jsx "^5.0.0" + micromark-extension-mdx-expression "^3.0.0" + micromark-extension-mdx-jsx "^3.0.0" + micromark-extension-mdx-md "^2.0.0" + micromark-extension-mdxjs-esm "^3.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-destination@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz#8fef8e0f7081f0474fbdd92deb50c990a0264639" + integrity sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-label@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz#5267efa97f1e5254efc7f20b459a38cb21058ba1" + integrity sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg== + dependencies: + devlop "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-mdx-expression@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz#bb09988610589c07d1c1e4425285895041b3dfa9" + integrity sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-position-from-estree "^2.0.0" + vfile-message "^4.0.0" + +micromark-factory-space@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz#36d0212e962b2b3121f8525fc7a3c7c029f334fc" + integrity sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-title@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz#237e4aa5d58a95863f01032d9ee9b090f1de6e94" + integrity sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-whitespace@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz#06b26b2983c4d27bfcc657b33e25134d4868b0b1" + integrity sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-character@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6" + integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-chunked@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz#47fbcd93471a3fccab86cff03847fc3552db1051" + integrity sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-classify-character@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz#d399faf9c45ca14c8b4be98b1ea481bced87b629" + integrity sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-combine-extensions@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz#2a0f490ab08bff5cc2fd5eec6dd0ca04f89b30a9" + integrity sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg== + dependencies: + micromark-util-chunked "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-decode-numeric-character-reference@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz#fcf15b660979388e6f118cdb6bf7d79d73d26fe5" + integrity sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-decode-string@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz#6cb99582e5d271e84efca8e61a807994d7161eb2" + integrity sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8" + integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== + +micromark-util-events-to-acorn@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz#e7a8a6b55a47e5a06c720d5a1c4abae8c37c98f3" + integrity sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg== + dependencies: + "@types/estree" "^1.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + estree-util-visit "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + vfile-message "^4.0.0" + +micromark-util-html-tag-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz#e40403096481986b41c106627f98f72d4d10b825" + integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA== + +micromark-util-normalize-identifier@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz#c30d77b2e832acf6526f8bf1aa47bc9c9438c16d" + integrity sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-resolve-all@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz#e1a2d62cdd237230a2ae11839027b19381e31e8b" + integrity sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg== + dependencies: + micromark-util-types "^2.0.0" + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7" + integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-subtokenize@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz#d8ade5ba0f3197a1cf6a2999fbbfe6357a1a19ee" + integrity sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-symbol@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8" + integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== + +micromark-util-types@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e" + integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== + +micromark@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.2.tgz#91395a3e1884a198e62116e33c9c568e39936fdb" + integrity sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +mrmime@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.1.tgz#bc3e87f7987853a54c9850eeb1f1078cd44adddc" + integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.11: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +neotraverse@^0.6.18: + version "0.6.18" + resolved "https://registry.yarnpkg.com/neotraverse/-/neotraverse-0.6.18.tgz#abcb33dda2e8e713cf6321b29405e822230cdb30" + integrity sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA== + +nlcst-to-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz#05511e8461ebfb415952eb0b7e9a1a7d40471bd4" + integrity sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA== + dependencies: + "@types/nlcst" "^2.0.0" + +node-fetch-native@^1.6.7: + version "1.6.7" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz#9d09ca63066cc48423211ed4caf5d70075d76a71" + integrity sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q== + +node-mock-http@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/node-mock-http/-/node-mock-http-1.0.4.tgz#21f2ab4ce2fe4fbe8a660d7c5195a1db85e042a4" + integrity sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ== + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +nth-check@^2.0.0, nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +ofetch@^1.4.1, ofetch@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/ofetch/-/ofetch-1.5.1.tgz#5c43cc56e03398b273014957060344254505c5c7" + integrity sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA== + dependencies: + destr "^2.0.5" + node-fetch-native "^1.6.7" + ufo "^1.6.1" + +ohash@^2.0.0: + version "2.0.11" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-2.0.11.tgz#60b11e8cff62ca9dee88d13747a5baa145f5900b" + integrity sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ== + +oniguruma-parser@^0.12.1: + version "0.12.1" + resolved "https://registry.yarnpkg.com/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz#82ba2208d7a2b69ee344b7efe0ae930c627dcc4a" + integrity sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w== + +oniguruma-to-es@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz#35ea9104649b7c05f3963c6b3b474d964625028b" + integrity sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g== + dependencies: + emoji-regex-xs "^1.0.0" + regex "^5.1.1" + regex-recursion "^5.1.1" + +oniguruma-to-es@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz#0b909d960faeb84511c979b1f2af64e9bc37ce34" + integrity sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA== + dependencies: + oniguruma-parser "^0.12.1" + regex "^6.0.1" + regex-recursion "^6.0.2" + +p-limit@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-6.2.0.tgz#c254d22ba6aeef441a3564c5e6c2f2da59268a0f" + integrity sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA== + dependencies: + yocto-queue "^1.1.1" + +p-queue@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-8.1.1.tgz#dac3e8c57412fffa18fe6c341b141dbb3a16408b" + integrity sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ== + dependencies: + eventemitter3 "^5.0.1" + p-timeout "^6.1.2" + +p-timeout@^6.1.2: + version "6.1.4" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-6.1.4.tgz#418e1f4dd833fa96a2e3f532547dd2abdb08dbc2" + integrity sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg== + +package-manager-detector@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/package-manager-detector/-/package-manager-detector-1.6.0.tgz#70d0cf0aa02c877eeaf66c4d984ede0be9130734" + integrity sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA== + +pagefind@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/pagefind/-/pagefind-1.4.0.tgz#0154b0a44b5ef9ef55c156824a3244bfc0c4008d" + integrity sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g== + optionalDependencies: + "@pagefind/darwin-arm64" "1.4.0" + "@pagefind/darwin-x64" "1.4.0" + "@pagefind/freebsd-x64" "1.4.0" + "@pagefind/linux-arm64" "1.4.0" + "@pagefind/linux-x64" "1.4.0" + "@pagefind/windows-x64" "1.4.0" + +pako@^0.2.5: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== + +parse-entities@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.2.tgz#61d46f5ed28e4ee62e9ddc43d6b010188443f159" + integrity sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw== + dependencies: + "@types/unist" "^2.0.0" + character-entities-legacy "^3.0.0" + character-reference-invalid "^2.0.0" + decode-named-character-reference "^1.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + is-hexadecimal "^2.0.0" + +parse-latin@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/parse-latin/-/parse-latin-7.0.0.tgz#8dfacac26fa603f76417f36233fc45602a323e1d" + integrity sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ== + dependencies: + "@types/nlcst" "^2.0.0" + "@types/unist" "^3.0.0" + nlcst-to-string "^4.0.0" + unist-util-modify-children "^4.0.0" + unist-util-visit-children "^3.0.0" + vfile "^6.0.0" + +parse5@^7.0.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05" + integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== + dependencies: + entities "^6.0.0" + +piccolore@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/piccolore/-/piccolore-0.1.3.tgz#ef33f6180e6a37b35fe12a45765a900171cfaedc" + integrity sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^4.0.2, picomatch@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + +postcss-nested@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.2.0.tgz#4c2d22ab5f20b9cb61e2c5c5915950784d068131" + integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== + dependencies: + postcss-selector-parser "^6.1.1" + +postcss-selector-parser@^6.1.1: + version "6.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss@^8.4.38, postcss@^8.5.3: + version "8.5.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +prismjs@^1.30.0: + version "1.30.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.30.0.tgz#d9709969d9d4e16403f6f348c63553b19f0975a9" + integrity sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw== + +prompts@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +property-information@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-7.1.0.tgz#b622e8646e02b580205415586b40804d3e8bfd5d" + integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== + +radix3@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/radix3/-/radix3-1.1.2.tgz#fd27d2af3896c6bf4bcdfab6427c69c2afc69ec0" + integrity sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA== + +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + +recma-build-jsx@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz#c02f29e047e103d2fab2054954e1761b8ea253c4" + integrity sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew== + dependencies: + "@types/estree" "^1.0.0" + estree-util-build-jsx "^3.0.0" + vfile "^6.0.0" + +recma-jsx@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/recma-jsx/-/recma-jsx-1.0.1.tgz#58e718f45e2102ed0bf2fa994f05b70d76801a1a" + integrity sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w== + dependencies: + acorn-jsx "^5.0.0" + estree-util-to-js "^2.0.0" + recma-parse "^1.0.0" + recma-stringify "^1.0.0" + unified "^11.0.0" + +recma-parse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/recma-parse/-/recma-parse-1.0.0.tgz#c351e161bb0ab47d86b92a98a9d891f9b6814b52" + integrity sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ== + dependencies: + "@types/estree" "^1.0.0" + esast-util-from-js "^2.0.0" + unified "^11.0.0" + vfile "^6.0.0" + +recma-stringify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/recma-stringify/-/recma-stringify-1.0.0.tgz#54632030631e0c7546136ff9ef8fde8e7b44f130" + integrity sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g== + dependencies: + "@types/estree" "^1.0.0" + estree-util-to-js "^2.0.0" + unified "^11.0.0" + vfile "^6.0.0" + +regex-recursion@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/regex-recursion/-/regex-recursion-5.1.1.tgz#5a73772d18adbf00f57ad097bf54171b39d78f8b" + integrity sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w== + dependencies: + regex "^5.1.1" + regex-utilities "^2.3.0" + +regex-recursion@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/regex-recursion/-/regex-recursion-6.0.2.tgz#a0b1977a74c87f073377b938dbedfab2ea582b33" + integrity sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg== + dependencies: + regex-utilities "^2.3.0" + +regex-utilities@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/regex-utilities/-/regex-utilities-2.3.0.tgz#87163512a15dce2908cf079c8960d5158ff43280" + integrity sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng== + +regex@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/regex/-/regex-5.1.1.tgz#cf798903f24d6fe6e531050a36686e082b29bd03" + integrity sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw== + dependencies: + regex-utilities "^2.3.0" + +regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/regex/-/regex-6.1.0.tgz#d7ce98f8ee32da7497c13f6601fca2bc4a6a7803" + integrity sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg== + dependencies: + regex-utilities "^2.3.0" + +rehype-expressive-code@^0.40.2: + version "0.40.2" + resolved "https://registry.yarnpkg.com/rehype-expressive-code/-/rehype-expressive-code-0.40.2.tgz#93b0541796228eb59a318fbbb1db4a97a2c6a38a" + integrity sha512-+kn+AMGCrGzvtH8Q5lC6Y5lnmTV/r33fdmi5QU/IH1KPHKobKr5UnLwJuqHv5jBTSN/0v2wLDS7RTM73FVzqmQ== + dependencies: + expressive-code "^0.40.2" + +rehype-format@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/rehype-format/-/rehype-format-5.0.1.tgz#e255e59bed0c062156aaf51c16fad5a521a1f5c8" + integrity sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ== + dependencies: + "@types/hast" "^3.0.0" + hast-util-format "^1.0.0" + +rehype-parse@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-9.0.1.tgz#9993bda129acc64c417a9d3654a7be38b2a94c20" + integrity sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag== + dependencies: + "@types/hast" "^3.0.0" + hast-util-from-html "^2.0.0" + unified "^11.0.0" + +rehype-raw@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-7.0.0.tgz#59d7348fd5dbef3807bbaa1d443efd2dd85ecee4" + integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww== + dependencies: + "@types/hast" "^3.0.0" + hast-util-raw "^9.0.0" + vfile "^6.0.0" + +rehype-recma@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rehype-recma/-/rehype-recma-1.0.0.tgz#d68ef6344d05916bd96e25400c6261775411aa76" + integrity sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw== + dependencies: + "@types/estree" "^1.0.0" + "@types/hast" "^3.0.0" + hast-util-to-estree "^3.0.0" + +rehype-stringify@^10.0.0, rehype-stringify@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/rehype-stringify/-/rehype-stringify-10.0.1.tgz#2ec1ebc56c6aba07905d3b4470bdf0f684f30b75" + integrity sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA== + dependencies: + "@types/hast" "^3.0.0" + hast-util-to-html "^9.0.0" + unified "^11.0.0" + +rehype@^13.0.1, rehype@^13.0.2: + version "13.0.2" + resolved "https://registry.yarnpkg.com/rehype/-/rehype-13.0.2.tgz#ab0b3ac26573d7b265a0099feffad450e4cf1952" + integrity sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A== + dependencies: + "@types/hast" "^3.0.0" + rehype-parse "^9.0.0" + rehype-stringify "^10.0.0" + unified "^11.0.0" + +remark-directive@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/remark-directive/-/remark-directive-3.0.1.tgz#689ba332f156cfe1118e849164cc81f157a3ef0a" + integrity sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-directive "^3.0.0" + micromark-extension-directive "^3.0.0" + unified "^11.0.0" + +remark-gfm@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.1.tgz#33227b2a74397670d357bf05c098eaf8513f0d6b" + integrity sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-gfm "^3.0.0" + micromark-extension-gfm "^3.0.0" + remark-parse "^11.0.0" + remark-stringify "^11.0.0" + unified "^11.0.0" + +remark-mdx@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-3.1.1.tgz#047f97038bc7ec387aebb4b0a4fe23779999d845" + integrity sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg== + dependencies: + mdast-util-mdx "^3.0.0" + micromark-extension-mdxjs "^3.0.0" + +remark-parse@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" + integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + micromark-util-types "^2.0.0" + unified "^11.0.0" + +remark-rehype@^11.0.0, remark-rehype@^11.1.2: + version "11.1.2" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-11.1.2.tgz#2addaadda80ca9bd9aa0da763e74d16327683b37" + integrity sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + mdast-util-to-hast "^13.0.0" + unified "^11.0.0" + vfile "^6.0.0" + +remark-smartypants@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/remark-smartypants/-/remark-smartypants-3.0.2.tgz#cbaf2b39624c78fcbd6efa224678c1d2e9bc1dfb" + integrity sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA== + dependencies: + retext "^9.0.0" + retext-smartypants "^6.0.0" + unified "^11.0.4" + unist-util-visit "^5.0.0" + +remark-stringify@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-11.0.0.tgz#4c5b01dd711c269df1aaae11743eb7e2e7636fd3" + integrity sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-to-markdown "^2.0.0" + unified "^11.0.0" + +restructure@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/restructure/-/restructure-3.0.2.tgz#e6b2fad214f78edee21797fa8160fef50eb9b49a" + integrity sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw== + +retext-latin@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/retext-latin/-/retext-latin-4.0.0.tgz#d02498aa1fd39f1bf00e2ff59b1384c05d0c7ce3" + integrity sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA== + dependencies: + "@types/nlcst" "^2.0.0" + parse-latin "^7.0.0" + unified "^11.0.0" + +retext-smartypants@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/retext-smartypants/-/retext-smartypants-6.2.0.tgz#4e852c2974cf2cfa253eeec427c97efc43b5d158" + integrity sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ== + dependencies: + "@types/nlcst" "^2.0.0" + nlcst-to-string "^4.0.0" + unist-util-visit "^5.0.0" + +retext-stringify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/retext-stringify/-/retext-stringify-4.0.0.tgz#501d5440bd4d121e351c7c509f8507de9611e159" + integrity sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA== + dependencies: + "@types/nlcst" "^2.0.0" + nlcst-to-string "^4.0.0" + unified "^11.0.0" + +retext@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/retext/-/retext-9.0.0.tgz#ab5cd72836894167b0ca6ae70fdcfaa166267f7a" + integrity sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA== + dependencies: + "@types/nlcst" "^2.0.0" + retext-latin "^4.0.0" + retext-stringify "^4.0.0" + unified "^11.0.0" + +rollup@^4.34.9: + version "4.54.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.54.0.tgz#930f4dfc41ff94d720006f9f62503612a6c319b8" + integrity sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw== + dependencies: + "@types/estree" "1.0.8" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.54.0" + "@rollup/rollup-android-arm64" "4.54.0" + "@rollup/rollup-darwin-arm64" "4.54.0" + "@rollup/rollup-darwin-x64" "4.54.0" + "@rollup/rollup-freebsd-arm64" "4.54.0" + "@rollup/rollup-freebsd-x64" "4.54.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.54.0" + "@rollup/rollup-linux-arm-musleabihf" "4.54.0" + "@rollup/rollup-linux-arm64-gnu" "4.54.0" + "@rollup/rollup-linux-arm64-musl" "4.54.0" + "@rollup/rollup-linux-loong64-gnu" "4.54.0" + "@rollup/rollup-linux-ppc64-gnu" "4.54.0" + "@rollup/rollup-linux-riscv64-gnu" "4.54.0" + "@rollup/rollup-linux-riscv64-musl" "4.54.0" + "@rollup/rollup-linux-s390x-gnu" "4.54.0" + "@rollup/rollup-linux-x64-gnu" "4.54.0" + "@rollup/rollup-linux-x64-musl" "4.54.0" + "@rollup/rollup-openharmony-arm64" "4.54.0" + "@rollup/rollup-win32-arm64-msvc" "4.54.0" + "@rollup/rollup-win32-ia32-msvc" "4.54.0" + "@rollup/rollup-win32-x64-gnu" "4.54.0" + "@rollup/rollup-win32-x64-msvc" "4.54.0" + fsevents "~2.3.2" + +sax@^1.4.1: + version "1.4.3" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.3.tgz#fcebae3b756cdc8428321805f4b70f16ec0ab5db" + integrity sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ== + +semver@^7.6.3, semver@^7.7.3: + version "7.7.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== + +sharp@^0.33.5: + version "0.33.5" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.5.tgz#13e0e4130cc309d6a9497596715240b2ec0c594e" + integrity sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw== + dependencies: + color "^4.2.3" + detect-libc "^2.0.3" + semver "^7.6.3" + optionalDependencies: + "@img/sharp-darwin-arm64" "0.33.5" + "@img/sharp-darwin-x64" "0.33.5" + "@img/sharp-libvips-darwin-arm64" "1.0.4" + "@img/sharp-libvips-darwin-x64" "1.0.4" + "@img/sharp-libvips-linux-arm" "1.0.5" + "@img/sharp-libvips-linux-arm64" "1.0.4" + "@img/sharp-libvips-linux-s390x" "1.0.4" + "@img/sharp-libvips-linux-x64" "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" + "@img/sharp-libvips-linuxmusl-x64" "1.0.4" + "@img/sharp-linux-arm" "0.33.5" + "@img/sharp-linux-arm64" "0.33.5" + "@img/sharp-linux-s390x" "0.33.5" + "@img/sharp-linux-x64" "0.33.5" + "@img/sharp-linuxmusl-arm64" "0.33.5" + "@img/sharp-linuxmusl-x64" "0.33.5" + "@img/sharp-wasm32" "0.33.5" + "@img/sharp-win32-ia32" "0.33.5" + "@img/sharp-win32-x64" "0.33.5" + +sharp@^0.34.0: + version "0.34.5" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.5.tgz#b6f148e4b8c61f1797bde11a9d1cfebbae2c57b0" + integrity sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg== + dependencies: + "@img/colour" "^1.0.0" + detect-libc "^2.1.2" + semver "^7.7.3" + optionalDependencies: + "@img/sharp-darwin-arm64" "0.34.5" + "@img/sharp-darwin-x64" "0.34.5" + "@img/sharp-libvips-darwin-arm64" "1.2.4" + "@img/sharp-libvips-darwin-x64" "1.2.4" + "@img/sharp-libvips-linux-arm" "1.2.4" + "@img/sharp-libvips-linux-arm64" "1.2.4" + "@img/sharp-libvips-linux-ppc64" "1.2.4" + "@img/sharp-libvips-linux-riscv64" "1.2.4" + "@img/sharp-libvips-linux-s390x" "1.2.4" + "@img/sharp-libvips-linux-x64" "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64" "1.2.4" + "@img/sharp-libvips-linuxmusl-x64" "1.2.4" + "@img/sharp-linux-arm" "0.34.5" + "@img/sharp-linux-arm64" "0.34.5" + "@img/sharp-linux-ppc64" "0.34.5" + "@img/sharp-linux-riscv64" "0.34.5" + "@img/sharp-linux-s390x" "0.34.5" + "@img/sharp-linux-x64" "0.34.5" + "@img/sharp-linuxmusl-arm64" "0.34.5" + "@img/sharp-linuxmusl-x64" "0.34.5" + "@img/sharp-wasm32" "0.34.5" + "@img/sharp-win32-arm64" "0.34.5" + "@img/sharp-win32-ia32" "0.34.5" + "@img/sharp-win32-x64" "0.34.5" + +shiki@^1.26.1: + version "1.29.2" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.29.2.tgz#5c93771f2d5305ce9c05975c33689116a27dc657" + integrity sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg== + dependencies: + "@shikijs/core" "1.29.2" + "@shikijs/engine-javascript" "1.29.2" + "@shikijs/engine-oniguruma" "1.29.2" + "@shikijs/langs" "1.29.2" + "@shikijs/themes" "1.29.2" + "@shikijs/types" "1.29.2" + "@shikijs/vscode-textmate" "^10.0.1" + "@types/hast" "^3.0.4" + +shiki@^3.15.0, shiki@^3.19.0: + version "3.20.0" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-3.20.0.tgz#1eb8669857373d74e90822e03663a86b5b1f9a24" + integrity sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg== + dependencies: + "@shikijs/core" "3.20.0" + "@shikijs/engine-javascript" "3.20.0" + "@shikijs/engine-oniguruma" "3.20.0" + "@shikijs/langs" "3.20.0" + "@shikijs/themes" "3.20.0" + "@shikijs/types" "3.20.0" + "@shikijs/vscode-textmate" "^10.0.2" + "@types/hast" "^3.0.4" + +simple-swizzle@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz#a8d11a45a11600d6a1ecdff6363329e3648c3667" + integrity sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw== + dependencies: + is-arrayish "^0.3.1" + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +sitemap@^8.0.0: + version "8.0.2" + resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-8.0.2.tgz#27bddb5fc2c61a1cf8f0194674cd89d762c9f5ae" + integrity sha512-LwktpJcyZDoa0IL6KT++lQ53pbSrx2c9ge41/SeLTyqy2XUNA6uR4+P9u5IVo5lPeL2arAcOKn1aZAxoYbCKlQ== + dependencies: + "@types/node" "^17.0.5" + "@types/sax" "^1.2.1" + arg "^5.0.0" + sax "^1.4.1" + +smol-toml@^1.5.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/smol-toml/-/smol-toml-1.6.0.tgz#7911830b47bb3e87be536f939453e10c9e1dfd36" + integrity sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw== + +source-map-js@^1.0.1, source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map@^0.7.0, source-map@^0.7.6: + version "0.7.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.6.tgz#a3658ab87e5b6429c8a1f3ba0083d4c61ca3ef02" + integrity sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ== + +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + +stream-replace-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/stream-replace-string/-/stream-replace-string-2.0.0.tgz#e49fd584bd1c633613e010bc73b9db49cb5024ad" + integrity sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w== + +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^7.0.0, string-width@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.1.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" + integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== + dependencies: + ansi-regex "^6.0.1" + +style-to-js@^1.0.0: + version "1.1.21" + resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.21.tgz#2908941187f857e79e28e9cd78008b9a0b3e0e8d" + integrity sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ== + dependencies: + style-to-object "1.0.14" + +style-to-object@1.0.14: + version "1.0.14" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.14.tgz#1d22f0e7266bb8c6d8cae5caf4ec4f005e08f611" + integrity sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw== + dependencies: + inline-style-parser "0.2.7" + +svgo@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-4.0.0.tgz#17e0fa2eaccf429e0ec0d2179169abde9ba8ad3d" + integrity sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw== + dependencies: + commander "^11.1.0" + css-select "^5.1.0" + css-tree "^3.0.1" + css-what "^6.1.0" + csso "^5.0.5" + picocolors "^1.1.1" + sax "^1.4.1" + +tiny-inflate@^1.0.0, tiny-inflate@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" + integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== + +tinyexec@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.0.2.tgz#bdd2737fe2ba40bd6f918ae26642f264b99ca251" + integrity sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg== + +tinyglobby@^0.2.13, tinyglobby@^0.2.15: + version "0.2.15" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" + integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.3" + +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + +trough@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" + integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== + +tsconfck@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.6.tgz#da1f0b10d82237ac23422374b3fce1edb23c3ead" + integrity sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w== + +tslib@^2.4.0, tslib@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +type-fest@^4.21.0: + version "4.41.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" + integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== + +ufo@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b" + integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA== + +ultrahtml@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/ultrahtml/-/ultrahtml-1.6.0.tgz#0d1aad7bbfeae512438d30e799c11622127a1ac8" + integrity sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw== + +uncrypto@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b" + integrity sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q== + +undici-types@~7.16.0: + version "7.16.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== + +unicode-properties@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/unicode-properties/-/unicode-properties-1.4.1.tgz#96a9cffb7e619a0dc7368c28da27e05fc8f9be5f" + integrity sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg== + dependencies: + base64-js "^1.3.0" + unicode-trie "^2.0.0" + +unicode-trie@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-trie/-/unicode-trie-2.0.0.tgz#8fd8845696e2e14a8b67d78fa9e0dd2cad62fec8" + integrity sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ== + dependencies: + pako "^0.2.5" + tiny-inflate "^1.0.0" + +unified@^11.0.0, unified@^11.0.4, unified@^11.0.5: + version "11.0.5" + resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.5.tgz#f66677610a5c0a9ee90cab2b8d4d66037026d9e1" + integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA== + dependencies: + "@types/unist" "^3.0.0" + bail "^2.0.0" + devlop "^1.0.0" + extend "^3.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^6.0.0" + +unifont@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/unifont/-/unifont-0.6.0.tgz#c0ddd6411f1917f934907d989b4566842c5b482b" + integrity sha512-5Fx50fFQMQL5aeHyWnZX9122sSLckcDvcfFiBf3QYeHa7a1MKJooUy52b67moi2MJYkrfo/TWY+CoLdr/w0tTA== + dependencies: + css-tree "^3.0.0" + ofetch "^1.4.1" + ohash "^2.0.0" + +unist-util-find-after@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz#3fccc1b086b56f34c8b798e1ff90b5c54468e896" + integrity sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-is@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.1.tgz#d0a3f86f2dd0db7acd7d8c2478080b5c67f9c6a9" + integrity sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-modify-children@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz#981d6308e887b005d1f491811d3cbcc254b315e9" + integrity sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw== + dependencies: + "@types/unist" "^3.0.0" + array-iterate "^2.0.0" + +unist-util-position-from-estree@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz#d94da4df596529d1faa3de506202f0c9a23f2200" + integrity sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" + integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-remove-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz#fea68a25658409c9460408bc6b4991b965b52163" + integrity sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q== + dependencies: + "@types/unist" "^3.0.0" + unist-util-visit "^5.0.0" + +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-visit-children@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz#4bced199b71d7f3c397543ea6cc39e7a7f37dc7e" + integrity sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-visit-parents@^6.0.0, unist-util-visit-parents@^6.0.1, unist-util-visit-parents@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz#777df7fb98652ce16b4b7cd999d0a1a40efa3a02" + integrity sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +unstorage@^1.17.3: + version "1.17.3" + resolved "https://registry.yarnpkg.com/unstorage/-/unstorage-1.17.3.tgz#805acbeab7f7b97f0d0492427af18e650eda4e57" + integrity sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q== + dependencies: + anymatch "^3.1.3" + chokidar "^4.0.3" + destr "^2.0.5" + h3 "^1.15.4" + lru-cache "^10.4.3" + node-fetch-native "^1.6.7" + ofetch "^1.5.1" + ufo "^1.6.1" + +util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +vfile-location@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.3.tgz#cb9eacd20f2b6426d19451e0eafa3d0a846225c3" + integrity sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg== + dependencies: + "@types/unist" "^3.0.0" + vfile "^6.0.0" + +vfile-message@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4" + integrity sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + +vfile@^6.0.0, vfile@^6.0.2, vfile@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" + integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== + dependencies: + "@types/unist" "^3.0.0" + vfile-message "^4.0.0" + +vite@^6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.4.1.tgz#afbe14518cdd6887e240a4b0221ab6d0ce733f96" + integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g== + dependencies: + esbuild "^0.25.0" + fdir "^6.4.4" + picomatch "^4.0.2" + postcss "^8.5.3" + rollup "^4.34.9" + tinyglobby "^0.2.13" + optionalDependencies: + fsevents "~2.3.3" + +vitefu@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-1.1.1.tgz#c39b7e4c91bf2f6c590fb96e0758f394dff5795b" + integrity sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ== + +web-namespaces@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" + integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== + +which-pm-runs@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.1.0.tgz#35ccf7b1a0fce87bd8b92a478c9d045785d3bf35" + integrity sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA== + +widest-line@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-5.0.0.tgz#b74826a1e480783345f0cd9061b49753c9da70d0" + integrity sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA== + dependencies: + string-width "^7.0.0" + +wrap-ansi@^9.0.0: + version "9.0.2" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz#956832dea9494306e6d209eb871643bb873d7c98" + integrity sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww== + dependencies: + ansi-styles "^6.2.1" + string-width "^7.0.0" + strip-ansi "^7.1.0" + +xxhash-wasm@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz#ffe7f0b98220a4afac171e3fb9b6d1f8771f015e" + integrity sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yocto-queue@^1.1.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.2.2.tgz#3e09c95d3f1aa89a58c114c99223edf639152c00" + integrity sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ== + +yocto-spinner@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/yocto-spinner/-/yocto-spinner-0.2.3.tgz#e803d2f267c7f0c3188645878522066764263a13" + integrity sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ== + dependencies: + yoctocolors "^2.1.1" + +yoctocolors@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yoctocolors/-/yoctocolors-2.1.2.tgz#d795f54d173494e7d8db93150cec0ed7f678c83a" + integrity sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug== + +zod-to-json-schema@^3.25.0: + version "3.25.0" + resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz#df504c957c4fb0feff467c74d03e6aab0b013e1c" + integrity sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ== + +zod-to-ts@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/zod-to-ts/-/zod-to-ts-1.2.0.tgz#873a2fd8242d7b649237be97e0c64d7954ae0c51" + integrity sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA== + +zod@^3.25.76: + version "3.25.76" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== + +zwitch@^2.0.0, zwitch@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== diff --git a/install/action.yml b/install/action.yml new file mode 100644 index 00000000..e84c4bfd --- /dev/null +++ b/install/action.yml @@ -0,0 +1,71 @@ +name: "Install Craft" +description: "Install Craft CLI (from build artifact for dogfooding, build from source, or from release)" + +inputs: + craft-version: + description: 'Version of Craft to install (tag or "latest"). Only used when installing from release.' + required: false + default: 'latest' + +runs: + using: "composite" + steps: + - name: Download Craft from build artifact + id: artifact + if: github.repository == 'getsentry/craft' + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 + continue-on-error: true + with: + name: ${{ github.sha }} + path: /tmp/craft-artifact + + - name: Install Craft from artifact + if: steps.artifact.outcome == 'success' + shell: bash + run: | + echo "Installing Craft from build artifact..." + sudo install -m 755 /tmp/craft-artifact/dist/craft /usr/local/bin/craft + + # For getsentry/craft repo: build from source if no artifact available + - name: Setup Node.js (for building from source) + if: github.repository == 'getsentry/craft' && steps.artifact.outcome != 'success' + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Build Craft from source + id: build + if: github.repository == 'getsentry/craft' && steps.artifact.outcome != 'success' + shell: bash + run: | + echo "Building Craft from source..." + yarn install --frozen-lockfile + yarn build + sudo install -m 755 dist/craft /usr/local/bin/craft + + - name: Install Craft from release + if: steps.artifact.outcome != 'success' && steps.build.outcome != 'success' + shell: bash + env: + CRAFT_VERSION: ${{ inputs.craft-version }} + run: | + if [[ "$CRAFT_VERSION" == "latest" || -z "$CRAFT_VERSION" ]]; then + # Try action ref first (e.g., v2, 2.15.0) + ACTION_REF="${{ github.action_ref }}" + CRAFT_URL="https://github.com/getsentry/craft/releases/download/${ACTION_REF}/craft" + + echo "Trying to download Craft from: ${CRAFT_URL}" + + # Fallback to latest if ref doesn't have a release + if ! curl -sfI "$CRAFT_URL" >/dev/null 2>&1; then + echo "Release not found for ref '${ACTION_REF}', falling back to latest..." + CRAFT_URL=$(curl -s "https://api.github.com/repos/getsentry/craft/releases/latest" \ + | jq -r '.assets[] | select(.name == "craft") | .browser_download_url') + fi + else + CRAFT_URL="https://github.com/getsentry/craft/releases/download/${CRAFT_VERSION}/craft" + fi + + echo "Installing Craft from: ${CRAFT_URL}" + sudo curl -sL -o /usr/local/bin/craft "$CRAFT_URL" + sudo chmod +x /usr/local/bin/craft diff --git a/package.json b/package.json index 445cf43d..16eda59d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/craft", - "version": "2.12.1", + "version": "2.16.0-dev.0", "description": "The universal sentry workflow CLI", "main": "dist/craft", "repository": "https://github.com/getsentry/craft", @@ -17,7 +17,7 @@ "**/dot-prop": "^5.3.0", "**/kind-of": ">=6.0.3", "**/node-fetch": "^2.6.7", - "**/yargs-parser": "~18.1.3", + "**/yargs-parser": ">=18.1.3", "**/parse-url": ">=5.0.3", "**/ansi-regex": ">=5.0.1 < 6.0.0", "@jest/reporters/**/strip-ansi": "^6.0.1" @@ -47,7 +47,7 @@ "@types/shell-quote": "^1.6.0", "@types/tar": "^4.0.0", "@types/tmp": "^0.0.33", - "@types/yargs": "^15.0.3", + "@types/yargs": "^17", "@typescript-eslint/eslint-plugin": "^5.19.0", "@typescript-eslint/parser": "^5.19.0", "ajv": "6.12.6", @@ -63,6 +63,7 @@ "extract-zip": "^2.0.1", "fast-xml-parser": "^4.2.4", "git-url-parse": "^16.1.0", + "glob": "^11.0.0", "is-ci": "^2.0.0", "jest": "^29.7.0", "js-yaml": "4.1.1", @@ -85,12 +86,12 @@ "tmp": "0.2.4", "ts-jest": "^29.1.1", "typescript": "^5.1.6", - "yargs": "15.4.1" + "yargs": "^18" }, "scripts": { "build:fat": "yarn run compile-config-schema && tsc -p tsconfig.build.json", "build:watch": "yarn run compile-config-schema && tsc -p tsconfig.build.json --watch", - "build": "yarn compile-config-schema && esbuild src/index.ts --sourcemap --bundle --platform=node --target=node20 --inject:./src/utils/import-meta-url.js --define:import.meta.url=import_meta_url --outfile=dist/craft --minify", + "build": "yarn compile-config-schema && esbuild src/index.ts --sourcemap --bundle --platform=node --target=node22 --inject:./src/utils/import-meta-url.js --define:import.meta.url=import_meta_url --outfile=dist/craft", "precli": "yarn build", "cli": "node -r source-map-support/register dist/craft", "clean": "rimraf dist coverage", @@ -98,7 +99,9 @@ "fix": "yarn lint --fix", "test": "jest", "test:watch": "jest --watch --notify", - "compile-config-schema": "node ./scripts/config-json-schema-to-ts.js" + "compile-config-schema": "node ./scripts/config-json-schema-to-ts.js", + "docs:dev": "cd docs && yarn dev", + "docs:build": "cd docs && yarn build" }, "volta": { "node": "22.12.0", diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 642eadb6..a9cb9e7e 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -1,3 +1,79 @@ -test('it works', () => { - expect(true).toBeTruthy(); +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import { resolve } from 'path'; + +const execFileAsync = promisify(execFile); + +// Path to the TypeScript source file - we use ts-node to run it directly +const CLI_ENTRY = resolve(__dirname, '../index.ts'); + +describe('CLI smoke tests', () => { + // Increase timeout for CLI tests as they spawn processes + jest.setTimeout(30000); + + test('CLI starts and shows help without runtime errors', async () => { + // This catches issues like: + // - Missing dependencies + // - Syntax errors + // - Runtime initialization errors (e.g., yargs singleton usage in v18) + const { stdout, stderr } = await execFileAsync( + 'npx', + ['ts-node', '--transpile-only', CLI_ENTRY, '--help'], + { env: { ...process.env, NODE_ENV: 'test' } } + ); + + expect(stdout).toMatch(//); + expect(stdout).toContain('prepare NEW-VERSION'); + expect(stdout).toContain('publish NEW-VERSION'); + expect(stdout).toContain('--help'); + // Ensure no error output (warnings are acceptable) + expect(stderr).not.toContain('Error'); + expect(stderr).not.toContain('TypeError'); + }); + + test('CLI shows version without errors', async () => { + const { stdout } = await execFileAsync( + 'npx', + ['ts-node', '--transpile-only', CLI_ENTRY, '--version'], + { env: { ...process.env, NODE_ENV: 'test' } } + ); + + // Version should be a semver-like string + expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+/); + }); + + test('CLI exits with error for unknown command', async () => { + // This ensures yargs command parsing works and async handlers are awaited + await expect( + execFileAsync( + 'npx', + ['ts-node', '--transpile-only', CLI_ENTRY, 'nonexistent-command'], + { env: { ...process.env, NODE_ENV: 'test' } } + ) + ).rejects.toMatchObject({ + code: 1, + }); + }); + + test('async command handler completes properly', async () => { + // The 'targets' command has an async handler and requires a .craft.yml + // Without proper await on parse(), this would exit before completing + // We expect it to fail due to missing config, but it should fail gracefully + // not due to premature exit + try { + await execFileAsync( + 'npx', + ['ts-node', '--transpile-only', CLI_ENTRY, 'targets'], + { + env: { ...process.env, NODE_ENV: 'test' }, + cwd: '/tmp', // No .craft.yml here + } + ); + } catch (error: any) { + // Should fail with a config error, not a silent exit or unhandled promise + expect(error.stderr || error.stdout).toMatch( + /Cannot find configuration file|craft\.yml|config/i + ); + } + }); }); diff --git a/src/commands/__tests__/prepare.test.ts b/src/commands/__tests__/prepare.test.ts index e5baaad1..d280fcde 100644 --- a/src/commands/__tests__/prepare.test.ts +++ b/src/commands/__tests__/prepare.test.ts @@ -69,6 +69,31 @@ describe('checkVersionOrPart', () => { } }); + test('return true for auto version', () => { + expect( + checkVersionOrPart( + { + newVersion: 'auto', + }, + null + ) + ).toBe(true); + }); + + test('return true for version bump types', () => { + const bumpTypes = ['major', 'minor', 'patch']; + for (const bumpType of bumpTypes) { + expect( + checkVersionOrPart( + { + newVersion: bumpType, + }, + null + ) + ).toBe(true); + } + }); + test('throw an error for invalid version', () => { const invalidVersions = [ { @@ -80,9 +105,6 @@ describe('checkVersionOrPart', () => { e: 'Invalid version or version part specified: "v2.3.3". Removing the "v" prefix will likely fix the issue', }, - { v: 'major', e: 'Version part is not supported yet' }, - { v: 'minor', e: 'Version part is not supported yet' }, - { v: 'patch', e: 'Version part is not supported yet' }, ]; for (const t of invalidVersions) { const fn = () => { diff --git a/src/commands/__tests__/targets.test.ts b/src/commands/__tests__/targets.test.ts new file mode 100644 index 00000000..2f2e6d8b --- /dev/null +++ b/src/commands/__tests__/targets.test.ts @@ -0,0 +1,117 @@ +import { handler } from '../targets'; + +jest.mock('../../config', () => ({ + getConfiguration: jest.fn(), + expandWorkspaceTargets: jest.fn(), +})); + +jest.mock('../../targets', () => ({ + getAllTargetNames: jest.fn(), +})); + +import { getConfiguration, expandWorkspaceTargets } from '../../config'; +import { getAllTargetNames } from '../../targets'; + +describe('targets command', () => { + const mockedGetConfiguration = getConfiguration as jest.Mock; + const mockedExpandWorkspaceTargets = expandWorkspaceTargets as jest.Mock; + const mockedGetAllTargetNames = getAllTargetNames as jest.Mock; + let consoleSpy: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + test('lists targets without expansion when no workspaces', async () => { + const targets = [ + { name: 'npm' }, + { name: 'github' }, + ]; + + mockedGetConfiguration.mockReturnValue({ targets }); + mockedExpandWorkspaceTargets.mockResolvedValue(targets); + mockedGetAllTargetNames.mockReturnValue(['npm', 'github', 'pypi']); + + await handler(); + + expect(mockedExpandWorkspaceTargets).toHaveBeenCalledWith(targets); + expect(consoleSpy).toHaveBeenCalled(); + const output = JSON.parse(consoleSpy.mock.calls[0][0]); + expect(output).toEqual(['npm', 'github']); + }); + + test('lists expanded workspace targets', async () => { + const originalTargets = [ + { name: 'npm', workspaces: true }, + { name: 'github' }, + ]; + + const expandedTargets = [ + { name: 'npm', id: '@sentry/core' }, + { name: 'npm', id: '@sentry/browser' }, + { name: 'github' }, + ]; + + mockedGetConfiguration.mockReturnValue({ targets: originalTargets }); + mockedExpandWorkspaceTargets.mockResolvedValue(expandedTargets); + mockedGetAllTargetNames.mockReturnValue(['npm', 'github']); + + await handler(); + + expect(mockedExpandWorkspaceTargets).toHaveBeenCalledWith(originalTargets); + expect(consoleSpy).toHaveBeenCalled(); + const output = JSON.parse(consoleSpy.mock.calls[0][0]); + expect(output).toEqual([ + 'npm[@sentry/core]', + 'npm[@sentry/browser]', + 'github', + ]); + }); + + test('filters out unknown target names', async () => { + const targets = [ + { name: 'npm' }, + { name: 'unknown-target' }, + { name: 'github' }, + ]; + + mockedGetConfiguration.mockReturnValue({ targets }); + mockedExpandWorkspaceTargets.mockResolvedValue(targets); + mockedGetAllTargetNames.mockReturnValue(['npm', 'github']); + + await handler(); + + expect(consoleSpy).toHaveBeenCalled(); + const output = JSON.parse(consoleSpy.mock.calls[0][0]); + expect(output).toEqual(['npm', 'github']); + }); + + test('handles empty targets list', async () => { + mockedGetConfiguration.mockReturnValue({ targets: [] }); + mockedExpandWorkspaceTargets.mockResolvedValue([]); + mockedGetAllTargetNames.mockReturnValue(['npm', 'github']); + + await handler(); + + expect(consoleSpy).toHaveBeenCalled(); + const output = JSON.parse(consoleSpy.mock.calls[0][0]); + expect(output).toEqual([]); + }); + + test('handles undefined targets', async () => { + mockedGetConfiguration.mockReturnValue({}); + mockedExpandWorkspaceTargets.mockResolvedValue([]); + mockedGetAllTargetNames.mockReturnValue(['npm', 'github']); + + await handler(); + + expect(consoleSpy).toHaveBeenCalled(); + const output = JSON.parse(consoleSpy.mock.calls[0][0]); + expect(output).toEqual([]); + }); +}); diff --git a/src/commands/changelog.ts b/src/commands/changelog.ts new file mode 100644 index 00000000..53cfed7b --- /dev/null +++ b/src/commands/changelog.ts @@ -0,0 +1,96 @@ +import { Argv, CommandBuilder } from 'yargs'; + +import { logger } from '../logger'; +import { getGitClient, getLatestTag } from '../utils/git'; +import { + generateChangesetFromGit, + generateChangelogWithHighlight, +} from '../utils/changelog'; +import { handleGlobalError } from '../utils/errors'; + +export const command = ['changelog']; +export const description = 'Generate changelog from git history'; + +/** Output format options */ +type OutputFormat = 'text' | 'json'; + +/** Command line options */ +interface ChangelogOptions { + /** Base revision to generate changelog from (defaults to latest tag) */ + since?: string; + /** PR number for the current (unmerged) PR */ + pr?: number; + /** Output format: text (default) or json */ + format?: OutputFormat; +} + +export const builder: CommandBuilder = (yargs: Argv) => + yargs + .option('since', { + alias: 's', + description: + 'Base revision (tag or SHA) to generate changelog from. Defaults to latest tag.', + type: 'string', + }) + .option('pr', { + description: + 'PR number for the current (unmerged) PR. The PR info will be fetched from GitHub API and the PR included in the changelog with highlighting.', + type: 'number', + }) + .option('format', { + alias: 'f', + description: 'Output format: text (default) or json', + type: 'string', + choices: ['text', 'json'] as const, + default: 'text', + }); + +/** + * Body of 'changelog' command + */ +export async function changelogMain(argv: ChangelogOptions): Promise { + const git = await getGitClient(); + + // Determine base revision for changelog generation + let since = argv.since; + if (!since) { + since = await getLatestTag(git); + if (since) { + logger.debug(`Using latest tag as base revision: ${since}`); + } else { + logger.debug('No tags found, generating changelog from beginning of history'); + } + } + + // Generate changelog - use different function depending on whether PR is specified + const result = argv.pr + ? await generateChangelogWithHighlight(git, since, argv.pr) + : await generateChangesetFromGit(git, since); + + // Output based on format + if (argv.format === 'json') { + const output = { + changelog: result.changelog || '', + bumpType: result.bumpType, + totalCommits: result.totalCommits, + matchedCommitsWithSemver: result.matchedCommitsWithSemver, + }; + console.log(JSON.stringify(output, null, 2)); + } else { + if (!result.changelog) { + console.log('No changelog entries found.'); + return; + } + console.log(result.changelog); + } +} + +export const handler = async (args: { + [argName: string]: any; +}): Promise => { + try { + return await changelogMain(args as ChangelogOptions); + } catch (e) { + handleGlobalError(e); + } +}; diff --git a/src/commands/prepare.ts b/src/commands/prepare.ts index d9429410..d2ed758f 100644 --- a/src/commands/prepare.ts +++ b/src/commands/prepare.ts @@ -8,9 +8,14 @@ import { getConfiguration, DEFAULT_RELEASE_BRANCH_NAME, getGlobalGitHubConfig, + requiresMinVersion, + loadConfigurationFromString, + CONFIG_FILE_NAME, + getVersioningPolicy, } from '../config'; import { logger } from '../logger'; -import { ChangelogPolicy } from '../schemas/project_config'; +import { ChangelogPolicy, VersioningPolicy } from '../schemas/project_config'; +import { calculateCalVer, DEFAULT_CALVER_CONFIG } from '../utils/calver'; import { sleep } from '../utils/async'; import { DEFAULT_CHANGELOG_PATH, @@ -26,7 +31,18 @@ import { reportError, } from '../utils/errors'; import { getGitClient, getDefaultBranch, getLatestTag } from '../utils/git'; -import { isDryRun, promptConfirmation } from '../utils/helpers'; +import { + getChangelogWithBumpType, + calculateNextVersion, + validateBumpType, + isBumpType, + type BumpType, +} from '../utils/autoVersion'; +import { + isDryRun, + promptConfirmation, + setGitHubActionsOutput, +} from '../utils/helpers'; import { formatJson } from '../utils/strings'; import { spawnProcess } from '../utils/system'; import { isValidVersion } from '../utils/version'; @@ -40,10 +56,17 @@ export const description = '🚢 Prepare a new release branch'; /** Default path to bump-version script, relative to project root */ const DEFAULT_BUMP_VERSION_PATH = join('scripts', 'bump-version.sh'); +/** Minimum craft version required for auto-versioning */ +const AUTO_VERSION_MIN_VERSION = '2.14.0'; + export const builder: CommandBuilder = (yargs: Argv) => yargs .positional('NEW-VERSION', { - description: 'The new version you want to release', + description: + 'The new version to release. Can be: a semver string (e.g., "1.2.3"), ' + + 'a bump type ("major", "minor", or "patch"), "auto" to determine automatically ' + + 'from conventional commits, or "calver" for calendar versioning. ' + + 'If omitted, uses the versioning.policy from .craft.yml', type: 'string', }) .option('rev', { @@ -77,12 +100,20 @@ export const builder: CommandBuilder = (yargs: Argv) => description: 'The git remote to use when pushing', type: 'string', }) + .option('config-from', { + description: 'Load .craft.yml from the specified remote branch instead of local file', + type: 'string', + }) + .option('calver-offset', { + description: 'Days to go back for CalVer date calculation (overrides config)', + type: 'number', + }) .check(checkVersionOrPart); /** Command line options. */ interface PrepareOptions { - /** The new version to release */ - newVersion: string; + /** The new version to release (optional if versioning.policy is configured) */ + newVersion?: string; /** The base revision to release */ rev: string; /** The git remote to use when pushing */ @@ -95,6 +126,10 @@ interface PrepareOptions { noPush: boolean; /** Run publish right after */ publish: boolean; + /** Load config from specified remote branch */ + configFrom?: string; + /** Override CalVer offset (days to go back) */ + calverOffset?: number; } /** @@ -106,17 +141,38 @@ const SLEEP_BEFORE_PUBLISH_SECONDS = 30; /** * Checks the provided version argument for validity * - * We check that the argument is either a valid version string, or a valid - * semantic version part. + * We check that the argument is either a valid version string, 'auto' for + * automatic version detection, 'calver' for calendar versioning, a version + * bump type (major/minor/patch), or a valid semantic version. + * Empty/undefined is also allowed (will use versioning.policy from config). * * @param argv Parsed yargs arguments * @param _opt A list of options and aliases */ export function checkVersionOrPart(argv: Arguments, _opt: any): boolean { const version = argv.newVersion; - if (['major', 'minor', 'patch'].indexOf(version) > -1) { - throw Error('Version part is not supported yet'); - } else if (isValidVersion(version)) { + + // Allow empty version (will use versioning.policy from config) + if (!version) { + return true; + } + + // Allow 'auto' for automatic version detection + if (version === 'auto') { + return true; + } + + // Allow 'calver' for calendar versioning + if (version === 'calver') { + return true; + } + + // Allow version bump types (major, minor, patch) + if (isBumpType(version)) { + return true; + } + + if (isValidVersion(version)) { return true; } else { let errMsg = `Invalid version or version part specified: "${version}"`; @@ -349,6 +405,7 @@ async function execPublish(remote: string, newVersion: string): Promise { * @param newVersion The new version we are releasing * @param changelogPolicy One of the changelog policies, such as "none", "simple", etc. * @param changelogPath Path to the changelog file + * @returns The changelog body for this version, or undefined if no changelog */ async function prepareChangelog( git: SimpleGit, @@ -356,12 +413,12 @@ async function prepareChangelog( newVersion: string, changelogPolicy: ChangelogPolicy = ChangelogPolicy.None, changelogPath: string = DEFAULT_CHANGELOG_PATH -): Promise { +): Promise { if (changelogPolicy === ChangelogPolicy.None) { logger.debug( `Changelog policy is set to "${changelogPolicy}", nothing to do.` ); - return; + return undefined; } if ( @@ -403,7 +460,9 @@ async function prepareChangelog( } if (!changeset.body) { replaceSection = changeset.name; - changeset.body = await generateChangesetFromGit(git, oldVersion); + // generateChangesetFromGit is memoized, so this won't duplicate API calls + const result = await generateChangesetFromGit(git, oldVersion); + changeset.body = result.changelog; } if (changeset.name === DEFAULT_UNRELEASED_TITLE) { replaceSection = changeset.name; @@ -436,6 +495,7 @@ async function prepareChangelog( logger.debug('Changelog entry found:', changeset.name); logger.trace(changeset.body); + return changeset?.body; } /** @@ -460,18 +520,145 @@ async function switchToDefaultBranch( } } +interface ResolveVersionOptions { + /** The raw version input from CLI (may be undefined, 'auto', 'calver', bump type, or semver) */ + versionArg?: string; + /** Override for CalVer offset (days to go back) */ + calverOffset?: number; +} + +/** + * Resolves the final semver version string from various input types. + * + * Handles: + * - No input: uses versioning.policy from config + * - 'calver': calculates calendar version + * - 'auto': analyzes commits to determine bump type + * - 'major'/'minor'/'patch': applies bump to latest tag + * - Explicit semver: returns as-is + * + * @param git Local git client + * @param options Version resolution options + * @returns The resolved semver version string + */ +async function resolveVersion( + git: SimpleGit, + options: ResolveVersionOptions +): Promise { + const config = getConfiguration(); + let version = options.versionArg; + + // If no version specified, use the versioning policy from config + if (!version) { + const policy = getVersioningPolicy(); + logger.debug(`No version specified, using versioning policy: ${policy}`); + + if (policy === VersioningPolicy.Manual) { + throw new ConfigurationError( + 'Version is required. Either specify a version argument or set ' + + 'versioning.policy to "auto" or "calver" in .craft.yml' + ); + } + + // Use the policy as the version type + version = policy; + } + + // Handle CalVer versioning + if (version === 'calver') { + if (!requiresMinVersion(AUTO_VERSION_MIN_VERSION)) { + throw new ConfigurationError( + `CalVer versioning requires minVersion >= ${AUTO_VERSION_MIN_VERSION} in .craft.yml. ` + + 'Please update your configuration or specify the version explicitly.' + ); + } + + // Build CalVer config with overrides + const calverOffset = + options.calverOffset ?? + (process.env.CRAFT_CALVER_OFFSET + ? parseInt(process.env.CRAFT_CALVER_OFFSET, 10) + : undefined) ?? + config.versioning?.calver?.offset ?? + DEFAULT_CALVER_CONFIG.offset; + + const calverFormat = + config.versioning?.calver?.format ?? DEFAULT_CALVER_CONFIG.format; + + return calculateCalVer(git, { + offset: calverOffset, + format: calverFormat, + }); + } + + // Handle automatic version detection or version bump types + if (version === 'auto' || isBumpType(version)) { + if (!requiresMinVersion(AUTO_VERSION_MIN_VERSION)) { + const featureName = isBumpType(version) + ? 'Version bump types' + : 'Auto-versioning'; + throw new ConfigurationError( + `${featureName} requires minVersion >= ${AUTO_VERSION_MIN_VERSION} in .craft.yml. ` + + 'Please update your configuration or specify the version explicitly.' + ); + } + + const latestTag = await getLatestTag(git); + + // Determine bump type - either from arg or from commit analysis + let bumpType: BumpType; + if (version === 'auto') { + const changelogResult = await getChangelogWithBumpType(git, latestTag); + validateBumpType(changelogResult); + bumpType = changelogResult.bumpType; + } else { + bumpType = version as BumpType; + } + + // Calculate new version from latest tag + const currentVersion = + latestTag && latestTag.replace(/^v/, '').match(/^\d/) + ? latestTag.replace(/^v/, '') + : '0.0.0'; + + const newVersion = calculateNextVersion(currentVersion, bumpType); + logger.info( + `Version bump: ${currentVersion} -> ${newVersion} (${bumpType} bump)` + ); + return newVersion; + } + + // Explicit semver version - return as-is + return version; +} + /** * Body of 'prepare' command * * @param argv Command-line arguments */ export async function prepareMain(argv: PrepareOptions): Promise { + const git = await getGitClient(); + + // Handle --config-from: load config from remote branch + if (argv.configFrom) { + logger.info(`Loading configuration from remote branch: ${argv.configFrom}`); + try { + await git.fetch([argv.remote, argv.configFrom]); + const configContent = await git.show([ + `${argv.remote}/${argv.configFrom}:${CONFIG_FILE_NAME}`, + ]); + loadConfigurationFromString(configContent); + } catch (error: any) { + throw new ConfigurationError( + `Failed to load ${CONFIG_FILE_NAME} from branch "${argv.configFrom}": ${error.message}` + ); + } + } + // Get repo configuration const config = getConfiguration(); const githubConfig = await getGlobalGitHubConfig(); - const newVersion = argv.newVersion; - - const git = await getGitClient(); const defaultBranch = await getDefaultBranch(git, argv.remote); logger.debug(`Default branch for the repo:`, defaultBranch); @@ -485,6 +672,15 @@ export async function prepareMain(argv: PrepareOptions): Promise { checkGitStatus(repoStatus, rev); } + // Resolve version from input, policy, or automatic detection + const newVersion = await resolveVersion(git, { + versionArg: argv.newVersion, + calverOffset: argv.calverOffset, + }); + + // Emit resolved version for GitHub Actions + setGitHubActionsOutput('version', newVersion); + logger.info(`Releasing version ${newVersion} from ${rev}`); if (!argv.rev && rev !== defaultBranch) { logger.warn("You're not on your default branch, so I have to ask..."); @@ -521,7 +717,7 @@ export async function prepareMain(argv: PrepareOptions): Promise { ? config.changelog.policy : config.changelogPolicy ) as ChangelogPolicy | undefined; - await prepareChangelog( + const changelogBody = await prepareChangelog( git, oldVersion, newVersion, @@ -546,6 +742,15 @@ export async function prepareMain(argv: PrepareOptions): Promise { // Push the release branch await pushReleaseBranch(git, branchName, argv.remote, !argv.noPush); + // Emit GitHub Actions outputs for downstream steps + const releaseSha = await git.revparse(['HEAD']); + setGitHubActionsOutput('branch', branchName); + setGitHubActionsOutput('sha', releaseSha); + setGitHubActionsOutput('previous_tag', oldVersion || ''); + if (changelogBody) { + setGitHubActionsOutput('changelog', changelogBody); + } + logger.info( `View diff at: https://github.com/${githubConfig.owner}/${githubConfig.repo}/compare/${branchName}` ); diff --git a/src/commands/publish.ts b/src/commands/publish.ts index 27d40f78..b24a01a5 100644 --- a/src/commands/publish.ts +++ b/src/commands/publish.ts @@ -16,6 +16,7 @@ import { getArtifactProviderFromConfig, DEFAULT_RELEASE_BRANCH_NAME, getGlobalGitHubConfig, + expandWorkspaceTargets, } from '../config'; import { formatTable, logger } from '../logger'; import { TargetConfig } from '../schemas/project_config'; @@ -501,7 +502,8 @@ export async function publishMain(argv: PublishOptions): Promise { } } - let targetConfigList = config.targets || []; + // Expand any npm workspace targets into individual package targets + let targetConfigList = await expandWorkspaceTargets(config.targets || []); logger.info(`Looking for publish state file for ${newVersion}...`); const publishStateFile = `.craft-publish-${newVersion}.json`; diff --git a/src/commands/targets.ts b/src/commands/targets.ts index f0aef8fa..57e6574c 100644 --- a/src/commands/targets.ts +++ b/src/commands/targets.ts @@ -1,4 +1,4 @@ -import { getConfiguration } from '../config'; +import { getConfiguration, expandWorkspaceTargets } from '../config'; import { formatJson } from '../utils/strings'; import { getAllTargetNames } from '../targets'; import { BaseTarget } from '../targets/base'; @@ -6,8 +6,12 @@ import { BaseTarget } from '../targets/base'; export const command = ['targets']; export const description = 'List defined targets as JSON array'; -export function handler(): any { - const definedTargets = getConfiguration().targets || []; +export async function handler(): Promise { + let definedTargets = getConfiguration().targets || []; + + // Expand workspace targets (e.g., npm workspaces) + definedTargets = await expandWorkspaceTargets(definedTargets); + const possibleTargetNames = new Set(getAllTargetNames()); const allowedTargetNames = definedTargets .filter(target => target.name && possibleTargetNames.has(target.name)) diff --git a/src/config.ts b/src/config.ts index 05cd34eb..890cc659 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,7 +12,9 @@ import { GitHubGlobalConfig, ArtifactProviderName, StatusProviderName, + TargetConfig, ChangelogPolicy, + VersioningPolicy, } from './schemas/project_config'; import { ConfigurationError } from './utils/errors'; import { @@ -20,6 +22,8 @@ import { parseVersion, versionGreaterOrEqualThan, } from './utils/version'; +// Note: We import getTargetByName lazily in expandWorkspaceTargets to avoid +// circular dependency: config -> targets -> registry -> utils/registry -> symlink -> version -> config import { BaseArtifactProvider } from './artifact_providers/base'; import { GitHubArtifactProvider } from './artifact_providers/github'; import { NoneArtifactProvider } from './artifact_providers/none'; @@ -159,6 +163,21 @@ export function getConfiguration(clearCache = false): CraftProjectConfig { return _configCache; } +/** + * Loads and caches configuration from a YAML string. + * + * This is used by --config-from to load config from a remote branch. + * + * @param configContent The raw YAML configuration content + */ +export function loadConfigurationFromString(configContent: string): CraftProjectConfig { + logger.debug('Loading configuration from provided content...'); + const rawConfig = load(configContent) as Record; + _configCache = validateConfiguration(rawConfig); + checkMinimalConfigVersion(_configCache); + return _configCache; +} + /** * Checks that the current "craft" version is compatible with the configuration * @@ -199,6 +218,66 @@ function checkMinimalConfigVersion(config: CraftProjectConfig): void { } } +/** + * Checks if the project's minVersion configuration meets a required minimum. + * + * This is used to gate features that require a certain version of craft. + * For example, auto-versioning requires minVersion >= 2.14.0. + * + * @param requiredVersion The minimum version required for the feature + * @returns true if the project's minVersion is >= requiredVersion, false otherwise + */ +export function requiresMinVersion(requiredVersion: string): boolean { + const config = getConfiguration(); + const minVersionRaw = config.minVersion; + + if (!minVersionRaw) { + // If no minVersion is configured, the feature is not available + return false; + } + + const configuredMinVersion = parseVersion(minVersionRaw); + const required = parseVersion(requiredVersion); + + if (!configuredMinVersion || !required) { + return false; + } + + return versionGreaterOrEqualThan(configuredMinVersion, required); +} + +/** Minimum craft version required for auto-versioning and CalVer */ +const AUTO_VERSION_MIN_VERSION = '2.14.0'; + +/** + * Returns the effective versioning policy for the project. + * + * The policy determines how versions are resolved when no explicit version + * is provided to `craft prepare`: + * - 'auto': Analyze commits to determine the bump type + * - 'manual': Require an explicit version argument + * - 'calver': Use calendar versioning + * + * If not explicitly configured, defaults to: + * - 'auto' if minVersion >= 2.14.0 + * - 'manual' otherwise (for backward compatibility) + * + * @returns The versioning policy + */ +export function getVersioningPolicy(): VersioningPolicy { + const config = getConfiguration(); + + // Use explicitly configured policy if available + if (config.versioning?.policy) { + return config.versioning.policy; + } + + // Default based on minVersion + return requiresMinVersion(AUTO_VERSION_MIN_VERSION) + ? VersioningPolicy.Auto + : VersioningPolicy.Manual; +} + /** * Return the parsed global GitHub configuration */ @@ -388,3 +467,57 @@ export function getChangelogConfig(): NormalizedChangelogConfig { scopeGrouping, }; } + +/** + * Type for target classes that support expansion + */ +interface ExpandableTargetClass { + expand(config: TargetConfig, rootDir: string): Promise; +} + +/** + * Check if a target class has an expand method + */ +function isExpandableTarget( + targetClass: unknown +): targetClass is ExpandableTargetClass { + return ( + typeof targetClass === 'function' && + 'expand' in targetClass && + typeof targetClass.expand === 'function' + ); +} + +/** + * Expand all expandable targets in the target list + * + * This function takes a list of target configs and expands any targets + * whose target class has an `expand` static method. This allows targets + * to implement their own expansion logic (e.g., npm workspace expansion). + * + * @param targets The original list of target configs + * @returns The expanded list of target configs + */ +export async function expandWorkspaceTargets( + targets: TargetConfig[] +): Promise { + // Lazy import to avoid circular dependency + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { getTargetByName } = require('./targets'); + + const rootDir = getConfigFileDir() || process.cwd(); + const expandedTargets: TargetConfig[] = []; + + for (const target of targets) { + const targetClass = getTargetByName(target.name); + + if (targetClass && isExpandableTarget(targetClass)) { + const expanded = await targetClass.expand(target, rootDir); + expandedTargets.push(...expanded); + } else { + expandedTargets.push(target); + } + } + + return expandedTargets; +} diff --git a/src/index.ts b/src/index.ts index fb6c2e50..acb7efe4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import * as publish from './commands/publish'; import * as targets from './commands/targets'; import * as config from './commands/config'; import * as artifacts from './commands/artifacts'; +import * as changelog from './commands/changelog'; function printVersion(): void { if (!process.argv.includes('-v') && !process.argv.includes('--version')) { @@ -65,7 +66,7 @@ function fixGlobalBooleanFlags(argv: string[]): string[] { /** * Main entrypoint */ -function main(): void { +async function main(): Promise { printVersion(); readEnvironmentConfig(); @@ -74,7 +75,7 @@ function main(): void { const argv = fixGlobalBooleanFlags(process.argv.slice(2)); - yargs + await yargs() .parserConfiguration({ 'boolean-negation': false, }) @@ -84,6 +85,7 @@ function main(): void { .command(targets) .command(config) .command(artifacts) + .command(changelog) .demandCommand() .version(getPackageVersion()) .alias('v', 'version') diff --git a/src/logger.ts b/src/logger.ts index 5d7bb5de..b1026aed 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -7,6 +7,17 @@ import consola, { LogLevel, } from 'consola'; +/** Reporter that writes all output to stderr (so JSON on stdout isn't polluted) */ +class StderrReporter extends BasicReporter { + public log(logObj: ConsolaReporterLogObject) { + const output = this.formatLogObj(logObj); + process.stderr.write(output + '\n'); + } +} + +// Redirect all console output to stderr so it doesn't interfere with JSON output on stdout +consola.setReporters([new StderrReporter()]); + /** * Format a list as a table * diff --git a/src/schemas/projectConfig.schema.ts b/src/schemas/projectConfig.schema.ts index d0bbf9d0..8d433141 100644 --- a/src/schemas/projectConfig.schema.ts +++ b/src/schemas/projectConfig.schema.ts @@ -106,6 +106,43 @@ const projectConfigJsonSchema = { additionalProperties: false, required: ['name'], }, + versioning: { + title: 'VersioningConfig', + description: 'Version resolution configuration', + type: 'object', + properties: { + policy: { + title: 'VersioningPolicy', + description: + 'Default versioning policy when no version argument is provided. ' + + 'auto: analyze commits to determine bump type, ' + + 'manual: require explicit version, ' + + 'calver: use calendar versioning', + type: 'string', + enum: ['auto', 'manual', 'calver'], + tsEnumNames: ['Auto', 'Manual', 'CalVer'], + }, + calver: { + title: 'CalVerConfig', + description: 'Calendar versioning configuration', + type: 'object', + properties: { + offset: { + type: 'number', + description: 'Days to go back for date calculation (default: 14)', + }, + format: { + type: 'string', + description: + 'strftime-like format for date part (default: %y.%-m). ' + + 'Supports: %y (2-digit year), %m (zero-padded month), %-m (month without padding)', + }, + }, + additionalProperties: false, + }, + }, + additionalProperties: false, + }, }, additionalProperties: false, @@ -172,6 +209,32 @@ const projectConfigJsonSchema = { properties: { access: { type: 'string', + description: 'NPM access level (public or restricted)', + }, + checkPackageName: { + type: 'string', + description: + 'Package name to check for latest version on the registry', + }, + workspaces: { + type: 'boolean', + description: + 'Enable workspace discovery to auto-generate npm targets for all workspace packages', + }, + includeWorkspaces: { + type: 'string', + description: + 'Regex pattern to filter which workspace packages to include', + }, + excludeWorkspaces: { + type: 'string', + description: + 'Regex pattern to filter which workspace packages to exclude', + }, + artifactTemplate: { + type: 'string', + description: + 'Template for artifact filenames. Variables: {{name}}, {{simpleName}}, {{version}}', }, }, additionalProperties: false, diff --git a/src/schemas/project_config.ts b/src/schemas/project_config.ts index b22e58a3..8785c7c2 100644 --- a/src/schemas/project_config.ts +++ b/src/schemas/project_config.ts @@ -25,6 +25,7 @@ export interface CraftProjectConfig { requireNames?: string[]; statusProvider?: BaseStatusProvider; artifactProvider?: BaseArtifactProvider; + versioning?: VersioningConfig; } /** * Global (non-target!) GitHub configuration for the project @@ -62,6 +63,26 @@ export interface BaseArtifactProvider { [k: string]: any; }; } +/** + * Version resolution configuration + */ +export interface VersioningConfig { + policy?: VersioningPolicy; + calver?: CalVerConfig; +} +/** + * Calendar versioning configuration + */ +export interface CalVerConfig { + /** + * Days to go back for date calculation (default: 14) + */ + offset?: number; + /** + * strftime-like format for date part (default: %y.%-m). Supports: %y (2-digit year), %m (zero-padded month), %-m (month without padding) + */ + format?: string; +} /** * DEPRECATED: Use changelog.policy instead. Different policies for changelog management @@ -85,3 +106,11 @@ export const enum ArtifactProviderName { GitHub = 'github', None = 'none', } +/** + * Default versioning policy when no version argument is provided. auto: analyze commits to determine bump type, manual: require explicit version, calver: use calendar versioning + */ +export const enum VersioningPolicy { + Auto = 'auto', + Manual = 'manual', + CalVer = 'calver', +} diff --git a/src/targets/__tests__/docker.test.ts b/src/targets/__tests__/docker.test.ts new file mode 100644 index 00000000..543f274a --- /dev/null +++ b/src/targets/__tests__/docker.test.ts @@ -0,0 +1,1217 @@ +import * as fs from 'node:fs'; +import * as os from 'node:os'; + +import { + DockerTarget, + extractRegistry, + registryToEnvPrefix, + normalizeImageRef, + isGoogleCloudRegistry, + hasGcloudCredentials, +} from '../docker'; +import { NoneArtifactProvider } from '../../artifact_providers/none'; +import * as system from '../../utils/system'; + +jest.mock('../../utils/system', () => ({ + ...jest.requireActual('../../utils/system'), + checkExecutableIsPresent: jest.fn(), + spawnProcess: jest.fn().mockResolvedValue(Buffer.from('')), +})); + +jest.mock('node:fs'); +jest.mock('node:os'); + +describe('normalizeImageRef', () => { + it('normalizes string source to object with image property', () => { + const config = { source: 'ghcr.io/org/image' }; + const result = normalizeImageRef(config, 'source'); + expect(result).toEqual({ + image: 'ghcr.io/org/image', + format: undefined, + registry: undefined, + usernameVar: undefined, + passwordVar: undefined, + }); + }); + + it('normalizes string target to object with image property', () => { + const config = { target: 'getsentry/craft' }; + const result = normalizeImageRef(config, 'target'); + expect(result).toEqual({ + image: 'getsentry/craft', + format: undefined, + registry: undefined, + usernameVar: undefined, + passwordVar: undefined, + }); + }); + + it('passes through object form', () => { + const config = { + source: { + image: 'ghcr.io/org/image', + registry: 'ghcr.io', + format: '{{{source}}}:latest', + usernameVar: 'MY_USER', + passwordVar: 'MY_PASS', + }, + }; + const result = normalizeImageRef(config, 'source'); + expect(result).toEqual({ + image: 'ghcr.io/org/image', + registry: 'ghcr.io', + format: '{{{source}}}:latest', + usernameVar: 'MY_USER', + passwordVar: 'MY_PASS', + }); + }); + + it('uses legacy source params as fallback for string form', () => { + const config = { + source: 'ghcr.io/org/image', + sourceFormat: '{{{source}}}:custom', + sourceRegistry: 'custom.registry.io', + sourceUsernameVar: 'LEGACY_USER', + sourcePasswordVar: 'LEGACY_PASS', + }; + const result = normalizeImageRef(config, 'source'); + expect(result).toEqual({ + image: 'ghcr.io/org/image', + format: '{{{source}}}:custom', + registry: 'custom.registry.io', + usernameVar: 'LEGACY_USER', + passwordVar: 'LEGACY_PASS', + }); + }); + + it('uses legacy target params as fallback for string form', () => { + const config = { + target: 'getsentry/craft', + targetFormat: '{{{target}}}:v{{{version}}}', + registry: 'docker.io', + usernameVar: 'LEGACY_USER', + passwordVar: 'LEGACY_PASS', + }; + const result = normalizeImageRef(config, 'target'); + expect(result).toEqual({ + image: 'getsentry/craft', + format: '{{{target}}}:v{{{version}}}', + registry: 'docker.io', + usernameVar: 'LEGACY_USER', + passwordVar: 'LEGACY_PASS', + }); + }); + + it('prefers object properties over legacy params', () => { + const config = { + source: { + image: 'ghcr.io/org/image', + registry: 'new.registry.io', + format: '{{{source}}}:new', + }, + sourceFormat: '{{{source}}}:legacy', + sourceRegistry: 'legacy.registry.io', + sourceUsernameVar: 'LEGACY_USER', + sourcePasswordVar: 'LEGACY_PASS', + }; + const result = normalizeImageRef(config, 'source'); + expect(result).toEqual({ + image: 'ghcr.io/org/image', + registry: 'new.registry.io', + format: '{{{source}}}:new', + usernameVar: 'LEGACY_USER', // Falls back to legacy since not in object + passwordVar: 'LEGACY_PASS', + }); + }); + + it('allows partial object with legacy fallback', () => { + const config = { + source: { image: 'ghcr.io/org/image' }, + sourceFormat: '{{{source}}}:legacy', + sourceRegistry: 'legacy.registry.io', + }; + const result = normalizeImageRef(config, 'source'); + expect(result).toEqual({ + image: 'ghcr.io/org/image', + format: '{{{source}}}:legacy', + registry: 'legacy.registry.io', + usernameVar: undefined, + passwordVar: undefined, + }); + }); + + it('throws ConfigurationError when source is missing', () => { + const config = { target: 'getsentry/craft' }; + expect(() => normalizeImageRef(config, 'source')).toThrow( + "Docker target requires a 'source' property. Please specify the source image." + ); + }); + + it('throws ConfigurationError when target is missing', () => { + const config = { source: 'ghcr.io/org/image' }; + expect(() => normalizeImageRef(config, 'target')).toThrow( + "Docker target requires a 'target' property. Please specify the target image." + ); + }); +}); + +describe('extractRegistry', () => { + it('returns undefined for Docker Hub images (user/image)', () => { + expect(extractRegistry('user/image')).toBeUndefined(); + expect(extractRegistry('getsentry/craft')).toBeUndefined(); + }); + + it('returns undefined for simple image names', () => { + expect(extractRegistry('nginx')).toBeUndefined(); + expect(extractRegistry('ubuntu')).toBeUndefined(); + }); + + it('extracts ghcr.io registry', () => { + expect(extractRegistry('ghcr.io/user/image')).toBe('ghcr.io'); + expect(extractRegistry('ghcr.io/getsentry/craft')).toBe('ghcr.io'); + }); + + it('extracts gcr.io and regional variants', () => { + expect(extractRegistry('gcr.io/project/image')).toBe('gcr.io'); + expect(extractRegistry('us.gcr.io/project/image')).toBe('us.gcr.io'); + expect(extractRegistry('eu.gcr.io/project/image')).toBe('eu.gcr.io'); + expect(extractRegistry('asia.gcr.io/project/image')).toBe('asia.gcr.io'); + }); + + it('extracts other registries with dots', () => { + expect(extractRegistry('registry.example.com/image')).toBe( + 'registry.example.com' + ); + }); + + it('treats docker.io variants as Docker Hub (returns undefined)', () => { + // docker.io is the canonical Docker Hub registry + expect(extractRegistry('docker.io/library/nginx')).toBeUndefined(); + expect(extractRegistry('docker.io/getsentry/craft')).toBeUndefined(); + // index.docker.io is the legacy Docker Hub registry + expect(extractRegistry('index.docker.io/library/nginx')).toBeUndefined(); + // registry-1.docker.io is another Docker Hub alias + expect(extractRegistry('registry-1.docker.io/user/image')).toBeUndefined(); + }); + + it('extracts registries with ports', () => { + expect(extractRegistry('localhost:5000/image')).toBe('localhost:5000'); + expect(extractRegistry('myregistry:8080/user/image')).toBe( + 'myregistry:8080' + ); + }); +}); + +describe('registryToEnvPrefix', () => { + it('converts ghcr.io to GHCR_IO', () => { + expect(registryToEnvPrefix('ghcr.io')).toBe('GHCR_IO'); + }); + + it('converts gcr.io to GCR_IO', () => { + expect(registryToEnvPrefix('gcr.io')).toBe('GCR_IO'); + }); + + it('converts regional GCR to correct prefix', () => { + expect(registryToEnvPrefix('us.gcr.io')).toBe('US_GCR_IO'); + expect(registryToEnvPrefix('eu.gcr.io')).toBe('EU_GCR_IO'); + expect(registryToEnvPrefix('asia.gcr.io')).toBe('ASIA_GCR_IO'); + }); + + it('handles hyphens in registry names', () => { + expect(registryToEnvPrefix('my-registry.example.com')).toBe( + 'MY_REGISTRY_EXAMPLE_COM' + ); + }); + + it('handles ports in registry names', () => { + expect(registryToEnvPrefix('localhost:5000')).toBe('LOCALHOST_5000'); + }); +}); + +describe('isGoogleCloudRegistry', () => { + it('returns true for gcr.io', () => { + expect(isGoogleCloudRegistry('gcr.io')).toBe(true); + }); + + it('returns true for regional GCR variants', () => { + expect(isGoogleCloudRegistry('us.gcr.io')).toBe(true); + expect(isGoogleCloudRegistry('eu.gcr.io')).toBe(true); + expect(isGoogleCloudRegistry('asia.gcr.io')).toBe(true); + }); + + it('returns true for Artifact Registry multi-region (pkg.dev)', () => { + expect(isGoogleCloudRegistry('us-docker.pkg.dev')).toBe(true); + expect(isGoogleCloudRegistry('europe-docker.pkg.dev')).toBe(true); + expect(isGoogleCloudRegistry('asia-docker.pkg.dev')).toBe(true); + }); + + it('returns true for Artifact Registry regional endpoints (pkg.dev)', () => { + expect(isGoogleCloudRegistry('us-west1-docker.pkg.dev')).toBe(true); + expect(isGoogleCloudRegistry('us-central1-docker.pkg.dev')).toBe(true); + expect(isGoogleCloudRegistry('us-east4-docker.pkg.dev')).toBe(true); + expect(isGoogleCloudRegistry('europe-west1-docker.pkg.dev')).toBe(true); + expect(isGoogleCloudRegistry('asia-east1-docker.pkg.dev')).toBe(true); + expect(isGoogleCloudRegistry('australia-southeast1-docker.pkg.dev')).toBe(true); + }); + + it('returns false for non-Google registries', () => { + expect(isGoogleCloudRegistry('ghcr.io')).toBe(false); + expect(isGoogleCloudRegistry('docker.io')).toBe(false); + expect(isGoogleCloudRegistry('custom.registry.io')).toBe(false); + }); + + it('returns false for undefined', () => { + expect(isGoogleCloudRegistry(undefined)).toBe(false); + }); +}); + +describe('hasGcloudCredentials', () => { + const mockFs = fs as jest.Mocked; + const mockOs = os as jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + mockFs.existsSync.mockReturnValue(false); + mockOs.homedir.mockReturnValue('/home/user'); + }); + + afterEach(() => { + delete process.env.GOOGLE_APPLICATION_CREDENTIALS; + delete process.env.GOOGLE_GHA_CREDS_PATH; + delete process.env.CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE; + }); + + it('returns true when GOOGLE_APPLICATION_CREDENTIALS points to existing file', () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = '/path/to/creds.json'; + mockFs.existsSync.mockImplementation( + (p: fs.PathLike) => p === '/path/to/creds.json' + ); + + expect(hasGcloudCredentials()).toBe(true); + }); + + it('returns true when GOOGLE_GHA_CREDS_PATH points to existing file', () => { + process.env.GOOGLE_GHA_CREDS_PATH = '/tmp/gha-creds.json'; + mockFs.existsSync.mockImplementation( + (p: fs.PathLike) => p === '/tmp/gha-creds.json' + ); + + expect(hasGcloudCredentials()).toBe(true); + }); + + it('returns true when CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE points to existing file', () => { + process.env.CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE = '/override/creds.json'; + mockFs.existsSync.mockImplementation( + (p: fs.PathLike) => p === '/override/creds.json' + ); + + expect(hasGcloudCredentials()).toBe(true); + }); + + it('returns true when default ADC file exists', () => { + mockFs.existsSync.mockImplementation( + (p: fs.PathLike) => + p === '/home/user/.config/gcloud/application_default_credentials.json' + ); + + expect(hasGcloudCredentials()).toBe(true); + }); + + it('returns false when no credentials are found', () => { + expect(hasGcloudCredentials()).toBe(false); + }); +}); + +describe('DockerTarget', () => { + const oldEnv = { ...process.env }; + const mockFs = fs as jest.Mocked; + const mockOs = os as jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + mockFs.existsSync.mockReturnValue(false); + mockOs.homedir.mockReturnValue('/home/user'); + // Clear all Docker-related env vars + delete process.env.DOCKER_USERNAME; + delete process.env.DOCKER_PASSWORD; + delete process.env.DOCKER_GHCR_IO_USERNAME; + delete process.env.DOCKER_GHCR_IO_PASSWORD; + delete process.env.DOCKER_GCR_IO_USERNAME; + delete process.env.DOCKER_GCR_IO_PASSWORD; + delete process.env.GITHUB_ACTOR; + delete process.env.GITHUB_TOKEN; + }); + + afterAll(() => { + process.env = { ...oldEnv }; + }); + + describe('target credential resolution', () => { + describe('Mode A: explicit usernameVar/passwordVar', () => { + it('uses explicit env vars when both are specified', () => { + process.env.MY_USER = 'custom-user'; + process.env.MY_PASS = 'custom-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'ghcr.io/org/image', + usernameVar: 'MY_USER', + passwordVar: 'MY_PASS', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials!.username).toBe('custom-user'); + expect(target.dockerConfig.target.credentials!.password).toBe('custom-pass'); + }); + + it('throws if only usernameVar is specified', () => { + process.env.MY_USER = 'custom-user'; + + expect( + () => + new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'ghcr.io/org/image', + usernameVar: 'MY_USER', + }, + new NoneArtifactProvider() + ) + ).toThrow('Both usernameVar and passwordVar must be specified together'); + }); + + it('throws if only passwordVar is specified', () => { + process.env.MY_PASS = 'custom-pass'; + + expect( + () => + new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'ghcr.io/org/image', + passwordVar: 'MY_PASS', + }, + new NoneArtifactProvider() + ) + ).toThrow('Both usernameVar and passwordVar must be specified together'); + }); + + it('throws if explicit env vars are not set (no fallback)', () => { + // Ensure fallback vars are set but should NOT be used + process.env.DOCKER_USERNAME = 'fallback-user'; + process.env.DOCKER_PASSWORD = 'fallback-pass'; + + expect( + () => + new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'ghcr.io/org/image', + usernameVar: 'NONEXISTENT_USER', + passwordVar: 'NONEXISTENT_PASS', + }, + new NoneArtifactProvider() + ) + ).toThrow( + 'Missing credentials: NONEXISTENT_USER and/or NONEXISTENT_PASS environment variable(s) not set' + ); + }); + }); + + describe('Mode B: automatic resolution', () => { + it('uses registry-derived env vars first', () => { + process.env.DOCKER_GHCR_IO_USERNAME = 'ghcr-user'; + process.env.DOCKER_GHCR_IO_PASSWORD = 'ghcr-pass'; + process.env.DOCKER_USERNAME = 'default-user'; + process.env.DOCKER_PASSWORD = 'default-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'ghcr.io/org/image', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials!.username).toBe('ghcr-user'); + expect(target.dockerConfig.target.credentials!.password).toBe('ghcr-pass'); + }); + + it('falls back to GHCR defaults (GITHUB_ACTOR/GITHUB_TOKEN) for ghcr.io', () => { + process.env.GITHUB_ACTOR = 'github-actor'; + process.env.GITHUB_TOKEN = 'github-token'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'ghcr.io/org/image', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials!.username).toBe('github-actor'); + expect(target.dockerConfig.target.credentials!.password).toBe('github-token'); + }); + + it('uses default DOCKER_* env vars for Docker Hub', () => { + process.env.DOCKER_USERNAME = 'dockerhub-user'; + process.env.DOCKER_PASSWORD = 'dockerhub-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'getsentry/craft', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials!.username).toBe('dockerhub-user'); + expect(target.dockerConfig.target.credentials!.password).toBe('dockerhub-pass'); + expect(target.dockerConfig.target.credentials!.registry).toBeUndefined(); + }); + + it('treats docker.io as Docker Hub and uses default credentials', () => { + process.env.DOCKER_USERNAME = 'dockerhub-user'; + process.env.DOCKER_PASSWORD = 'dockerhub-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'docker.io/getsentry/craft', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials!.username).toBe('dockerhub-user'); + expect(target.dockerConfig.target.credentials!.password).toBe('dockerhub-pass'); + expect(target.dockerConfig.target.credentials!.registry).toBeUndefined(); + }); + + it('treats index.docker.io as Docker Hub and uses default credentials', () => { + process.env.DOCKER_USERNAME = 'dockerhub-user'; + process.env.DOCKER_PASSWORD = 'dockerhub-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'index.docker.io/getsentry/craft', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials!.username).toBe('dockerhub-user'); + expect(target.dockerConfig.target.credentials!.password).toBe('dockerhub-pass'); + expect(target.dockerConfig.target.credentials!.registry).toBeUndefined(); + }); + + it('falls back to DOCKER_* when registry-specific vars are not set', () => { + process.env.DOCKER_USERNAME = 'default-user'; + process.env.DOCKER_PASSWORD = 'default-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'gcr.io/project/image', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials!.username).toBe('default-user'); + expect(target.dockerConfig.target.credentials!.password).toBe('default-pass'); + }); + + it('throws when no credentials are available', () => { + expect( + () => + new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'getsentry/craft', + }, + new NoneArtifactProvider() + ) + ).toThrow('Cannot perform Docker release: missing credentials'); + }); + + it('includes registry-specific hint in error message', () => { + // Use a non-Google Cloud registry that will require credentials + expect( + () => + new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'custom.registry.io/project/image', + }, + new NoneArtifactProvider() + ) + ).toThrow('DOCKER_CUSTOM_REGISTRY_IO_USERNAME/PASSWORD'); + }); + }); + + describe('registry config override', () => { + it('uses explicit registry config over auto-detection', () => { + process.env.DOCKER_GCR_IO_USERNAME = 'gcr-user'; + process.env.DOCKER_GCR_IO_PASSWORD = 'gcr-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'us.gcr.io/project/image', + registry: 'gcr.io', // Override to share creds across regions + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials!.registry).toBe('gcr.io'); + expect(target.dockerConfig.target.credentials!.username).toBe('gcr-user'); + expect(target.dockerConfig.target.credentials!.password).toBe('gcr-pass'); + }); + }); + }); + + describe('source credential resolution', () => { + it('resolves source credentials when source registry differs from target', () => { + process.env.DOCKER_GHCR_IO_USERNAME = 'ghcr-user'; + process.env.DOCKER_GHCR_IO_PASSWORD = 'ghcr-pass'; + process.env.DOCKER_USERNAME = 'dockerhub-user'; + process.env.DOCKER_PASSWORD = 'dockerhub-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'getsentry/craft', + }, + new NoneArtifactProvider() + ); + + // Target should use Docker Hub credentials + expect(target.dockerConfig.target.credentials!.username).toBe('dockerhub-user'); + expect(target.dockerConfig.target.credentials!.password).toBe('dockerhub-pass'); + expect(target.dockerConfig.target.credentials!.registry).toBeUndefined(); + + // Source should use GHCR credentials + expect(target.dockerConfig.source.credentials).toBeDefined(); + expect(target.dockerConfig.source.credentials?.username).toBe('ghcr-user'); + expect(target.dockerConfig.source.credentials?.password).toBe('ghcr-pass'); + expect(target.dockerConfig.source.credentials?.registry).toBe('ghcr.io'); + }); + + it('does not set source credentials when source and target registries are the same', () => { + process.env.DOCKER_GHCR_IO_USERNAME = 'ghcr-user'; + process.env.DOCKER_GHCR_IO_PASSWORD = 'ghcr-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/source-image', + target: 'ghcr.io/org/target-image', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.source.credentials).toBeUndefined(); + }); + + it('uses explicit sourceUsernameVar/sourcePasswordVar for source credentials', () => { + process.env.MY_SOURCE_USER = 'source-user'; + process.env.MY_SOURCE_PASS = 'source-pass'; + process.env.DOCKER_USERNAME = 'dockerhub-user'; + process.env.DOCKER_PASSWORD = 'dockerhub-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'getsentry/craft', + sourceUsernameVar: 'MY_SOURCE_USER', + sourcePasswordVar: 'MY_SOURCE_PASS', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.source.credentials?.username).toBe('source-user'); + expect(target.dockerConfig.source.credentials?.password).toBe('source-pass'); + }); + + it('throws if only sourceUsernameVar is specified', () => { + process.env.MY_SOURCE_USER = 'source-user'; + process.env.DOCKER_USERNAME = 'dockerhub-user'; + process.env.DOCKER_PASSWORD = 'dockerhub-pass'; + + expect( + () => + new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'getsentry/craft', + sourceUsernameVar: 'MY_SOURCE_USER', + }, + new NoneArtifactProvider() + ) + ).toThrow('Both usernameVar and passwordVar must be specified together'); + }); + + it('does not require source credentials if source is assumed public', () => { + process.env.DOCKER_USERNAME = 'dockerhub-user'; + process.env.DOCKER_PASSWORD = 'dockerhub-pass'; + // No GHCR credentials set - source assumed to be public + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/public-image', + target: 'getsentry/craft', + }, + new NoneArtifactProvider() + ); + + // Should not throw, source credentials are optional + expect(target.dockerConfig.source.credentials).toBeUndefined(); + }); + + it('uses sourceRegistry config override', () => { + process.env.DOCKER_GCR_IO_USERNAME = 'gcr-user'; + process.env.DOCKER_GCR_IO_PASSWORD = 'gcr-pass'; + process.env.DOCKER_USERNAME = 'dockerhub-user'; + process.env.DOCKER_PASSWORD = 'dockerhub-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'us.gcr.io/project/image', + target: 'getsentry/craft', + sourceRegistry: 'gcr.io', // Use gcr.io creds for us.gcr.io + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.source.credentials?.registry).toBe('gcr.io'); + expect(target.dockerConfig.source.credentials?.username).toBe('gcr-user'); + expect(target.dockerConfig.source.credentials?.password).toBe('gcr-pass'); + }); + }); + + describe('nested object config format', () => { + it('supports target as object with image property', () => { + process.env.DOCKER_GHCR_IO_USERNAME = 'ghcr-user'; + process.env.DOCKER_GHCR_IO_PASSWORD = 'ghcr-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/source-image', + target: { + image: 'ghcr.io/org/target-image', + }, + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.image).toBe('ghcr.io/org/target-image'); + expect(target.dockerConfig.target.credentials!.registry).toBe('ghcr.io'); + }); + + it('supports source as object with image property', () => { + process.env.DOCKER_GHCR_IO_USERNAME = 'ghcr-user'; + process.env.DOCKER_GHCR_IO_PASSWORD = 'ghcr-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: { + image: 'ghcr.io/org/source-image', + }, + target: 'ghcr.io/org/target-image', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.source.image).toBe('ghcr.io/org/source-image'); + }); + + it('supports both source and target as objects', () => { + process.env.DOCKER_GHCR_IO_USERNAME = 'ghcr-user'; + process.env.DOCKER_GHCR_IO_PASSWORD = 'ghcr-pass'; + process.env.DOCKER_USERNAME = 'dockerhub-user'; + process.env.DOCKER_PASSWORD = 'dockerhub-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: { + image: 'ghcr.io/org/source-image', + }, + target: { + image: 'getsentry/craft', + }, + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.source.image).toBe('ghcr.io/org/source-image'); + expect(target.dockerConfig.target.image).toBe('getsentry/craft'); + }); + + it('uses registry from object config', () => { + process.env.DOCKER_GCR_IO_USERNAME = 'gcr-user'; + process.env.DOCKER_GCR_IO_PASSWORD = 'gcr-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/source-image', + target: { + image: 'us.gcr.io/project/image', + registry: 'gcr.io', // Override to share creds across regions + }, + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials!.registry).toBe('gcr.io'); + expect(target.dockerConfig.target.credentials!.username).toBe('gcr-user'); + }); + + it('uses usernameVar/passwordVar from object config', () => { + process.env.MY_TARGET_USER = 'target-user'; + process.env.MY_TARGET_PASS = 'target-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/source-image', + target: { + image: 'getsentry/craft', + usernameVar: 'MY_TARGET_USER', + passwordVar: 'MY_TARGET_PASS', + }, + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials!.username).toBe('target-user'); + expect(target.dockerConfig.target.credentials!.password).toBe('target-pass'); + }); + + it('uses format from object config', () => { + process.env.DOCKER_USERNAME = 'user'; + process.env.DOCKER_PASSWORD = 'pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: { + image: 'ghcr.io/org/source-image', + format: '{{{source}}}:sha-{{{revision}}}', + }, + target: { + image: 'getsentry/craft', + format: '{{{target}}}:v{{{version}}}', + }, + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.source.format).toBe( + '{{{source}}}:sha-{{{revision}}}' + ); + expect(target.dockerConfig.target.format).toBe( + '{{{target}}}:v{{{version}}}' + ); + }); + + it('supports source object with credentials for cross-registry publishing', () => { + process.env.MY_SOURCE_USER = 'source-user'; + process.env.MY_SOURCE_PASS = 'source-pass'; + process.env.DOCKER_USERNAME = 'dockerhub-user'; + process.env.DOCKER_PASSWORD = 'dockerhub-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: { + image: 'ghcr.io/org/private-image', + usernameVar: 'MY_SOURCE_USER', + passwordVar: 'MY_SOURCE_PASS', + }, + target: 'getsentry/craft', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.source.credentials?.username).toBe('source-user'); + expect(target.dockerConfig.source.credentials?.password).toBe('source-pass'); + expect(target.dockerConfig.target.credentials!.username).toBe('dockerhub-user'); + }); + }); + + describe('login', () => { + it('passes registry to docker login command', async () => { + process.env.DOCKER_GHCR_IO_USERNAME = 'user'; + process.env.DOCKER_GHCR_IO_PASSWORD = 'pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'ghcr.io/org/image', + }, + new NoneArtifactProvider() + ); + + await target.login(); + + expect(system.spawnProcess).toHaveBeenCalledWith( + 'docker', + ['login', '--username=user', '--password-stdin', 'ghcr.io'], + {}, + { stdin: 'pass' } + ); + }); + + it('omits registry for Docker Hub', async () => { + process.env.DOCKER_USERNAME = 'user'; + process.env.DOCKER_PASSWORD = 'pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'getsentry/craft', + }, + new NoneArtifactProvider() + ); + + await target.login(); + + expect(system.spawnProcess).toHaveBeenCalledWith( + 'docker', + ['login', '--username=user', '--password-stdin'], + {}, + { stdin: 'pass' } + ); + }); + + it('uses password-stdin for security', async () => { + process.env.DOCKER_USERNAME = 'user'; + process.env.DOCKER_PASSWORD = 'secret-password'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'getsentry/craft', + }, + new NoneArtifactProvider() + ); + + await target.login(); + + // Verify password is passed via stdin, not command line + const callArgs = (system.spawnProcess as jest.Mock).mock.calls[0]; + expect(callArgs[1]).not.toContain('--password=secret-password'); + expect(callArgs[1]).toContain('--password-stdin'); + expect(callArgs[3]).toEqual({ stdin: 'secret-password' }); + }); + + it('logs into both source and target registries for cross-registry publishing', async () => { + process.env.DOCKER_GHCR_IO_USERNAME = 'ghcr-user'; + process.env.DOCKER_GHCR_IO_PASSWORD = 'ghcr-pass'; + process.env.DOCKER_USERNAME = 'dockerhub-user'; + process.env.DOCKER_PASSWORD = 'dockerhub-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'getsentry/craft', + }, + new NoneArtifactProvider() + ); + + await target.login(); + + // Should login to both registries + expect(system.spawnProcess).toHaveBeenCalledTimes(2); + + // First call: login to source (GHCR) + expect(system.spawnProcess).toHaveBeenNthCalledWith( + 1, + 'docker', + ['login', '--username=ghcr-user', '--password-stdin', 'ghcr.io'], + {}, + { stdin: 'ghcr-pass' } + ); + + // Second call: login to target (Docker Hub) + expect(system.spawnProcess).toHaveBeenNthCalledWith( + 2, + 'docker', + ['login', '--username=dockerhub-user', '--password-stdin'], + {}, + { stdin: 'dockerhub-pass' } + ); + }); + + it('only logs into target when source has no credentials (public source)', async () => { + process.env.DOCKER_USERNAME = 'dockerhub-user'; + process.env.DOCKER_PASSWORD = 'dockerhub-pass'; + // No GHCR credentials - source is public + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/public-image', + target: 'getsentry/craft', + }, + new NoneArtifactProvider() + ); + + await target.login(); + + // Should only login to Docker Hub + expect(system.spawnProcess).toHaveBeenCalledTimes(1); + expect(system.spawnProcess).toHaveBeenCalledWith( + 'docker', + ['login', '--username=dockerhub-user', '--password-stdin'], + {}, + { stdin: 'dockerhub-pass' } + ); + }); + + it('only logs in once when source and target are same registry', async () => { + process.env.DOCKER_GHCR_IO_USERNAME = 'ghcr-user'; + process.env.DOCKER_GHCR_IO_PASSWORD = 'ghcr-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/source-image', + target: 'ghcr.io/org/target-image', + }, + new NoneArtifactProvider() + ); + + await target.login(); + + // Should only login once + expect(system.spawnProcess).toHaveBeenCalledTimes(1); + expect(system.spawnProcess).toHaveBeenCalledWith( + 'docker', + ['login', '--username=ghcr-user', '--password-stdin', 'ghcr.io'], + {}, + { stdin: 'ghcr-pass' } + ); + }); + + it('skips login when target.skipLogin is true', async () => { + // No credentials set - would normally throw + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: { + image: 'us.gcr.io/project/image', + skipLogin: true, // Auth handled externally (e.g., gcloud workload identity) + }, + }, + new NoneArtifactProvider() + ); + + await target.login(); + + // Should not attempt any login + expect(system.spawnProcess).not.toHaveBeenCalled(); + }); + + it('skips login when source.skipLogin is true', async () => { + process.env.DOCKER_USERNAME = 'dockerhub-user'; + process.env.DOCKER_PASSWORD = 'dockerhub-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: { + image: 'us.gcr.io/project/image', + skipLogin: true, // Auth handled externally + }, + target: 'getsentry/craft', + }, + new NoneArtifactProvider() + ); + + await target.login(); + + // Should only login to target (Docker Hub) + expect(system.spawnProcess).toHaveBeenCalledTimes(1); + expect(system.spawnProcess).toHaveBeenCalledWith( + 'docker', + ['login', '--username=dockerhub-user', '--password-stdin'], + {}, + { stdin: 'dockerhub-pass' } + ); + }); + + it('skips login for both when both have skipLogin', async () => { + const target = new DockerTarget( + { + name: 'docker', + source: { + image: 'us.gcr.io/project/source', + skipLogin: true, + }, + target: { + image: 'us.gcr.io/project/target', + skipLogin: true, + }, + }, + new NoneArtifactProvider() + ); + + await target.login(); + + // Should not attempt any login + expect(system.spawnProcess).not.toHaveBeenCalled(); + }); + + it('auto-configures gcloud for GCR registries when credentials are available', async () => { + // Set up gcloud credentials + process.env.GOOGLE_APPLICATION_CREDENTIALS = '/path/to/creds.json'; + mockFs.existsSync.mockReturnValue(true); + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'gcr.io/project/image', + }, + new NoneArtifactProvider() + ); + + await target.login(); + + // Should call gcloud auth configure-docker + expect(system.spawnProcess).toHaveBeenCalledWith( + 'gcloud', + ['auth', 'configure-docker', 'gcr.io', '--quiet'], + {}, + {} + ); + }); + + it('auto-configures gcloud for Artifact Registry (pkg.dev)', async () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = '/path/to/creds.json'; + mockFs.existsSync.mockReturnValue(true); + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'us-docker.pkg.dev/project/repo/image', + }, + new NoneArtifactProvider() + ); + + await target.login(); + + // Should call gcloud auth configure-docker with Artifact Registry + expect(system.spawnProcess).toHaveBeenCalledWith( + 'gcloud', + ['auth', 'configure-docker', 'us-docker.pkg.dev', '--quiet'], + {}, + {} + ); + }); + + it('configures multiple GCR registries in one call', async () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = '/path/to/creds.json'; + mockFs.existsSync.mockReturnValue(true); + + const target = new DockerTarget( + { + name: 'docker', + source: 'us.gcr.io/project/source', + target: 'eu.gcr.io/project/target', + }, + new NoneArtifactProvider() + ); + + await target.login(); + + // Should configure both registries in one call + expect(system.spawnProcess).toHaveBeenCalledWith( + 'gcloud', + ['auth', 'configure-docker', 'us.gcr.io,eu.gcr.io', '--quiet'], + {}, + {} + ); + }); + + it('skips gcloud configuration when no credentials are available', async () => { + // No credentials set, fs.existsSync returns false + mockFs.existsSync.mockReturnValue(false); + + // Use Docker Hub as target (requires DOCKER_USERNAME/PASSWORD) + process.env.DOCKER_USERNAME = 'user'; + process.env.DOCKER_PASSWORD = 'pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'gcr.io/project/image', + target: 'getsentry/craft', + }, + new NoneArtifactProvider() + ); + + await target.login(); + + // Should not call gcloud, only docker login + expect(system.spawnProcess).not.toHaveBeenCalledWith( + 'gcloud', + expect.any(Array), + expect.any(Object), + expect.any(Object) + ); + expect(system.spawnProcess).toHaveBeenCalledWith( + 'docker', + expect.arrayContaining(['login']), + {}, + expect.any(Object) + ); + }); + + it('does not require credentials for GCR registries at config time', () => { + // This should not throw even though no credentials are set + // because GCR registries can use gcloud auth + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'gcr.io/project/image', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials).toBeUndefined(); + }); + }); +}); diff --git a/src/targets/__tests__/npm.test.ts b/src/targets/__tests__/npm.test.ts index cd34da08..bafe3402 100644 --- a/src/targets/__tests__/npm.test.ts +++ b/src/targets/__tests__/npm.test.ts @@ -1,5 +1,11 @@ -import { getPublishTag, getLatestVersion } from '../npm'; +import { + getPublishTag, + getLatestVersion, + NpmTarget, + NpmPackageAccess, +} from '../npm'; import * as system from '../../utils/system'; +import * as workspaces from '../../utils/workspaces'; const defaultNpmConfig = { useYarn: false, @@ -171,3 +177,160 @@ describe('getPublishTag', () => { expect(spawnProcessMock).toBeCalledTimes(1); }); }); + +describe('NpmTarget.expand', () => { + let discoverWorkspacesMock: jest.SpyInstance; + + afterEach(() => { + discoverWorkspacesMock?.mockRestore(); + }); + + it('returns config as-is when workspaces is not enabled', async () => { + const config = { name: 'npm', id: '@sentry/browser' }; + const result = await NpmTarget.expand(config, '/root'); + + expect(result).toEqual([config]); + }); + + it('throws error when public package depends on private workspace package', async () => { + discoverWorkspacesMock = jest + .spyOn(workspaces, 'discoverWorkspaces') + .mockResolvedValue({ + type: 'npm', + packages: [ + { + name: '@sentry/browser', + location: '/root/packages/browser', + private: false, + hasPublicAccess: true, + workspaceDependencies: ['@sentry/core', '@sentry-internal/utils'], + }, + { + name: '@sentry/core', + location: '/root/packages/core', + private: false, + hasPublicAccess: true, + workspaceDependencies: [], + }, + { + name: '@sentry-internal/utils', + location: '/root/packages/utils', + private: true, // This is private! + hasPublicAccess: false, + workspaceDependencies: [], + }, + ], + }); + + const config = { name: 'npm', workspaces: true }; + + await expect(NpmTarget.expand(config, '/root')).rejects.toThrow( + /Public package "@sentry\/browser" depends on private workspace package\(s\): @sentry-internal\/utils/ + ); + }); + + it('allows public packages to depend on other public packages', async () => { + discoverWorkspacesMock = jest + .spyOn(workspaces, 'discoverWorkspaces') + .mockResolvedValue({ + type: 'npm', + packages: [ + { + name: '@sentry/browser', + location: '/root/packages/browser', + private: false, + hasPublicAccess: true, + workspaceDependencies: ['@sentry/core'], + }, + { + name: '@sentry/core', + location: '/root/packages/core', + private: false, + hasPublicAccess: true, + workspaceDependencies: [], + }, + ], + }); + + const config = { name: 'npm', workspaces: true }; + const result = await NpmTarget.expand(config, '/root'); + + // Should return targets in dependency order (core before browser) + expect(result).toHaveLength(2); + expect(result[0].id).toBe('@sentry/core'); + expect(result[1].id).toBe('@sentry/browser'); + }); + + it('excludes private packages from expanded targets', async () => { + discoverWorkspacesMock = jest + .spyOn(workspaces, 'discoverWorkspaces') + .mockResolvedValue({ + type: 'npm', + packages: [ + { + name: '@sentry/browser', + location: '/root/packages/browser', + private: false, + hasPublicAccess: true, + workspaceDependencies: [], + }, + { + name: '@sentry-internal/test-utils', + location: '/root/packages/test-utils', + private: true, + hasPublicAccess: false, + workspaceDependencies: [], + }, + ], + }); + + const config = { name: 'npm', workspaces: true }; + const result = await NpmTarget.expand(config, '/root'); + + // Should only include the public package + expect(result).toHaveLength(1); + expect(result[0].id).toBe('@sentry/browser'); + }); + + it('propagates excludeNames and other options to expanded targets', async () => { + discoverWorkspacesMock = jest + .spyOn(workspaces, 'discoverWorkspaces') + .mockResolvedValue({ + type: 'npm', + packages: [ + { + name: '@sentry/browser', + location: '/root/packages/browser', + private: false, + hasPublicAccess: true, + workspaceDependencies: [], + }, + { + name: '@sentry/node', + location: '/root/packages/node', + private: false, + hasPublicAccess: true, + workspaceDependencies: [], + }, + ], + }); + + const config = { + name: 'npm', + workspaces: true, + excludeNames: '/.*-debug\\.tgz$/', + access: NpmPackageAccess.PUBLIC, + checkPackageName: '@sentry/browser', + }; + const result = await NpmTarget.expand(config, '/root'); + + expect(result).toHaveLength(2); + + // Both expanded targets should have the propagated options + for (const target of result) { + expect(target.excludeNames).toBe('/.*-debug\\.tgz$/'); + expect(target.access).toBe(NpmPackageAccess.PUBLIC); + expect(target.checkPackageName).toBe('@sentry/browser'); + } + }); +}); diff --git a/src/targets/docker.ts b/src/targets/docker.ts index 3545fb7b..6d773514 100644 --- a/src/targets/docker.ts +++ b/src/targets/docker.ts @@ -1,3 +1,7 @@ +import { existsSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; + import { TargetConfig } from '../schemas/project_config'; import { BaseArtifactProvider } from '../artifact_providers/base'; import { ConfigurationError } from '../utils/errors'; @@ -12,22 +16,241 @@ const DEFAULT_DOCKER_BIN = 'docker'; */ const DOCKER_BIN = process.env.DOCKER_BIN || DEFAULT_DOCKER_BIN; -/** Options for "docker" target */ -export interface DockerTargetOptions { +/** Docker Hub registry hostnames that should be treated as the default registry */ +const DOCKER_HUB_REGISTRIES = ['docker.io', 'index.docker.io', 'registry-1.docker.io']; + +/** + * Google Cloud registry patterns. + * - gcr.io and regional variants (Container Registry - being deprecated) + * - *.pkg.dev (Artifact Registry - recommended) + */ +const GCR_REGISTRY_PATTERNS = [ + /^gcr\.io$/, + /^[a-z]+-gcr\.io$/, // us-gcr.io, eu-gcr.io, asia-gcr.io, etc. + /^[a-z]+\.gcr\.io$/, // us.gcr.io, eu.gcr.io, asia.gcr.io, etc. + /^[a-z][a-z0-9-]*-docker\.pkg\.dev$/, // us-docker.pkg.dev, us-west1-docker.pkg.dev, europe-west1-docker.pkg.dev, etc. +]; + +/** + * Checks if a registry is a Google Cloud registry (GCR or Artifact Registry). + */ +export function isGoogleCloudRegistry(registry: string | undefined): boolean { + if (!registry) return false; + return GCR_REGISTRY_PATTERNS.some(pattern => pattern.test(registry)); +} + +/** + * Checks if gcloud credentials are available in the environment. + * These are typically set by google-github-actions/auth or `gcloud auth login`. + * + * Detection methods: + * 1. GOOGLE_APPLICATION_CREDENTIALS env var pointing to a valid file + * 2. GOOGLE_GHA_CREDS_PATH env var (set by google-github-actions/auth) + * 3. CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE env var + * 4. Default ADC location: ~/.config/gcloud/application_default_credentials.json + */ +export function hasGcloudCredentials(): boolean { + // Check environment variables that point to credential files + const credPaths = [ + process.env.GOOGLE_APPLICATION_CREDENTIALS, + process.env.GOOGLE_GHA_CREDS_PATH, + process.env.CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE, + ]; + + for (const credPath of credPaths) { + if (credPath && existsSync(credPath)) { + return true; + } + } + + // Check default Application Default Credentials location + const defaultAdcPath = join( + homedir(), + '.config', + 'gcloud', + 'application_default_credentials.json' + ); + if (existsSync(defaultAdcPath)) { + return true; + } + + return false; +} + +/** + * Checks if the gcloud CLI is available. + */ +export async function isGcloudAvailable(): Promise { + try { + await spawnProcess('gcloud', ['--version'], {}, {}); + return true; + } catch { + return false; + } +} + +/** + * Extracts the registry host from a Docker image path. + * + * @param imagePath Docker image path (e.g., "ghcr.io/user/image" or "user/image") + * @returns The registry host if present (e.g., "ghcr.io"), undefined for Docker Hub + */ +export function extractRegistry(imagePath: string): string | undefined { + const parts = imagePath.split('/'); + // Registry hosts contain dots (ghcr.io, gcr.io, us.gcr.io, etc.) + // or colons for ports (localhost:5000) + if (parts.length >= 2 && (parts[0].includes('.') || parts[0].includes(':'))) { + const registry = parts[0]; + // Treat Docker Hub registries as the default (return undefined) + if (DOCKER_HUB_REGISTRIES.includes(registry)) { + return undefined; + } + return registry; + } + return undefined; +} + +/** + * Converts a registry hostname to an environment variable prefix. + * + * @param registry Registry hostname (e.g., "ghcr.io", "us.gcr.io") + * @returns Environment variable prefix (e.g., "GHCR_IO", "US_GCR_IO") + */ +export function registryToEnvPrefix(registry: string): string { + return registry.toUpperCase().replace(/[.\-:]/g, '_'); +} + +/** Credentials for a Docker registry */ +export interface RegistryCredentials { username: string; password: string; - /** Source image path, like `us.gcr.io/sentryio/craft` */ - source: string; - /** Full name template for the source image path, defaults to `{{{source}}}:{{{revision}}}` */ - sourceTemplate: string; - /** Full name template for the target image path, defaults to `{{{target}}}:{{{version}}}` */ - targetTemplate: string; - /** Target image path, like `getsentry/craft` */ - target: string; + registry?: string; +} + +/** + * Image reference configuration (object form). + * Can also be specified as a string shorthand for just the image path. + */ +export interface ImageRefConfig { + /** Docker image path (e.g., "ghcr.io/user/image" or "user/image") */ + image: string; + /** Override the registry for credentials (auto-detected from image if not specified) */ + registry?: string; + /** Format template for the image name */ + format?: string; + /** Env var name for username (must be used with passwordVar) */ + usernameVar?: string; + /** Env var name for password (must be used with usernameVar) */ + passwordVar?: string; + /** + * Skip docker login for this registry. + * Use when auth is configured externally (e.g., gcloud workload identity, service account). + * When true, craft assumes Docker is already authenticated to access this registry. + */ + skipLogin?: boolean; +} + +/** Image reference can be a string (image path) or full config object */ +export type ImageRef = string | ImageRefConfig; + +/** Legacy config keys for source and target */ +interface LegacyConfigKeys { + format: string; + registry: string; + usernameVar: string; + passwordVar: string; + skipLogin: string; } +const LEGACY_KEYS: Record<'source' | 'target', LegacyConfigKeys> = { + source: { + format: 'sourceFormat', + registry: 'sourceRegistry', + usernameVar: 'sourceUsernameVar', + passwordVar: 'sourcePasswordVar', + skipLogin: 'sourceSkipLogin', + }, + target: { + format: 'targetFormat', + registry: 'registry', + usernameVar: 'usernameVar', + passwordVar: 'passwordVar', + skipLogin: 'skipLogin', + }, +}; + /** - * Target responsible for publishing releases on Docker Hub (https://hub.docker.com) + * Normalizes an image reference to object form. + * Handles backwards compatibility with legacy flat config. + * + * @param config The full target config object + * @param type Whether this is 'source' or 'target' image reference + */ +export function normalizeImageRef( + config: Record, + type: 'source' | 'target' +): ImageRefConfig { + const ref = config[type] as ImageRef; + + // Validate that the required field is present + if (ref === undefined || ref === null) { + throw new ConfigurationError( + `Docker target requires a '${type}' property. Please specify the ${type} image.` + ); + } + + const keys = LEGACY_KEYS[type]; + + // Get legacy values from config + const legacyFormat = config[keys.format] as string | undefined; + const legacyRegistry = config[keys.registry] as string | undefined; + const legacyUsernameVar = config[keys.usernameVar] as string | undefined; + const legacyPasswordVar = config[keys.passwordVar] as string | undefined; + const legacySkipLogin = config[keys.skipLogin] as boolean | undefined; + + if (typeof ref === 'string') { + return { + image: ref, + format: legacyFormat, + registry: legacyRegistry, + usernameVar: legacyUsernameVar, + passwordVar: legacyPasswordVar, + skipLogin: legacySkipLogin, + }; + } + + // Object form - prefer object properties over legacy, but allow legacy as fallback + return { + image: ref.image, + format: ref.format ?? legacyFormat, + registry: ref.registry ?? legacyRegistry, + usernameVar: ref.usernameVar ?? legacyUsernameVar, + passwordVar: ref.passwordVar ?? legacyPasswordVar, + skipLogin: ref.skipLogin ?? legacySkipLogin, + }; +} + +/** Resolved image configuration with credentials */ +export interface ResolvedImageConfig extends ImageRefConfig { + /** Resolved format template (with defaults applied) */ + format: string; + /** Resolved credentials for this registry (undefined if public/same as other) */ + credentials?: RegistryCredentials; +} + +/** Options for "docker" target */ +export interface DockerTargetOptions { + /** Source image configuration with resolved credentials */ + source: ResolvedImageConfig; + /** Target image configuration with resolved credentials (or skipLogin for external auth) */ + target: ResolvedImageConfig; +} + +/** + * Target responsible for publishing releases to Docker registries. + * + * Supports multiple registries including Docker Hub, GitHub Container Registry (ghcr.io), + * Google Container Registry (gcr.io), and other OCI-compliant registries. */ export class DockerTarget extends BaseTarget { /** Target name */ @@ -45,41 +268,303 @@ export class DockerTarget extends BaseTarget { } /** - * Extracts Docker target options from the environment + * Resolves credentials for a registry. + * + * Credential resolution follows two modes: + * + * Mode A (explicit env vars): If usernameVar and passwordVar are provided, + * only those env vars are used. Throws if either is missing. + * + * Mode B (automatic resolution): Tries in order: + * 1. Registry-derived env vars: DOCKER__USERNAME / DOCKER__PASSWORD + * 2. Built-in defaults for known registries (GHCR: GITHUB_ACTOR / GITHUB_TOKEN) + * 3. Default: DOCKER_USERNAME / DOCKER_PASSWORD (only if useDefaultFallback is true) + * + * @param registry The registry host (e.g., "ghcr.io"), undefined for Docker Hub + * @param usernameVar Optional explicit env var name for username + * @param passwordVar Optional explicit env var name for password + * @param required Whether credentials are required (throws if missing) + * @param useDefaultFallback Whether to fall back to DOCKER_USERNAME/PASSWORD defaults + * @returns Credentials if found, undefined if not required and not found */ - public getDockerConfig(): DockerTargetOptions { - if (!process.env.DOCKER_USERNAME || !process.env.DOCKER_PASSWORD) { + private resolveCredentials( + registry: string | undefined, + usernameVar?: string, + passwordVar?: string, + required = true, + useDefaultFallback = true + ): RegistryCredentials | undefined { + let username: string | undefined; + let password: string | undefined; + + // Mode A: Explicit env var override - no fallback for security + if (usernameVar || passwordVar) { + if (!usernameVar || !passwordVar) { + throw new ConfigurationError( + 'Both usernameVar and passwordVar must be specified together' + ); + } + username = process.env[usernameVar]; + password = process.env[passwordVar]; + + if (!username || !password) { + if (required) { + throw new ConfigurationError( + `Missing credentials: ${usernameVar} and/or ${passwordVar} environment variable(s) not set` + ); + } + return undefined; + } + } else { + // Mode B: Automatic resolution with fallback chain + + // 1. Registry-derived env vars + if (registry) { + const prefix = `DOCKER_${registryToEnvPrefix(registry)}_`; + username = process.env[`${prefix}USERNAME`]; + password = process.env[`${prefix}PASSWORD`]; + } + + // 2. Built-in defaults for known registries + if (!username || !password) { + if (registry === 'ghcr.io') { + // GHCR defaults: use GitHub Actions built-in env vars + // GITHUB_ACTOR and GITHUB_TOKEN are available by default in GitHub Actions + // See: https://docs.github.com/en/actions/reference/workflows-and-actions/variables + username = username ?? process.env.GITHUB_ACTOR; + password = password ?? process.env.GITHUB_TOKEN; + } + } + + // 3. Fallback to defaults (only for target registry, not for source) + if (useDefaultFallback) { + username = username ?? process.env.DOCKER_USERNAME; + password = password ?? process.env.DOCKER_PASSWORD; + } + } + + if (!username || !password) { + if (required) { + const registryHint = registry + ? `DOCKER_${registryToEnvPrefix(registry)}_USERNAME/PASSWORD or ` + : ''; throw new ConfigurationError( `Cannot perform Docker release: missing credentials. - Please use DOCKER_USERNAME and DOCKER_PASSWORD environment variables.`.replace( +Please use ${registryHint}DOCKER_USERNAME and DOCKER_PASSWORD environment variables.`.replace( /^\s+/gm, '' ) + ); + } + return undefined; + } + + return { username, password, registry }; + } + + /** + * Extracts Docker target options from the environment. + * + * Supports both new nested config format and legacy flat format: + * + * New format: + * source: { image: "ghcr.io/org/image", registry: "ghcr.io", usernameVar: "X" } + * target: "getsentry/craft" # string shorthand + * + * Legacy format: + * source: "ghcr.io/org/image" + * sourceRegistry: "ghcr.io" + * sourceUsernameVar: "X" + */ + public getDockerConfig(): DockerTargetOptions { + // Normalize source and target configs (handles string vs object, legacy vs new) + const source = normalizeImageRef(this.config, 'source'); + const target = normalizeImageRef(this.config, 'target'); + + // Resolve registries (explicit config > auto-detected from image) + const targetRegistry = target.registry ?? extractRegistry(target.image); + const sourceRegistry = source.registry ?? extractRegistry(source.image); + + // Resolve target credentials + // - Skip if skipLogin is set (auth configured externally) + // - For Google Cloud registries, credentials are optional (can use gcloud auth) + // - For other registries, credentials are required + let targetCredentials: RegistryCredentials | undefined; + if (!target.skipLogin) { + const isGcrTarget = isGoogleCloudRegistry(targetRegistry); + targetCredentials = this.resolveCredentials( + targetRegistry, + target.usernameVar, + target.passwordVar, + // Required unless it's a GCR registry (which can use gcloud auth) + !isGcrTarget + ); + } + + // Resolve source credentials if source registry differs from target + // Source credentials are optional - if not found, we assume the source is public + // We don't fall back to default DOCKER_* credentials for source (those are for target) + let sourceCredentials: RegistryCredentials | undefined; + if (!source.skipLogin && sourceRegistry !== targetRegistry) { + sourceCredentials = this.resolveCredentials( + sourceRegistry, + source.usernameVar, + source.passwordVar, + // Only required if explicit source env vars are specified + !!(source.usernameVar || source.passwordVar), + // Don't fall back to DOCKER_USERNAME/PASSWORD for source + false ); } return { - password: process.env.DOCKER_PASSWORD, - source: this.config.source, - target: this.config.target, - sourceTemplate: this.config.sourceFormat || '{{{source}}}:{{{revision}}}', - targetTemplate: this.config.targetFormat || '{{{target}}}:{{{version}}}', - username: process.env.DOCKER_USERNAME, + source: { + ...source, + format: source.format || '{{{source}}}:{{{revision}}}', + credentials: sourceCredentials, + }, + target: { + ...target, + format: target.format || '{{{target}}}:{{{version}}}', + credentials: targetCredentials, + }, }; } /** - * Logs into docker client with the provided username and password in config + * Logs into a Docker registry with the provided credentials. * * NOTE: This may change the globally logged in Docker user on the system + * + * @param credentials The registry credentials to use */ - public async login(): Promise { - const { username, password } = this.dockerConfig; - return spawnProcess(DOCKER_BIN, [ - 'login', - `--username=${username}`, - `--password=${password}`, - ]); + private async loginToRegistry(credentials: RegistryCredentials): Promise { + const { username, password, registry } = credentials; + const args = ['login', `--username=${username}`, '--password-stdin']; + if (registry) { + args.push(registry); + } + const registryName = registry || 'Docker Hub'; + this.logger.debug(`Logging into ${registryName}...`); + // Pass password via stdin for security (avoids exposure in ps/process list) + await spawnProcess(DOCKER_BIN, args, {}, { stdin: password }); + } + + /** + * Configures Docker to use gcloud for authentication to Google Cloud registries. + * This runs `gcloud auth configure-docker` which sets up the credential helper. + * + * @param registries List of Google Cloud registries to configure + * @returns true if configuration was successful, false otherwise + */ + private async configureGcloudDocker(registries: string[]): Promise { + if (registries.length === 0) { + return false; + } + + // Check if gcloud credentials are available + if (!hasGcloudCredentials()) { + this.logger.debug('No gcloud credentials detected, skipping gcloud auth configure-docker'); + return false; + } + + // Check if gcloud is available + if (!(await isGcloudAvailable())) { + this.logger.debug('gcloud CLI not available, skipping gcloud auth configure-docker'); + return false; + } + + const registryList = registries.join(','); + this.logger.debug(`Configuring Docker for Google Cloud registries: ${registryList}`); + + try { + // Run gcloud auth configure-docker with the registries + // This configures Docker's credential helper to use gcloud for these registries + await spawnProcess('gcloud', ['auth', 'configure-docker', registryList, '--quiet'], {}, {}); + this.logger.info(`Configured Docker authentication for: ${registryList}`); + return true; + } catch (error) { + this.logger.warn(`Failed to configure gcloud Docker auth: ${error}`); + return false; + } + } + + /** + * Logs into all required Docker registries (source and target). + * + * For Google Cloud registries (gcr.io, *.pkg.dev), automatically uses + * `gcloud auth configure-docker` if gcloud credentials are available. + * + * If the source registry differs from target and has credentials configured, + * logs into both. Otherwise, only logs into the target registry. + */ + public async login(): Promise { + const { source, target } = this.dockerConfig; + + // Resolve registries from the config + const sourceRegistry = source.registry ?? extractRegistry(source.image); + const targetRegistry = target.registry ?? extractRegistry(target.image); + + // Collect Google Cloud registries that need authentication + const gcrRegistries: string[] = []; + const gcrConfiguredRegistries = new Set(); + + // Check if source registry is a Google Cloud registry and needs auth + if ( + !source.skipLogin && + !source.credentials && + sourceRegistry && + isGoogleCloudRegistry(sourceRegistry) + ) { + gcrRegistries.push(sourceRegistry); + } + + // Check if target registry is a Google Cloud registry and needs auth + if ( + !target.skipLogin && + !target.credentials && + targetRegistry && + isGoogleCloudRegistry(targetRegistry) + ) { + // Avoid duplicates + if (!gcrRegistries.includes(targetRegistry)) { + gcrRegistries.push(targetRegistry); + } + } + + // Try to configure gcloud for Google Cloud registries + if (gcrRegistries.length > 0) { + const configured = await this.configureGcloudDocker(gcrRegistries); + if (configured) { + gcrRegistries.forEach(r => gcrConfiguredRegistries.add(r)); + } + } + + // Login to source registry (if needed and not already configured via gcloud) + if (source.credentials) { + await this.loginToRegistry(source.credentials); + } else if ( + sourceRegistry && + !source.skipLogin && + !gcrConfiguredRegistries.has(sourceRegistry) + ) { + // Source registry needs auth but we couldn't configure it + // This is okay - source might be public or already authenticated + this.logger.debug(`No credentials for source registry ${sourceRegistry}, assuming public`); + } + + // Login to target registry (if needed and not already configured via gcloud) + if (target.credentials) { + await this.loginToRegistry(target.credentials); + } else if (!target.skipLogin && !gcrConfiguredRegistries.has(targetRegistry || '')) { + // Target registry needs auth but we have no credentials and couldn't configure gcloud + // This will likely fail when pushing, but we let it proceed + if (targetRegistry) { + this.logger.warn( + `No credentials for target registry ${targetRegistry}. Push may fail.` + ); + } + } } /** @@ -91,12 +576,16 @@ export class DockerTarget extends BaseTarget { * @param version The release version for the target image */ async copy(sourceRevision: string, version: string): Promise { - const sourceImage = renderTemplateSafe(this.dockerConfig.sourceTemplate, { - ...this.dockerConfig, + const { source, target } = this.dockerConfig; + + const sourceImage = renderTemplateSafe(source.format, { + source: source.image, + target: target.image, revision: sourceRevision, }); - const targetImage = renderTemplateSafe(this.dockerConfig.targetTemplate, { - ...this.dockerConfig, + const targetImage = renderTemplateSafe(target.format, { + source: source.image, + target: target.image, version, }); @@ -110,12 +599,12 @@ export class DockerTarget extends BaseTarget { } /** - * Pushes a source image to Docker Hub + * Publishes a source image to the target registry * * @param version The new version * @param revision The SHA revision of the new version */ - public async publish(version: string, revision: string): Promise { + public async publish(version: string, revision: string): Promise { await this.login(); await this.copy(revision, version); diff --git a/src/targets/github.ts b/src/targets/github.ts index 563f118f..d4103bc5 100644 --- a/src/targets/github.ts +++ b/src/targets/github.ts @@ -18,6 +18,7 @@ import { isDryRun } from '../utils/helpers'; import { isPreviewRelease, parseVersion, + SemVer, versionGreaterOrEqualThan, versionToTag, } from '../utils/version'; @@ -42,6 +43,12 @@ export interface GitHubTargetConfig extends GitHubGlobalConfig { previewReleases: boolean; /** Do not create a full GitHub release, only push a git tag */ tagOnly: boolean; + /** + * Floating tags to create/update when publishing a release. + * Supports placeholders: {major}, {minor}, {patch} + * Example: "v{major}" creates a "v2" tag for version "2.15.0" + */ + floatingTags: string[]; } /** @@ -96,6 +103,7 @@ export class GitHubTarget extends BaseTarget { !!this.config.previewReleases, tagPrefix: this.config.tagPrefix || '', tagOnly: !!this.config.tagOnly, + floatingTags: this.config.floatingTags || [], }; this.github = getGitHubClient(); } @@ -376,6 +384,86 @@ export class GitHubTarget extends BaseTarget { } } + /** + * Resolves a floating tag pattern by replacing placeholders with version components. + * + * @param pattern The pattern string (e.g., "v{major}") + * @param parsedVersion The parsed semantic version + * @returns The resolved tag name (e.g., "v2") + */ + protected resolveFloatingTag(pattern: string, parsedVersion: SemVer): string { + return pattern + .replace('{major}', String(parsedVersion.major)) + .replace('{minor}', String(parsedVersion.minor)) + .replace('{patch}', String(parsedVersion.patch)); + } + + /** + * Creates or updates floating tags for the release. + * + * Floating tags (like "v2") point to the latest release in a major version line. + * They are force-updated if they already exist. + * + * @param version The version being released + * @param revision Git commit SHA to point the tags to + */ + protected async updateFloatingTags( + version: string, + revision: string + ): Promise { + const floatingTags = this.githubConfig.floatingTags; + if (!floatingTags || floatingTags.length === 0) { + return; + } + + const parsedVersion = parseVersion(version); + if (!parsedVersion) { + this.logger.warn( + `Cannot parse version "${version}" for floating tags, skipping` + ); + return; + } + + for (const pattern of floatingTags) { + const tag = this.resolveFloatingTag(pattern, parsedVersion); + const tagRef = `refs/tags/${tag}`; + + if (isDryRun()) { + this.logger.info( + `[dry-run] Not updating floating tag: "${tag}" (from pattern "${pattern}")` + ); + continue; + } + + this.logger.info(`Updating floating tag: "${tag}"...`); + + try { + // Try to update existing tag + await this.github.rest.git.updateRef({ + owner: this.githubConfig.owner, + repo: this.githubConfig.repo, + ref: `tags/${tag}`, + sha: revision, + force: true, + }); + this.logger.debug(`Updated existing floating tag: "${tag}"`); + } catch (error) { + // Tag doesn't exist, create it + if (error.status === 422) { + await this.github.rest.git.createRef({ + owner: this.githubConfig.owner, + repo: this.githubConfig.repo, + ref: tagRef, + sha: revision, + }); + this.logger.debug(`Created new floating tag: "${tag}"`); + } else { + throw error; + } + } + } + } + /** * Creates a new GitHub release and publish all available artifacts. * @@ -389,7 +477,9 @@ export class GitHubTarget extends BaseTarget { this.logger.info( `Not creating a GitHub release because "tagOnly" flag was set.` ); - return this.createGitTag(version, revision); + await this.createGitTag(version, revision); + await this.updateFloatingTags(version, revision); + return; } const config = getConfiguration(); @@ -449,6 +539,9 @@ export class GitHubTarget extends BaseTarget { ); await this.publishRelease(draftRelease, { makeLatest }); + + // Update floating tags (e.g., v2 for version 2.15.0) + await this.updateFloatingTags(version, revision); } } diff --git a/src/targets/npm.ts b/src/targets/npm.ts index fc9d7288..7db3d956 100644 --- a/src/targets/npm.ts +++ b/src/targets/npm.ts @@ -3,6 +3,7 @@ import prompts from 'prompts'; import { TargetConfig } from '../schemas/project_config'; import { ConfigurationError, reportError } from '../utils/errors'; +import { stringToRegexp } from '../utils/filters'; import { isDryRun } from '../utils/helpers'; import { hasExecutable, spawnProcess } from '../utils/system'; import { @@ -10,6 +11,13 @@ import { parseVersion, versionGreaterOrEqualThan, } from '../utils/version'; +import { + discoverWorkspaces, + filterWorkspacePackages, + packageNameToArtifactPattern, + packageNameToArtifactFromTemplate, + topologicalSortPackages, +} from '../utils/workspaces'; import { BaseTarget } from './base'; import { BaseArtifactProvider, @@ -17,6 +25,7 @@ import { } from '../artifact_providers/base'; import { withTempFile } from '../utils/files'; import { writeFileSync } from 'fs'; +import { logger } from '../logger'; /** Command to launch "npm" */ export const NPM_BIN = process.env.NPM_BIN || 'npm'; @@ -44,6 +53,29 @@ export interface NpmTargetConfig extends TargetConfig { access?: NpmPackageAccess; /** If defined, lookup this package name on the registry to get the current latest version. */ checkPackageName?: string; + /** + * Enable workspace discovery to auto-generate npm targets for all workspace packages. + * When enabled, this target will be expanded into multiple targets, one per workspace package. + */ + workspaces?: boolean; + /** + * Regex pattern to filter which workspace packages to include. + * Only packages matching this pattern will be published. + * Example: '/^@sentry\\//' + */ + includeWorkspaces?: string; + /** + * Regex pattern to filter which workspace packages to exclude. + * Packages matching this pattern will not be published. + * Example: '/^@sentry-internal\\//' + */ + excludeWorkspaces?: string; + /** + * Template for generating artifact filenames from package names. + * Variables: {{name}} (full package name), {{simpleName}} (without @scope/), {{version}} + * Default convention: @sentry/browser -> sentry-browser-{version}.tgz + */ + artifactTemplate?: string; } /** NPM target configuration options */ @@ -77,6 +109,139 @@ export class NpmTarget extends BaseTarget { /** Target options */ public readonly npmConfig: NpmTargetOptions; + /** + * Expand an npm target config into multiple targets if workspaces is enabled. + * This static method is called during config loading to expand workspace targets. + * + * @param config The npm target config + * @param rootDir The root directory of the project + * @returns Array of expanded target configs, or the original config in an array + */ + public static async expand( + config: NpmTargetConfig, + rootDir: string + ): Promise { + // If workspaces is not enabled, return the config as-is + if (!config.workspaces) { + return [config]; + } + + const result = await discoverWorkspaces(rootDir); + + if (result.type === 'none' || result.packages.length === 0) { + logger.warn( + 'npm target has workspaces enabled but no workspace packages were found' + ); + return []; + } + // Filter packages based on include/exclude patterns + let includePattern: RegExp | undefined; + let excludePattern: RegExp | undefined; + + if (config.includeWorkspaces) { + includePattern = stringToRegexp(config.includeWorkspaces); + } + if (config.excludeWorkspaces) { + excludePattern = stringToRegexp(config.excludeWorkspaces); + } + + const filteredPackages = filterWorkspacePackages( + result.packages, + includePattern, + excludePattern + ); + + // Also filter out private packages by default (they shouldn't be published) + const publishablePackages = filteredPackages.filter(pkg => !pkg.private); + const privatePackageNames = new Set( + filteredPackages.filter(pkg => pkg.private).map(pkg => pkg.name) + ); + + // Validate: public packages should not depend on private workspace packages + for (const pkg of publishablePackages) { + const privateDeps = pkg.workspaceDependencies.filter(dep => + privatePackageNames.has(dep) + ); + if (privateDeps.length > 0) { + throw new ConfigurationError( + `Public package "${ + pkg.name + }" depends on private workspace package(s): ${privateDeps.join( + ', ' + )}. ` + + `Private packages cannot be published to npm, so this dependency cannot be resolved by consumers.` + ); + } + + // Warn about scoped packages without publishConfig.access: 'public' + const isScoped = pkg.name.startsWith('@'); + if (isScoped && !pkg.hasPublicAccess) { + logger.warn( + `Scoped package "${pkg.name}" does not have publishConfig.access set to 'public'. ` + + `This may cause npm publish to fail for public packages.` + ); + } + } + + if (publishablePackages.length === 0) { + logger.warn('No publishable workspace packages found after filtering'); + return []; + } + + logger.info( + `Discovered ${publishablePackages.length} publishable ${result.type} workspace packages` + ); + + + + // Sort packages by dependency order (dependencies first, then dependents) + const sortedPackages = topologicalSortPackages(publishablePackages); + + logger.debug( + `Expanding npm workspace target to ${ + sortedPackages.length + } packages (dependency order): ${sortedPackages + .map(p => p.name) + .join(', ')}` + ); + + // Generate a target config for each package + return sortedPackages.map(pkg => { + // Generate the artifact pattern + let includeNames: string; + if (config.artifactTemplate) { + includeNames = packageNameToArtifactFromTemplate( + pkg.name, + config.artifactTemplate + ); + } else { + includeNames = packageNameToArtifactPattern(pkg.name); + } + + // Create the expanded target config + const expandedTarget: TargetConfig = { + name: 'npm', + id: pkg.name, + includeNames, + }; + + // Copy over common target options + if (config.excludeNames) { + expandedTarget.excludeNames = config.excludeNames; + } + + // Copy over npm-specific target options + if (config.access) { + expandedTarget.access = config.access; + } + if (config.checkPackageName) { + expandedTarget.checkPackageName = config.checkPackageName; + } + + return expandedTarget; + }); + } + public constructor( config: NpmTargetConfig, artifactProvider: BaseArtifactProvider diff --git a/src/utils/__fixtures__/workspaces/no-workspace/package.json b/src/utils/__fixtures__/workspaces/no-workspace/package.json new file mode 100644 index 00000000..e46b4225 --- /dev/null +++ b/src/utils/__fixtures__/workspaces/no-workspace/package.json @@ -0,0 +1,4 @@ +{ + "name": "single-package", + "version": "1.0.0" +} diff --git a/src/utils/__fixtures__/workspaces/npm-workspace/package.json b/src/utils/__fixtures__/workspaces/npm-workspace/package.json new file mode 100644 index 00000000..4114dcf9 --- /dev/null +++ b/src/utils/__fixtures__/workspaces/npm-workspace/package.json @@ -0,0 +1,5 @@ +{ + "name": "npm-workspace-root", + "private": true, + "workspaces": ["packages/*"] +} diff --git a/src/utils/__fixtures__/workspaces/npm-workspace/packages/pkg-a/package.json b/src/utils/__fixtures__/workspaces/npm-workspace/packages/pkg-a/package.json new file mode 100644 index 00000000..ee465ff7 --- /dev/null +++ b/src/utils/__fixtures__/workspaces/npm-workspace/packages/pkg-a/package.json @@ -0,0 +1,4 @@ +{ + "name": "@test/pkg-a", + "version": "1.0.0" +} diff --git a/src/utils/__fixtures__/workspaces/npm-workspace/packages/pkg-b/package.json b/src/utils/__fixtures__/workspaces/npm-workspace/packages/pkg-b/package.json new file mode 100644 index 00000000..5b6dfa4d --- /dev/null +++ b/src/utils/__fixtures__/workspaces/npm-workspace/packages/pkg-b/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/pkg-b", + "version": "1.0.0", + "private": true, + "dependencies": { + "@test/pkg-a": "^1.0.0" + } +} diff --git a/src/utils/__fixtures__/workspaces/pnpm-workspace/package.json b/src/utils/__fixtures__/workspaces/pnpm-workspace/package.json new file mode 100644 index 00000000..5e5ba2b2 --- /dev/null +++ b/src/utils/__fixtures__/workspaces/pnpm-workspace/package.json @@ -0,0 +1,4 @@ +{ + "name": "pnpm-workspace-root", + "private": true +} diff --git a/src/utils/__fixtures__/workspaces/pnpm-workspace/packages/pkg-a/package.json b/src/utils/__fixtures__/workspaces/pnpm-workspace/packages/pkg-a/package.json new file mode 100644 index 00000000..db7c4e70 --- /dev/null +++ b/src/utils/__fixtures__/workspaces/pnpm-workspace/packages/pkg-a/package.json @@ -0,0 +1,4 @@ +{ + "name": "@pnpm/pkg-a", + "version": "1.0.0" +} diff --git a/src/utils/__fixtures__/workspaces/pnpm-workspace/packages/pkg-b/package.json b/src/utils/__fixtures__/workspaces/pnpm-workspace/packages/pkg-b/package.json new file mode 100644 index 00000000..1983967a --- /dev/null +++ b/src/utils/__fixtures__/workspaces/pnpm-workspace/packages/pkg-b/package.json @@ -0,0 +1,4 @@ +{ + "name": "@pnpm/pkg-b", + "version": "1.0.0" +} diff --git a/src/utils/__fixtures__/workspaces/pnpm-workspace/pnpm-workspace.yaml b/src/utils/__fixtures__/workspaces/pnpm-workspace/pnpm-workspace.yaml new file mode 100644 index 00000000..924b55f4 --- /dev/null +++ b/src/utils/__fixtures__/workspaces/pnpm-workspace/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - packages/* diff --git a/src/utils/__tests__/autoVersion.test.ts b/src/utils/__tests__/autoVersion.test.ts new file mode 100644 index 00000000..20fdbe19 --- /dev/null +++ b/src/utils/__tests__/autoVersion.test.ts @@ -0,0 +1,250 @@ +/* eslint-env jest */ + +jest.mock('../githubApi.ts'); +jest.mock('../git'); +jest.mock('fs', () => ({ + ...jest.requireActual('fs'), + readFileSync: jest.fn(), +})); +jest.mock('../../config', () => ({ + ...jest.requireActual('../../config'), + getConfigFileDir: jest.fn(), + getGlobalGitHubConfig: jest.fn(), +})); + +import { readFileSync } from 'fs'; +import type { SimpleGit } from 'simple-git'; + +import * as config from '../../config'; +import { getChangesSince } from '../git'; +import { getGitHubClient } from '../githubApi'; +import { + calculateNextVersion, + getChangelogWithBumpType, + validateBumpType, +} from '../autoVersion'; +import { clearChangesetCache } from '../changelog'; + +const getConfigFileDirMock = config.getConfigFileDir as jest.MockedFunction< + typeof config.getConfigFileDir +>; +const getGlobalGitHubConfigMock = + config.getGlobalGitHubConfig as jest.MockedFunction< + typeof config.getGlobalGitHubConfig + >; +const readFileSyncMock = readFileSync as jest.MockedFunction< + typeof readFileSync +>; +const getChangesSinceMock = getChangesSince as jest.MockedFunction< + typeof getChangesSince +>; + +describe('calculateNextVersion', () => { + test('increments major version', () => { + expect(calculateNextVersion('1.2.3', 'major')).toBe('2.0.0'); + }); + + test('increments minor version', () => { + expect(calculateNextVersion('1.2.3', 'minor')).toBe('1.3.0'); + }); + + test('increments patch version', () => { + expect(calculateNextVersion('1.2.3', 'patch')).toBe('1.2.4'); + }); + + test('handles empty version as 0.0.0', () => { + expect(calculateNextVersion('', 'patch')).toBe('0.0.1'); + expect(calculateNextVersion('', 'minor')).toBe('0.1.0'); + expect(calculateNextVersion('', 'major')).toBe('1.0.0'); + }); + + test('handles prerelease versions', () => { + // Semver patch on prerelease "releases" it (removes prerelease suffix) + expect(calculateNextVersion('1.2.3-beta.1', 'patch')).toBe('1.2.3'); + // Minor bump on prerelease increments minor and removes prerelease + expect(calculateNextVersion('1.2.3-rc.0', 'minor')).toBe('1.3.0'); + }); +}); + +describe('validateBumpType', () => { + test('throws error when no commits found', () => { + const result = { + changelog: '', + bumpType: null, + totalCommits: 0, + matchedCommitsWithSemver: 0, + }; + + expect(() => validateBumpType(result)).toThrow( + 'Cannot determine version automatically: no commits found since the last release.' + ); + }); + + test('throws error when no commits match semver categories', () => { + const result = { + changelog: '', + bumpType: null, + totalCommits: 5, + matchedCommitsWithSemver: 0, + }; + + expect(() => validateBumpType(result)).toThrow( + 'Cannot determine version automatically' + ); + }); + + test('does not throw when bumpType is present', () => { + const result = { + changelog: '### Features\n- feat: new feature', + bumpType: 'minor' as const, + totalCommits: 1, + matchedCommitsWithSemver: 1, + }; + + expect(() => validateBumpType(result)).not.toThrow(); + }); +}); + +describe('getChangelogWithBumpType', () => { + const mockGit = {} as SimpleGit; + + beforeEach(() => { + jest.clearAllMocks(); + clearChangesetCache(); // Clear memoization cache between tests + getConfigFileDirMock.mockReturnValue('/test/repo'); + readFileSyncMock.mockImplementation(() => { + const error: NodeJS.ErrnoException = new Error('ENOENT'); + error.code = 'ENOENT'; + throw error; + }); + getGlobalGitHubConfigMock.mockResolvedValue({ + owner: 'testowner', + repo: 'testrepo', + }); + }); + + test('returns changelog and minor bump type for feature commits', async () => { + getChangesSinceMock.mockResolvedValue([ + { hash: 'abc123', title: 'feat: new feature', body: '', pr: '123' }, + ]); + (getGitHubClient as jest.Mock).mockReturnValue({ + graphql: jest.fn().mockResolvedValue({ + repository: { + Cabc123: { + author: { user: { login: 'testuser' } }, + associatedPullRequests: { + nodes: [ + { + number: '123', + title: 'feat: new feature', + body: '', + labels: { nodes: [] }, + }, + ], + }, + }, + }, + }), + }); + + const result = await getChangelogWithBumpType(mockGit, 'v1.0.0'); + + expect(result.bumpType).toBe('minor'); + expect(result.changelog).toBeDefined(); + expect(result.totalCommits).toBe(1); + }); + + test('returns null bumpType when no commits found', async () => { + getChangesSinceMock.mockResolvedValue([]); + + const result = await getChangelogWithBumpType(mockGit, 'v1.0.0'); + + expect(result.bumpType).toBeNull(); + expect(result.totalCommits).toBe(0); + }); + + test('returns null bumpType when no commits match semver categories', async () => { + getChangesSinceMock.mockResolvedValue([ + { + hash: 'abc123', + title: 'random commit without conventional format', + body: '', + pr: null, + }, + ]); + (getGitHubClient as jest.Mock).mockReturnValue({ + graphql: jest.fn().mockResolvedValue({ + repository: { + Cabc123: { + author: { user: { login: 'testuser' } }, + associatedPullRequests: { nodes: [] }, + }, + }, + }), + }); + + const result = await getChangelogWithBumpType(mockGit, 'v1.0.0'); + + expect(result.bumpType).toBeNull(); + expect(result.totalCommits).toBe(1); + expect(result.matchedCommitsWithSemver).toBe(0); + }); + + test('returns patch bump type for fix commits', async () => { + getChangesSinceMock.mockResolvedValue([ + { hash: 'abc123', title: 'fix: bug fix', body: '', pr: '456' }, + ]); + (getGitHubClient as jest.Mock).mockReturnValue({ + graphql: jest.fn().mockResolvedValue({ + repository: { + Cabc123: { + author: { user: { login: 'testuser' } }, + associatedPullRequests: { + nodes: [ + { + number: '456', + title: 'fix: bug fix', + body: '', + labels: { nodes: [] }, + }, + ], + }, + }, + }, + }), + }); + + const result = await getChangelogWithBumpType(mockGit, 'v2.0.0'); + + expect(result.bumpType).toBe('patch'); + }); + + test('returns major bump type for breaking changes', async () => { + getChangesSinceMock.mockResolvedValue([ + { hash: 'abc123', title: 'feat!: breaking change', body: '', pr: '789' }, + ]); + (getGitHubClient as jest.Mock).mockReturnValue({ + graphql: jest.fn().mockResolvedValue({ + repository: { + Cabc123: { + author: { user: { login: 'testuser' } }, + associatedPullRequests: { + nodes: [ + { + number: '789', + title: 'feat!: breaking change', + body: '', + labels: { nodes: [] }, + }, + ], + }, + }, + }, + }), + }); + + const result = await getChangelogWithBumpType(mockGit, ''); + + expect(result.bumpType).toBe('major'); + }); +}); diff --git a/src/utils/__tests__/calver.test.ts b/src/utils/__tests__/calver.test.ts new file mode 100644 index 00000000..2afb073e --- /dev/null +++ b/src/utils/__tests__/calver.test.ts @@ -0,0 +1,199 @@ +import { formatCalVerDate, calculateCalVer, DEFAULT_CALVER_CONFIG } from '../calver'; + +// Mock the config module to control tagPrefix +jest.mock('../../config', () => ({ + getGitTagPrefix: jest.fn(() => ''), +})); + +import { getGitTagPrefix } from '../../config'; + +const mockGetGitTagPrefix = getGitTagPrefix as jest.Mock; + +describe('formatCalVerDate', () => { + it('formats %y as 2-digit year', () => { + const date = new Date('2024-12-15'); + expect(formatCalVerDate(date, '%y')).toBe('24'); + }); + + it('formats %Y as 4-digit year', () => { + const date = new Date('2024-12-15'); + expect(formatCalVerDate(date, '%Y')).toBe('2024'); + }); + + it('formats %m as zero-padded month', () => { + const date = new Date('2024-01-15'); + expect(formatCalVerDate(date, '%m')).toBe('01'); + + const date2 = new Date('2024-12-15'); + expect(formatCalVerDate(date2, '%m')).toBe('12'); + }); + + it('formats %-m as month without padding', () => { + const date = new Date('2024-01-15'); + expect(formatCalVerDate(date, '%-m')).toBe('1'); + + const date2 = new Date('2024-12-15'); + expect(formatCalVerDate(date2, '%-m')).toBe('12'); + }); + + it('formats %d as zero-padded day', () => { + const date = new Date('2024-12-05'); + expect(formatCalVerDate(date, '%d')).toBe('05'); + + const date2 = new Date('2024-12-25'); + expect(formatCalVerDate(date2, '%d')).toBe('25'); + }); + + it('formats %-d as day without padding', () => { + const date = new Date('2024-12-05'); + expect(formatCalVerDate(date, '%-d')).toBe('5'); + + const date2 = new Date('2024-12-25'); + expect(formatCalVerDate(date2, '%-d')).toBe('25'); + }); + + it('handles the default format %y.%-m', () => { + const date = new Date('2024-12-15'); + expect(formatCalVerDate(date, '%y.%-m')).toBe('24.12'); + + const date2 = new Date('2024-01-15'); + expect(formatCalVerDate(date2, '%y.%-m')).toBe('24.1'); + }); + + it('handles complex format strings', () => { + const date = new Date('2024-03-05'); + expect(formatCalVerDate(date, '%Y.%m.%d')).toBe('2024.03.05'); + expect(formatCalVerDate(date, '%y.%-m.%-d')).toBe('24.3.5'); + }); +}); + +describe('calculateCalVer', () => { + const mockGit = { + tags: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockGetGitTagPrefix.mockReturnValue(''); + // Mock Date to return a fixed date + jest.useFakeTimers(); + jest.setSystemTime(new Date('2024-12-23')); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('returns first patch version when no tags exist', async () => { + mockGit.tags.mockResolvedValue({ all: [] }); + + const version = await calculateCalVer(mockGit as any, { + offset: 0, + format: '%y.%-m', + }); + + expect(version).toBe('24.12.0'); + }); + + it('increments patch version when tag exists', async () => { + mockGit.tags.mockResolvedValue({ all: ['24.12.0'] }); + + const version = await calculateCalVer(mockGit as any, { + offset: 0, + format: '%y.%-m', + }); + + expect(version).toBe('24.12.1'); + }); + + it('finds the highest patch and increments', async () => { + mockGit.tags.mockResolvedValue({ all: ['24.12.0', '24.12.1', '24.12.2'] }); + + const version = await calculateCalVer(mockGit as any, { + offset: 0, + format: '%y.%-m', + }); + + expect(version).toBe('24.12.3'); + }); + + it('ignores tags from different date parts', async () => { + mockGit.tags.mockResolvedValue({ all: ['24.11.0', '24.11.1', '23.12.0'] }); + + const version = await calculateCalVer(mockGit as any, { + offset: 0, + format: '%y.%-m', + }); + + expect(version).toBe('24.12.0'); + }); + + it('applies offset correctly', async () => { + // Date is 2024-12-23, with 14 day offset should be 2024-12-09 (still December) + mockGit.tags.mockResolvedValue({ all: [] }); + + const version = await calculateCalVer(mockGit as any, { + offset: 14, + format: '%y.%-m', + }); + + expect(version).toBe('24.12.0'); + }); + + it('applies large offset that changes month', async () => { + // Date is 2024-12-23, with 30 day offset should be 2024-11-23 + mockGit.tags.mockResolvedValue({ all: [] }); + + const version = await calculateCalVer(mockGit as any, { + offset: 30, + format: '%y.%-m', + }); + + expect(version).toBe('24.11.0'); + }); + + it('handles non-numeric patch suffixes gracefully', async () => { + mockGit.tags.mockResolvedValue({ all: ['24.12.0', '24.12.beta', '24.12.1'] }); + + const version = await calculateCalVer(mockGit as any, { + offset: 0, + format: '%y.%-m', + }); + + expect(version).toBe('24.12.2'); + }); + + it('uses default config values', () => { + expect(DEFAULT_CALVER_CONFIG.offset).toBe(14); + expect(DEFAULT_CALVER_CONFIG.format).toBe('%y.%-m'); + }); + + it('accounts for git tag prefix when searching for existing tags', async () => { + // When tagPrefix is 'v', tags are like 'v24.12.0' + mockGetGitTagPrefix.mockReturnValue('v'); + mockGit.tags.mockResolvedValue({ all: ['v24.12.0', 'v24.12.1'] }); + + const version = await calculateCalVer(mockGit as any, { + offset: 0, + format: '%y.%-m', + }); + + // Should find v24.12.1 and increment to 24.12.2 + expect(version).toBe('24.12.2'); + }); + + it('ignores tags without the configured prefix', async () => { + mockGetGitTagPrefix.mockReturnValue('v'); + // Mix of prefixed and non-prefixed tags + mockGit.tags.mockResolvedValue({ all: ['24.12.5', 'v24.12.0', 'v24.12.1'] }); + + const version = await calculateCalVer(mockGit as any, { + offset: 0, + format: '%y.%-m', + }); + + // Should only find v24.12.0 and v24.12.1, increment to 24.12.2 + // The non-prefixed '24.12.5' should be ignored + expect(version).toBe('24.12.2'); + }); +}); diff --git a/src/utils/__tests__/changelog.test.ts b/src/utils/__tests__/changelog.test.ts index 6155e48d..595ef0f9 100644 --- a/src/utils/__tests__/changelog.test.ts +++ b/src/utils/__tests__/changelog.test.ts @@ -26,6 +26,7 @@ import { extractScope, formatScopeTitle, extractChangelogEntry, + clearChangesetCache, SKIP_CHANGELOG_MAGIC_WORD, BODY_IN_CHANGELOG_MAGIC_WORD, } from '../changelog'; @@ -332,6 +333,9 @@ describe('generateChangesetFromGit', () => { commits: TestCommit[], releaseConfig?: string | null ): void { + // Clear memoization cache to ensure fresh results + clearChangesetCache(); + mockGetChangesSince.mockResolvedValueOnce( commits.map(commit => ({ hash: commit.hash, @@ -805,7 +809,8 @@ describe('generateChangesetFromGit', () => { output: string ) => { setup(commits, releaseConfig); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; expect(changes).toBe(output); } ); @@ -855,7 +860,8 @@ describe('generateChangesetFromGit', () => { expect(getConfigFileDirMock).toBeDefined(); expect(readFileSyncMock).toBeDefined(); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; // Verify getConfigFileDir was called expect(getConfigFileDirMock).toHaveBeenCalled(); @@ -910,7 +916,8 @@ describe('generateChangesetFromGit', () => { - feature` ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; expect(changes).not.toContain('#1'); expect(changes).toContain('#2'); }); @@ -953,7 +960,8 @@ describe('generateChangesetFromGit', () => { - skip-release` ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; // PR #1 is excluded from Features category but should appear in Other // (category-level exclusions only exclude from that specific category) expect(changes).toContain('#1'); @@ -990,7 +998,8 @@ describe('generateChangesetFromGit', () => { - '*'` ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; expect(changes).toContain('### All Changes'); expect(changes).toContain( 'Any PR by @alice in [#1](https://github.com/test-owner/test-repo/pull/1)' @@ -1028,7 +1037,8 @@ describe('generateChangesetFromGit', () => { null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; // When no config exists, default conventional commits patterns are used expect(changes).toContain('### New Features'); expect(changes).toContain('### Bug Fixes'); @@ -1059,7 +1069,8 @@ describe('generateChangesetFromGit', () => { - feature` ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; expect(changes).toContain('### Features'); expect(changes).not.toContain('### Other'); expect(changes).toContain( @@ -1087,7 +1098,8 @@ describe('generateChangesetFromGit', () => { categories: "this is a string, not an array"` ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; // Should not crash, and PR should appear in output (no categories applied) expect(changes).toContain('#1'); }); @@ -1134,7 +1146,8 @@ describe('generateChangesetFromGit', () => { releaseConfigYaml ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; expect(changes).toContain('### Features'); expect(changes).toContain('### Bug Fixes'); @@ -1186,7 +1199,8 @@ describe('generateChangesetFromGit', () => { releaseConfigYaml ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; expect(changes).toContain('### Labeled Features'); expect(changes).toContain('### Pattern Features'); @@ -1265,7 +1279,8 @@ describe('generateChangesetFromGit', () => { null // No release.yml - should use default config ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; expect(changes).toContain('### New Features'); expect(changes).toContain('### Bug Fixes'); @@ -1301,7 +1316,8 @@ describe('generateChangesetFromGit', () => { ); // Should not crash, and valid pattern should still work - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; expect(changes).toContain('### Features'); expect(changes).toContain('feat: new feature'); }); @@ -1345,7 +1361,8 @@ describe('generateChangesetFromGit', () => { releaseConfigYaml ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; expect(changes).toContain('### Features'); expect(changes).not.toContain('### Other'); @@ -1394,7 +1411,8 @@ describe('generateChangesetFromGit', () => { releaseConfigYaml ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; expect(changes).toContain('### Features'); // PR #1 should be excluded from Features (but appear in Other) @@ -1441,7 +1459,8 @@ describe('generateChangesetFromGit', () => { releaseConfigYaml ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; expect(changes).toContain('### Features'); expect(changes).not.toContain('### Other'); @@ -1486,7 +1505,8 @@ describe('generateChangesetFromGit', () => { releaseConfigYaml ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; expect(changes).toContain('### Features'); expect(changes).toContain('feat(api): add endpoint'); @@ -1536,7 +1556,8 @@ describe('generateChangesetFromGit', () => { null // Use default config ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; expect(changes).toContain('### Bug Fixes'); expect(changes).toContain('### New Features'); @@ -1591,7 +1612,8 @@ describe('generateChangesetFromGit', () => { null // Use default config ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; expect(changes).toContain('### Build / dependencies / internal'); expect(changes).toContain('refactor: clean up code'); @@ -1632,12 +1654,72 @@ describe('generateChangesetFromGit', () => { null // Use default config ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; expect(changes).toContain('### Breaking Changes'); expect(changes).toContain('feat(my-api)!: breaking api change'); expect(changes).toContain('fix!: breaking fix'); }); + + it('should trim leading and trailing whitespace from PR titles before pattern matching', async () => { + const releaseConfigYaml = `changelog: + categories: + - title: Features + commit_patterns: + - "^feat:" + - title: Bug Fixes + commit_patterns: + - "^fix:"`; + + setup( + [ + { + hash: 'abc123', + title: ' feat: feature with leading space', + body: '', + pr: { + remote: { + number: '1', + author: { login: 'alice' }, + labels: [], + title: ' feat: feature with leading space', // PR title from GitHub has leading space + }, + }, + }, + { + hash: 'def456', + title: 'fix: bug fix with trailing space ', + body: '', + pr: { + remote: { + number: '2', + author: { login: 'bob' }, + labels: [], + title: 'fix: bug fix with trailing space ', // PR title from GitHub has trailing space + }, + }, + }, + ], + releaseConfigYaml + ); + + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 3); + const changes = result.changelog; + + // Both should be properly categorized despite whitespace + expect(changes).toContain('### Features'); + expect(changes).toContain('### Bug Fixes'); + // Titles should be trimmed in output (no leading/trailing spaces) + expect(changes).toContain( + 'feat: feature with leading space by @alice in [#1](https://github.com/test-owner/test-repo/pull/1)' + ); + expect(changes).toContain( + 'fix: bug fix with trailing space by @bob in [#2](https://github.com/test-owner/test-repo/pull/2)' + ); + // Should NOT go to Other section + expect(changes).not.toContain('### Other'); + }); }); describe('section ordering', () => { @@ -1701,7 +1783,8 @@ describe('generateChangesetFromGit', () => { releaseConfigYaml ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; // Sections should appear in config order, not encounter order const featuresIndex = changes.indexOf('### Features'); @@ -1782,7 +1865,8 @@ describe('generateChangesetFromGit', () => { releaseConfigYaml ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; // Features should still come before Bug Fixes per config order const featuresIndex = changes.indexOf('### Features'); @@ -1870,7 +1954,8 @@ describe('generateChangesetFromGit', () => { null // No config - use defaults ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; // Default order from DEFAULT_RELEASE_CONFIG: // Breaking Changes, Build/deps, Bug Fixes, Documentation, New Features @@ -1972,7 +2057,8 @@ describe('generateChangesetFromGit', () => { null // Use default config ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; // Verify Api scope header exists (has 2 entries) const apiSection = getSectionContent(changes, /#### Api\n/); @@ -1989,7 +2075,7 @@ describe('generateChangesetFromGit', () => { expect(uiSection).not.toContain('feat(api):'); }); - it('should place scopeless entries at the bottom without sub-header', async () => { + it('should place scopeless entries at the bottom under "Other" header when scoped entries exist', async () => { setup( [ { @@ -2032,35 +2118,131 @@ describe('generateChangesetFromGit', () => { null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; // Should have Api scope header (has 2 entries) expect(changes).toContain('#### Api'); - // Scopeless entry should appear after all scope headers (at the bottom) - const lastScopeHeaderIndex = changes.lastIndexOf('#### '); - const scopelessIndex = changes.indexOf('feat: add feature without scope'); - expect(scopelessIndex).toBeGreaterThan(lastScopeHeaderIndex); + // Should have an "Other" header for scopeless entries (since Api has a header) + expect(changes).toContain('#### Other'); - // Verify the scopeless entry doesn't have its own #### header before it - // by checking that the line immediately before it is not a scope header - const lines = changes.split('\n'); - const scopelessLineIndex = lines.findIndex(line => - line.includes('feat: add feature without scope') - ); - // Find the closest non-empty line before the scopeless entry - let prevLineIndex = scopelessLineIndex - 1; - while (prevLineIndex >= 0 && lines[prevLineIndex].trim() === '') { - prevLineIndex--; - } - // The previous non-empty line should not be a #### header - expect(lines[prevLineIndex]).not.toMatch(/^#### /); + // Scopeless entry should appear after the "Other" header + const otherHeaderIndex = changes.indexOf('#### Other'); + const scopelessIndex = changes.indexOf('feat: add feature without scope'); + expect(scopelessIndex).toBeGreaterThan(otherHeaderIndex); // Verify Api scope entry comes before scopeless entry const apiEntryIndex = changes.indexOf('feat(api): add endpoint'); expect(apiEntryIndex).toBeLessThan(scopelessIndex); }); + it('should not add "Other" header when no scoped entries have headers', async () => { + setup( + [ + { + hash: 'abc123', + title: 'feat(api): single api feature', + body: '', + pr: { + remote: { + number: '1', + author: { login: 'alice' }, + labels: [], + }, + }, + }, + { + hash: 'def456', + title: 'feat: feature without scope', + body: '', + pr: { + remote: { + number: '2', + author: { login: 'bob' }, + labels: [], + }, + }, + }, + ], + null + ); + + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; + + // Neither scope should have a header (both have only 1 entry) + expect(changes).not.toContain('#### Api'); + // No "Other" header should be added since there are no other scope headers + expect(changes).not.toContain('#### Other'); + + // But both PRs should still appear in the output + expect(changes).toContain('feat(api): single api feature'); + expect(changes).toContain('feat: feature without scope'); + }); + + it('should not add extra newlines between entries without scope headers', async () => { + setup( + [ + { + hash: 'abc123', + title: 'feat(docker): add docker feature', + body: '', + pr: { + remote: { + number: '1', + author: { login: 'alice' }, + labels: [], + }, + }, + }, + { + hash: 'def456', + title: 'feat: add feature without scope 1', + body: '', + pr: { + remote: { + number: '2', + author: { login: 'bob' }, + labels: [], + }, + }, + }, + { + hash: 'ghi789', + title: 'feat: add feature without scope 2', + body: '', + pr: { + remote: { + number: '3', + author: { login: 'charlie' }, + labels: [], + }, + }, + }, + ], + null + ); + + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; + + // All three should appear without extra blank lines between them + // (no scope headers since docker only has 1 entry and scopeless don't trigger headers) + expect(changes).not.toContain('#### Docker'); + expect(changes).not.toContain('#### Other'); + + // Verify no double newlines between entries (which would indicate separate sections) + const featuresSection = getSectionContent(changes, /### New Features[^\n]*\n/); + expect(featuresSection).not.toBeNull(); + // There should be no blank lines between the three entries + expect(featuresSection).not.toMatch(/\n\n-/); + // All entries should be present + expect(featuresSection).toContain('feat(docker): add docker feature'); + expect(featuresSection).toContain('feat: add feature without scope 1'); + expect(featuresSection).toContain('feat: add feature without scope 2'); + }); + it('should skip scope header for scopes with only one entry', async () => { setup( [ @@ -2092,7 +2274,8 @@ describe('generateChangesetFromGit', () => { null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; // Neither scope should have a header (both have only 1 entry) expect(changes).not.toContain('#### Api'); @@ -2146,7 +2329,8 @@ describe('generateChangesetFromGit', () => { null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; // Should only have one Api header (all merged) const apiMatches = changes.match(/#### Api/gi); @@ -2239,7 +2423,8 @@ describe('generateChangesetFromGit', () => { null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; const alphaIndex = changes.indexOf('#### Alpha'); const betaIndex = changes.indexOf('#### Beta'); @@ -2311,7 +2496,8 @@ describe('generateChangesetFromGit', () => { null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; // Verify scope headers are formatted correctly (each has 2 entries) expect(changes).toContain('#### Another Component'); @@ -2398,7 +2584,8 @@ describe('generateChangesetFromGit', () => { - enhancement` ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; expect(changes).toContain('### Features'); @@ -2471,7 +2658,8 @@ describe('generateChangesetFromGit', () => { null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; expect(changes).toContain('### Breaking Changes'); @@ -2520,7 +2708,8 @@ describe('generateChangesetFromGit', () => { null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; // Should only have one "My Component" header (merged via normalization) const myComponentMatches = changes.match(/#### My Component/gi); @@ -2565,7 +2754,8 @@ Closes #123`, null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; // Should use the custom changelog entry, not the PR title expect(changes).toContain( @@ -2600,7 +2790,8 @@ Closes #456`, null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; // Should use PR title when no custom changelog entry exists expect(changes).toContain('feat: Add bar function'); @@ -2656,7 +2847,8 @@ Custom entry for bug fix C`, null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; expect(changes).toContain('Custom entry for feature A'); expect(changes).toContain('feat: Add feature B'); @@ -2705,7 +2897,8 @@ Custom entry for bug fix C`, - bug` ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; expect(changes).toContain('### Features'); expect(changes).toContain('### Bug Fixes'); @@ -2756,7 +2949,8 @@ Add endpoint for data export`, null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; // Should still group by scope even with custom changelog entries expect(changes).toContain('#### Api'); @@ -2786,7 +2980,8 @@ Update all dependencies to their latest versions for improved security`, null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; expect(changes).toContain( 'Update all dependencies to their latest versions for improved security' @@ -2819,7 +3014,8 @@ Update all dependencies to their latest versions for improved security`, null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; expect(changes).toContain( 'Add comprehensive user authentication system by @alice in [#1]' @@ -2853,7 +3049,8 @@ Update all dependencies to their latest versions for improved security`, null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; // Should have 3 separate changelog entries from the same PR expect(changes).toContain('Add OAuth2 authentication by @alice in [#1]'); @@ -2888,7 +3085,8 @@ Update all dependencies to their latest versions for improved security`, null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; // First entry with nested content expect(changes).toContain('Add authentication by @alice in [#1]'); @@ -2925,7 +3123,8 @@ Closes #123`, null ); - const changes = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const result = await generateChangesetFromGit(dummyGit, '1.0.0', 10); + const changes = result.changelog; // Should fall back to PR title when changelog entry is empty expect(changes).toContain('feat: Add feature'); diff --git a/src/utils/__tests__/workspaces.test.ts b/src/utils/__tests__/workspaces.test.ts new file mode 100644 index 00000000..be73dccd --- /dev/null +++ b/src/utils/__tests__/workspaces.test.ts @@ -0,0 +1,265 @@ +import { resolve } from 'path'; + +import { + discoverWorkspaces, + filterWorkspacePackages, + packageNameToArtifactPattern, + packageNameToArtifactFromTemplate, + topologicalSortPackages, + WorkspacePackage, +} from '../workspaces'; + +const fixturesDir = resolve(__dirname, '../__fixtures__/workspaces'); + +describe('discoverWorkspaces', () => { + test('discovers npm workspaces', async () => { + const result = await discoverWorkspaces(resolve(fixturesDir, 'npm-workspace')); + + expect(result.type).toBe('npm'); + expect(result.packages).toHaveLength(2); + + const packageNames = result.packages.map(p => p.name).sort(); + expect(packageNames).toEqual(['@test/pkg-a', '@test/pkg-b']); + + // Check that pkg-b is marked as private + const pkgB = result.packages.find(p => p.name === '@test/pkg-b'); + expect(pkgB?.private).toBe(true); + // Check that pkg-b has pkg-a as a workspace dependency + expect(pkgB?.workspaceDependencies).toEqual(['@test/pkg-a']); + + const pkgA = result.packages.find(p => p.name === '@test/pkg-a'); + expect(pkgA?.private).toBe(false); + expect(pkgA?.workspaceDependencies).toEqual([]); + }); + + test('discovers pnpm workspaces', async () => { + const result = await discoverWorkspaces(resolve(fixturesDir, 'pnpm-workspace')); + + expect(result.type).toBe('pnpm'); + expect(result.packages).toHaveLength(2); + + const packageNames = result.packages.map(p => p.name).sort(); + expect(packageNames).toEqual(['@pnpm/pkg-a', '@pnpm/pkg-b']); + }); + + test('returns none type when no workspaces found', async () => { + const result = await discoverWorkspaces(resolve(fixturesDir, 'no-workspace')); + + expect(result.type).toBe('none'); + expect(result.packages).toHaveLength(0); + }); + + test('returns none type for non-existent directory', async () => { + const result = await discoverWorkspaces(resolve(fixturesDir, 'does-not-exist')); + + expect(result.type).toBe('none'); + expect(result.packages).toHaveLength(0); + }); +}); + +describe('filterWorkspacePackages', () => { + const testPackages: WorkspacePackage[] = [ + { name: '@sentry/browser', location: '/path/browser', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry-internal/utils'] }, + { name: '@sentry/node', location: '/path/node', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry-internal/utils'] }, + { name: '@sentry-internal/utils', location: '/path/utils', private: false, hasPublicAccess: true, workspaceDependencies: [] }, + { name: '@other/package', location: '/path/other', private: false, hasPublicAccess: false, workspaceDependencies: [] }, + ]; + + test('returns all packages when no filters provided', () => { + const result = filterWorkspacePackages(testPackages); + expect(result).toHaveLength(4); + }); + + test('filters packages by include pattern', () => { + const result = filterWorkspacePackages( + testPackages, + /^@sentry\// + ); + + expect(result).toHaveLength(2); + expect(result.map(p => p.name)).toEqual(['@sentry/browser', '@sentry/node']); + }); + + test('filters packages by exclude pattern', () => { + const result = filterWorkspacePackages( + testPackages, + undefined, + /^@sentry-internal\// + ); + + expect(result).toHaveLength(3); + expect(result.map(p => p.name)).toEqual([ + '@sentry/browser', + '@sentry/node', + '@other/package', + ]); + }); + + test('applies both include and exclude patterns', () => { + const result = filterWorkspacePackages( + testPackages, + /^@sentry/, + /^@sentry-internal\// + ); + + expect(result).toHaveLength(2); + expect(result.map(p => p.name)).toEqual(['@sentry/browser', '@sentry/node']); + }); +}); + +describe('packageNameToArtifactPattern', () => { + test('converts scoped package name to pattern', () => { + const pattern = packageNameToArtifactPattern('@sentry/browser'); + expect(pattern).toBe('/^sentry-browser-\\d.*\\.tgz$/'); + }); + + test('converts nested scoped package name to pattern', () => { + const pattern = packageNameToArtifactPattern('@sentry-internal/browser-utils'); + expect(pattern).toBe('/^sentry-internal-browser-utils-\\d.*\\.tgz$/'); + }); + + test('converts unscoped package name to pattern', () => { + const pattern = packageNameToArtifactPattern('my-package'); + expect(pattern).toBe('/^my-package-\\d.*\\.tgz$/'); + }); +}); + +describe('packageNameToArtifactFromTemplate', () => { + test('replaces {{name}} with full package name', () => { + const result = packageNameToArtifactFromTemplate( + '@sentry/browser', + '{{name}}.tgz' + ); + expect(result).toBe('/^@sentry\\/browser\\.tgz$/'); + }); + + test('replaces {{simpleName}} with normalized name', () => { + const result = packageNameToArtifactFromTemplate( + '@sentry/browser', + '{{simpleName}}.tgz' + ); + expect(result).toBe('/^sentry-browser\\.tgz$/'); + }); + + test('replaces {{version}} with version placeholder', () => { + const result = packageNameToArtifactFromTemplate( + '@sentry/browser', + '{{simpleName}}-{{version}}.tgz' + ); + expect(result).toBe('/^sentry-browser-\\d.*\\.tgz$/'); + }); + + test('replaces {{version}} with specific version', () => { + const result = packageNameToArtifactFromTemplate( + '@sentry/browser', + '{{simpleName}}-{{version}}.tgz', + '1.0.0' + ); + expect(result).toBe('/^sentry-browser-1\\.0\\.0\\.tgz$/'); + }); + + test('handles complex templates', () => { + const result = packageNameToArtifactFromTemplate( + '@sentry/browser', + 'dist/{{simpleName}}/{{simpleName}}-{{version}}.tgz' + ); + expect(result).toBe('/^dist\\/sentry-browser\\/sentry-browser-\\d.*\\.tgz$/'); + }); +}); + +describe('topologicalSortPackages', () => { + test('returns packages in dependency order', () => { + const packages: WorkspacePackage[] = [ + { name: '@sentry/browser', location: '/path/browser', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry/core'] }, + { name: '@sentry/core', location: '/path/core', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry/types'] }, + { name: '@sentry/types', location: '/path/types', private: false, hasPublicAccess: true, workspaceDependencies: [] }, + ]; + + const sorted = topologicalSortPackages(packages); + + expect(sorted.map(p => p.name)).toEqual([ + '@sentry/types', // no dependencies, comes first + '@sentry/core', // depends on types + '@sentry/browser', // depends on core + ]); + }); + + test('handles packages with no dependencies', () => { + const packages: WorkspacePackage[] = [ + { name: 'pkg-a', location: '/path/a', private: false, hasPublicAccess: false, workspaceDependencies: [] }, + { name: 'pkg-b', location: '/path/b', private: false, hasPublicAccess: false, workspaceDependencies: [] }, + { name: 'pkg-c', location: '/path/c', private: false, hasPublicAccess: false, workspaceDependencies: [] }, + ]; + + const sorted = topologicalSortPackages(packages); + + // All packages have no dependencies, order should be preserved + expect(sorted.map(p => p.name)).toEqual(['pkg-a', 'pkg-b', 'pkg-c']); + }); + + test('handles diamond dependencies', () => { + // Diamond: A depends on B and C, both B and C depend on D + const packages: WorkspacePackage[] = [ + { name: 'A', location: '/path/a', private: false, hasPublicAccess: false, workspaceDependencies: ['B', 'C'] }, + { name: 'B', location: '/path/b', private: false, hasPublicAccess: false, workspaceDependencies: ['D'] }, + { name: 'C', location: '/path/c', private: false, hasPublicAccess: false, workspaceDependencies: ['D'] }, + { name: 'D', location: '/path/d', private: false, hasPublicAccess: false, workspaceDependencies: [] }, + ]; + + const sorted = topologicalSortPackages(packages); + + // D must come before B and C, B and C must come before A + expect(sorted.map(p => p.name)).toEqual(['D', 'B', 'C', 'A']); + }); + + test('ignores dependencies not in the package list', () => { + const packages: WorkspacePackage[] = [ + { name: 'pkg-a', location: '/path/a', private: false, hasPublicAccess: false, workspaceDependencies: ['external-dep', 'pkg-b'] }, + { name: 'pkg-b', location: '/path/b', private: false, hasPublicAccess: false, workspaceDependencies: ['lodash'] }, + ]; + + const sorted = topologicalSortPackages(packages); + + // pkg-b comes first because pkg-a depends on it + expect(sorted.map(p => p.name)).toEqual(['pkg-b', 'pkg-a']); + }); + + test('throws error on circular dependencies', () => { + const packages: WorkspacePackage[] = [ + { name: 'pkg-a', location: '/path/a', private: false, hasPublicAccess: false, workspaceDependencies: ['pkg-b'] }, + { name: 'pkg-b', location: '/path/b', private: false, hasPublicAccess: false, workspaceDependencies: ['pkg-c'] }, + { name: 'pkg-c', location: '/path/c', private: false, hasPublicAccess: false, workspaceDependencies: ['pkg-a'] }, + ]; + + expect(() => topologicalSortPackages(packages)).toThrow(/Circular dependency/); + }); + + test('handles complex dependency graph with multiple branches', () => { + // Real-world-like setup similar to sentry-javascript: + // types -> core -> (browser, node-core -> node) -> nextjs (depends on browser and node) + const packages: WorkspacePackage[] = [ + { name: '@sentry/nextjs', location: '/path/nextjs', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry/browser', '@sentry/node'] }, + { name: '@sentry/browser', location: '/path/browser', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry/core'] }, + { name: '@sentry/node', location: '/path/node', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry/node-core'] }, + { name: '@sentry/node-core', location: '/path/node-core', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry/core'] }, + { name: '@sentry/core', location: '/path/core', private: false, hasPublicAccess: true, workspaceDependencies: ['@sentry/types'] }, + { name: '@sentry/types', location: '/path/types', private: false, hasPublicAccess: true, workspaceDependencies: [] }, + ]; + + const sorted = topologicalSortPackages(packages); + const names = sorted.map(p => p.name); + + // Verify exact expected order with two branches: + // types -> core -> browser (branch 1) + // types -> core -> node-core -> node (branch 2) + // nextjs depends on both browser and node + expect(names).toEqual([ + '@sentry/types', // depth 0: no dependencies + '@sentry/core', // depth 1: depends on types + '@sentry/browser', // depth 2: depends on core + '@sentry/node-core', // depth 2: depends on core + '@sentry/node', // depth 3: depends on node-core + '@sentry/nextjs', // depth 4: depends on browser and node + ]); + }); +}); diff --git a/src/utils/autoVersion.ts b/src/utils/autoVersion.ts new file mode 100644 index 00000000..7537af41 --- /dev/null +++ b/src/utils/autoVersion.ts @@ -0,0 +1,91 @@ +import * as semver from 'semver'; +import type { SimpleGit } from 'simple-git'; + +import { logger } from '../logger'; +import { + generateChangesetFromGit, + BUMP_TYPES, + isBumpType, + type BumpType, + type ChangelogResult, +} from './changelog'; + +// Re-export for convenience +export { BUMP_TYPES, isBumpType, type BumpType, type ChangelogResult }; + +/** + * Calculates the next version by applying the bump type to the current version. + * + * @param currentVersion The current version string (e.g., "1.2.3") + * @param bumpType The type of bump to apply + * @returns The new version string + * @throws Error if the version cannot be incremented + */ +export function calculateNextVersion( + currentVersion: string, + bumpType: BumpType +): string { + // Handle empty/missing current version (new project) + const versionToBump = currentVersion || '0.0.0'; + + const newVersion = semver.inc(versionToBump, bumpType); + + if (!newVersion) { + throw new Error( + `Failed to increment version "${versionToBump}" with bump type "${bumpType}"` + ); + } + + return newVersion; +} + +/** + * Generates changelog and determines version bump type from commits. + * This is a convenience wrapper around generateChangesetFromGit that logs progress. + * + * @param git The SimpleGit instance + * @param rev The revision (tag) to analyze from + * @returns The changelog result (bumpType may be null if no matching commits) + */ +export async function getChangelogWithBumpType( + git: SimpleGit, + rev: string +): Promise { + logger.info( + `Analyzing commits since ${rev || '(beginning of history)'} for auto-versioning...` + ); + + const result = await generateChangesetFromGit(git, rev); + + if (result.bumpType) { + logger.info( + `Auto-version: determined ${result.bumpType} bump ` + + `(${result.matchedCommitsWithSemver}/${result.totalCommits} commits matched)` + ); + } + + return result; +} + +/** + * Validates that a changelog result has the required bump type for auto-versioning. + * + * @param result The changelog result to validate + * @throws Error if no commits found or none match categories with semver fields + */ +export function validateBumpType(result: ChangelogResult): asserts result is ChangelogResult & { bumpType: BumpType } { + if (result.totalCommits === 0) { + throw new Error( + 'Cannot determine version automatically: no commits found since the last release.' + ); + } + + if (result.bumpType === null) { + throw new Error( + `Cannot determine version automatically: ${result.totalCommits} commit(s) found, ` + + 'but none matched a category with a "semver" field in the release configuration. ' + + 'Please ensure your .github/release.yml categories have "semver" fields defined, ' + + 'or specify the version explicitly.' + ); + } +} diff --git a/src/utils/calver.ts b/src/utils/calver.ts new file mode 100644 index 00000000..4ecccd9f --- /dev/null +++ b/src/utils/calver.ts @@ -0,0 +1,101 @@ +import type { SimpleGit } from 'simple-git'; + +import { getGitTagPrefix } from '../config'; +import { logger } from '../logger'; + +/** + * Configuration for CalVer versioning + */ +export interface CalVerConfig { + /** Days to go back for date calculation */ + offset: number; + /** strftime-like format for date part */ + format: string; +} + +/** + * Default CalVer configuration + */ +export const DEFAULT_CALVER_CONFIG: CalVerConfig = { + offset: 14, + format: '%y.%-m', +}; + +/** + * Formats a date according to a strftime-like format string. + * + * Supported format specifiers: + * - %y: 2-digit year (e.g., "24" for 2024) + * - %Y: 4-digit year (e.g., "2024") + * - %m: Zero-padded month (e.g., "01" for January) + * - %-m: Month without zero padding (e.g., "1" for January) + * - %d: Zero-padded day (e.g., "05") + * - %-d: Day without zero padding (e.g., "5") + * + * @param date The date to format + * @param format The format string + * @returns The formatted date string + */ +export function formatCalVerDate(date: Date, format: string): string { + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + + return format + .replace('%Y', String(year)) + .replace('%y', String(year).slice(-2)) + .replace('%-m', String(month)) + .replace('%m', String(month).padStart(2, '0')) + .replace('%-d', String(day)) + .replace('%d', String(day).padStart(2, '0')); +} + +/** + * Calculates the next CalVer version based on existing tags. + * + * The version format is: {datePart}.{patch} + * For example, with format '%y.%-m' and no existing tags: "24.12.0" + * + * @param git SimpleGit instance for checking existing tags + * @param config CalVer configuration + * @returns The next CalVer version string + */ +export async function calculateCalVer( + git: SimpleGit, + config: CalVerConfig +): Promise { + // Calculate date with offset + const date = new Date(); + date.setDate(date.getDate() - config.offset); + + // Format date part + const datePart = formatCalVerDate(date, config.format); + + logger.debug(`CalVer: using date ${date.toISOString()}, date part: ${datePart}`); + + // Find existing tags and determine next patch version + // Account for git tag prefix (e.g., 'v') when searching + const gitTagPrefix = getGitTagPrefix(); + const searchPrefix = `${gitTagPrefix}${datePart}.`; + + logger.debug(`CalVer: searching for tags with prefix: ${searchPrefix}`); + + const tags = await git.tags(); + let patch = 0; + + // Find the highest patch version for this date part + for (const tag of tags.all) { + if (tag.startsWith(searchPrefix)) { + const patchStr = tag.slice(searchPrefix.length); + const patchNum = parseInt(patchStr, 10); + if (!isNaN(patchNum) && patchNum >= patch) { + patch = patchNum + 1; + } + } + } + + const version = `${datePart}.${patch}`; + logger.info(`CalVer: determined version ${version}`); + + return version; +} diff --git a/src/utils/changelog.ts b/src/utils/changelog.ts index 16412404..f9f92759 100644 --- a/src/utils/changelog.ts +++ b/src/utils/changelog.ts @@ -13,6 +13,72 @@ import { getChangesSince } from './git'; import { getGitHubClient } from './githubApi'; import { getVersion } from './version'; +/** Information about the current (unmerged) PR to inject into changelog */ +export interface CurrentPRInfo { + number: number; + title: string; + body: string; + author: string; + labels: string[]; + /** Base branch ref (e.g., "master") for computing merge base */ + baseRef: string; +} + +/** + * Fetches PR details from GitHub API by PR number. + * + * @param prNumber The PR number to fetch + * @returns PR info + * @throws Error if PR cannot be fetched + */ +async function fetchPRInfo(prNumber: number): Promise { + const { repo, owner } = await getGlobalGitHubConfig(); + const github = getGitHubClient(); + + const { data: pr } = await github.pulls.get({ + owner, + repo, + pull_number: prNumber, + }); + + const { data: labels } = await github.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: prNumber, + }); + + return { + number: prNumber, + title: pr.title, + body: pr.body ?? '', + author: pr.user?.login ?? '', + labels: labels.map(l => l.name), + baseRef: pr.base.ref, + }; +} + +/** + * Version bump types. + */ +export type BumpType = 'major' | 'minor' | 'patch'; + +/** + * Version bump type priorities (lower number = higher priority). + * Used for determining the highest bump type from commits. + */ +export const BUMP_TYPES: Map = new Map([ + ['major', 0], + ['minor', 1], + ['patch', 2], +]); + +/** + * Type guard to check if a string is a valid BumpType. + */ +export function isBumpType(value: string): value is BumpType { + return BUMP_TYPES.has(value as BumpType); +} + /** * Path to the changelog file in the target repository */ @@ -391,6 +457,8 @@ interface PullRequest { hash: string; body: string; title: string; + /** Whether this entry should be highlighted in output */ + highlight?: boolean; } interface Commit { @@ -404,22 +472,48 @@ interface Commit { prBody?: string | null; labels: string[]; category: string | null; + /** Whether this entry should be highlighted in output */ + highlight?: boolean; +} + +/** + * Raw commit/PR info before categorization. + * This is the input to the categorization step. + */ +interface RawCommitInfo { + hash: string; + title: string; + body: string; + author?: string; + pr?: string; + prTitle?: string; + prBody?: string; + labels: string[]; + /** Whether this entry should be highlighted in output */ + highlight?: boolean; } +/** + * Valid semver bump types for auto-versioning + */ +export type SemverBumpType = 'major' | 'minor' | 'patch'; + /** * Release configuration structure matching GitHub's release.yml format */ -interface ReleaseConfigCategory { +export interface ReleaseConfigCategory { title: string; labels?: string[]; commit_patterns?: string[]; + /** Semver bump type when commits match this category (for auto-versioning) */ + semver?: SemverBumpType; exclude?: { labels?: string[]; authors?: string[]; }; } -interface ReleaseConfig { +export interface ReleaseConfig { changelog?: { exclude?: { labels?: string[]; @@ -433,28 +527,36 @@ interface ReleaseConfig { * Default release configuration based on conventional commits * Used when .github/release.yml doesn't exist */ -const DEFAULT_RELEASE_CONFIG: ReleaseConfig = { +export const DEFAULT_RELEASE_CONFIG: ReleaseConfig = { changelog: { + exclude: { + labels: ['skip-changelog'], + }, categories: [ { title: 'Breaking Changes 🛠', commit_patterns: ['^\\w+(?:\\([^)]+\\))?!:'], + semver: 'major', }, { title: 'New Features ✨', commit_patterns: ['^feat\\b'], + semver: 'minor', }, { title: 'Bug Fixes 🐛', commit_patterns: ['^fix\\b'], + semver: 'patch', }, { title: 'Documentation 📚', commit_patterns: ['^docs?\\b'], + semver: 'patch', }, { title: 'Build / dependencies / internal 🔧', - commit_patterns: ['^(?:build|refactor|meta|chore|ci)\\b'], + commit_patterns: ['^(?:build|refactor|meta|chore|ci|ref|perf)\\b'], + semver: 'patch', }, ], }, @@ -464,7 +566,7 @@ const DEFAULT_RELEASE_CONFIG: ReleaseConfig = { * Normalized release config with Sets for efficient lookups * All fields are non-optional - use empty sets/arrays when not present */ -interface NormalizedReleaseConfig { +export interface NormalizedReleaseConfig { changelog: { exclude: { labels: Set; @@ -474,10 +576,12 @@ interface NormalizedReleaseConfig { }; } -interface NormalizedCategory { +export interface NormalizedCategory { title: string; labels: string[]; commitLogPatterns: RegExp[]; + /** Semver bump type when commits match this category (for auto-versioning) */ + semver?: SemverBumpType; exclude: { labels: Set; authors: Set; @@ -493,7 +597,7 @@ type CategoryWithPRs = { * Reads and parses .github/release.yml from the repository root * @returns Parsed release configuration, or the default config if file doesn't exist */ -function readReleaseConfig(): ReleaseConfig { +export function readReleaseConfig(): ReleaseConfig { const configFileDir = getConfigFileDir(); if (!configFileDir) { return DEFAULT_RELEASE_CONFIG; @@ -520,7 +624,7 @@ function readReleaseConfig(): ReleaseConfig { /** * Normalizes the release config by converting arrays to Sets and compiling regex patterns */ -function normalizeReleaseConfig( +export function normalizeReleaseConfig( config: ReleaseConfig ): NormalizedReleaseConfig | null { if (!config?.changelog) { @@ -577,6 +681,7 @@ function normalizeReleaseConfig( } }) .filter((r): r is RegExp => r !== null), + semver: category.semver, exclude: { labels: new Set(), authors: new Set(), @@ -607,7 +712,7 @@ function normalizeReleaseConfig( /** * Checks if a PR should be excluded globally based on release config */ -function shouldExcludePR( +export function shouldExcludePR( labels: Set, author: string | undefined, config: NormalizedReleaseConfig | null @@ -634,7 +739,7 @@ function shouldExcludePR( /** * Checks if a category excludes the given PR based on labels and author */ -function isCategoryExcluded( +export function isCategoryExcluded( category: NormalizedCategory, labels: Set, author: string | undefined @@ -655,18 +760,18 @@ function isCategoryExcluded( } /** - * Matches a PR's labels or commit title to a category from release config + * Matches a PR's labels or commit title to a category from release config. * Labels take precedence over commit log pattern matching. * Category-level exclusions are checked here - they exclude the PR from matching this specific category, * allowing it to potentially match other categories or fall through to "Other" - * @returns Category title or null if no match or excluded from this category + * @returns The matched category or null if no match or excluded from all categories */ -function matchPRToCategory( +export function matchCommitToCategory( labels: Set, author: string | undefined, title: string, config: NormalizedReleaseConfig | null -): string | null { +): NormalizedCategory | null { if (!config?.changelog || config.changelog.categories.length === 0) { return null; } @@ -696,7 +801,7 @@ function matchPRToCategory( for (const category of regularCategories) { const matchesCategory = category.labels.some(label => labels.has(label)); if (matchesCategory && !isCategoryExcluded(category, labels, author)) { - return category.title; + return category; } } } @@ -707,7 +812,7 @@ function matchPRToCategory( re.test(title) ); if (matchesPattern && !isCategoryExcluded(category, labels, author)) { - return category.title; + return category; } } @@ -715,7 +820,7 @@ function matchPRToCategory( if (isCategoryExcluded(wildcardCategory, labels, author)) { return null; } - return wildcardCategory.title; + return wildcardCategory; } return null; @@ -733,11 +838,14 @@ interface ChangelogEntry { body?: string; /** Base URL for the repository, e.g. https://github.com/owner/repo */ repoUrl: string; + /** Whether this entry should be highlighted (rendered as blockquote) */ + highlight?: boolean; } /** * Formats a single changelog entry with consistent full markdown link format. * Format: `- Title by @author in [#123](pr-url)` or `- Title in [abcdef12](commit-url)` + * When highlight is true, the entry is prefixed with `> ` (blockquote). */ function formatChangelogEntry(entry: ChangelogEntry): string { let title = entry.title; @@ -792,18 +900,173 @@ function formatChangelogEntry(entry: ChangelogEntry): string { } } + // Apply blockquote highlighting if requested + if (entry.highlight) { + text = text + .split('\n') + .map(line => `> ${line}`) + .join('\n'); + } + return text; } +/** + * Result of changelog generation, includes both the formatted changelog + * and the determined version bump type based on commit categories. + */ +export interface ChangelogResult { + /** The formatted changelog string */ + changelog: string; + /** The highest version bump type found, or null if no commits matched categories with semver */ + bumpType: BumpType | null; + /** Number of commits analyzed */ + totalCommits: number; + /** Number of commits that matched a category with a semver field */ + matchedCommitsWithSemver: number; +} + +/** + * Raw changelog data before serialization to markdown. + * This intermediate representation allows manipulation of entries + * before final formatting. + */ +export interface RawChangelogData { + /** Categories with their PR entries, keyed by category title */ + categories: Map; + /** Commits that didn't match any category */ + leftovers: Commit[]; + /** Release config for serialization */ + releaseConfig: NormalizedReleaseConfig | null; +} + +/** + * Statistics from changelog generation, used for auto-versioning. + */ +interface ChangelogStats { + /** The highest version bump type found */ + bumpType: BumpType | null; + /** Number of commits analyzed */ + totalCommits: number; + /** Number of commits that matched a category with a semver field */ + matchedCommitsWithSemver: number; +} + +/** + * Result from raw changelog generation, includes both data and stats. + */ +interface RawChangelogResult { + data: RawChangelogData; + stats: ChangelogStats; +} + +// Memoization cache for generateChangesetFromGit +// Caches promises to coalesce concurrent calls with the same arguments +const changesetCache = new Map>(); + +function getChangesetCacheKey(rev: string, maxLeftovers: number): string { + return `${rev}:${maxLeftovers}`; +} + +/** + * Clears the memoization cache for generateChangesetFromGit. + * Primarily used for testing. + */ +export function clearChangesetCache(): void { + changesetCache.clear(); +} + export async function generateChangesetFromGit( git: SimpleGit, rev: string, maxLeftovers: number = MAX_LEFTOVERS -): Promise { - const rawConfig = readReleaseConfig(); - const releaseConfig = normalizeReleaseConfig(rawConfig); +): Promise { + const cacheKey = getChangesetCacheKey(rev, maxLeftovers); + + // Return cached promise if available (coalesces concurrent calls) + const cached = changesetCache.get(cacheKey); + if (cached) { + return cached; + } + + // Create and cache the promise + const promise = generateChangesetFromGitImpl(git, rev, maxLeftovers); + changesetCache.set(cacheKey, promise); + + return promise; +} + +/** + * Generates a changelog preview for a PR, showing how it will appear in the changelog. + * This function: + * 1. Fetches PR info from GitHub API (including base branch) + * 2. Fetches all commit/PR info up to base branch + * 3. Adds the current PR to the list with highlight flag + * 4. Runs categorization on the combined list + * 5. Serializes to markdown + * + * @param git Local git client + * @param rev Base revision (tag or SHA) to generate changelog from + * @param currentPRNumber PR number to fetch from GitHub and include (highlighted) + * @returns The changelog result with formatted markdown + */ +export async function generateChangelogWithHighlight( + git: SimpleGit, + rev: string, + currentPRNumber: number +): Promise { + // Step 1: Fetch PR info from GitHub + const prInfo = await fetchPRInfo(currentPRNumber); + + // Step 2: Fetch the base branch to get current state + await git.fetch('origin', prInfo.baseRef); + const baseRef = `origin/${prInfo.baseRef}`; + logger.debug(`Using PR base branch "${prInfo.baseRef}" for changelog`); + + // Step 3: Fetch raw commit info up to base branch + const rawCommits = await fetchRawCommitInfo(git, rev, baseRef); + + // Step 4: Add current PR to the list with highlight flag (at the beginning) + const currentPRCommit: RawCommitInfo = { + hash: '', + title: prInfo.title.trim(), + body: prInfo.body, + author: prInfo.author, + pr: String(prInfo.number), + prTitle: prInfo.title, + prBody: prInfo.body, + labels: prInfo.labels, + highlight: true, + }; + const allCommits = [currentPRCommit, ...rawCommits]; + + // Step 5: Run categorization on combined list + const { data: rawData, stats } = categorizeCommits(allCommits); - const gitCommits = (await getChangesSince(git, rev)).filter( + // Step 6: Serialize to markdown + const changelog = await serializeChangelog(rawData, MAX_LEFTOVERS); + + return { + changelog, + ...stats, + }; +} + +/** + * Fetches raw commit/PR info from git history and GitHub. + * This is the first step - just gathering data, no categorization. + * + * @param git Local git client + * @param rev Base revision (tag or SHA) to start from + * @param until Optional end revision (defaults to HEAD) + * @returns Array of raw commit info + */ +async function fetchRawCommitInfo( + git: SimpleGit, + rev: string, + until?: string +): Promise { + const gitCommits = (await getChangesSince(git, rev, until)).filter( ({ body }) => !body.includes(SKIP_CHANGELOG_MAGIC_WORD) ); @@ -811,105 +1074,152 @@ export async function generateChangesetFromGit( gitCommits.map(({ hash }) => hash) ); - const categories = new Map(); - const commits: Record = {}; - const leftovers: Commit[] = []; - const missing: Commit[] = []; + const result: RawCommitInfo[] = []; for (const gitCommit of gitCommits) { - const hash = gitCommit.hash; + const githubCommit = githubCommits[gitCommit.hash]; - const githubCommit = githubCommits[hash]; + // Skip if PR body has skip marker if (githubCommit?.prBody?.includes(SKIP_CHANGELOG_MAGIC_WORD)) { continue; } - const labelsArray = githubCommit?.labels ?? []; - const labels = new Set(labelsArray); - const author = githubCommit?.author; + result.push({ + hash: gitCommit.hash, + title: gitCommit.title, + body: gitCommit.body, + author: githubCommit?.author, + pr: githubCommit?.pr ?? gitCommit.pr ?? undefined, + prTitle: githubCommit?.prTitle ?? undefined, + prBody: githubCommit?.prBody ?? undefined, + labels: githubCommit?.labels ?? [], + }); + } + + return result; +} + +/** + * Categorizes raw commits into changelog structure. + * This is the second step - grouping by category and scope. + * + * @param rawCommits Array of raw commit info to categorize + * @returns Categorized changelog data and stats + */ +function categorizeCommits(rawCommits: RawCommitInfo[]): RawChangelogResult { + const rawConfig = readReleaseConfig(); + const releaseConfig = normalizeReleaseConfig(rawConfig); + + const categories = new Map(); + const leftovers: Commit[] = []; + const missing: RawCommitInfo[] = []; + + // Track bump type for auto-versioning (lower priority value = higher bump) + let bumpPriority: number | null = null; + let matchedCommitsWithSemver = 0; - if (shouldExcludePR(labels, author, releaseConfig)) { + for (const raw of rawCommits) { + const labels = new Set(raw.labels); + + if (shouldExcludePR(labels, raw.author, releaseConfig)) { continue; } // Use PR title if available, otherwise use commit title for pattern matching - const titleForMatching = githubCommit?.prTitle ?? gitCommit.title; - const categoryTitle = matchPRToCategory( + const titleForMatching = (raw.prTitle ?? raw.title).trim(); + const matchedCategory = matchCommitToCategory( labels, - author, + raw.author, titleForMatching, releaseConfig ); + const categoryTitle = matchedCategory?.title ?? null; + + // Track bump type if category has semver field + if (matchedCategory?.semver) { + const priority = BUMP_TYPES.get(matchedCategory.semver); + if (priority !== undefined) { + matchedCommitsWithSemver++; + bumpPriority = Math.min(bumpPriority ?? priority, priority); + } + } - const commit: Commit = { - author: author, - hash: hash, - title: gitCommit.title, - body: gitCommit.body, - hasPRinTitle: Boolean(gitCommit.pr), - // Use GitHub PR number, falling back to locally parsed PR from title - pr: githubCommit?.pr ?? gitCommit.pr ?? null, - prTitle: githubCommit?.prTitle ?? null, - prBody: githubCommit?.prBody ?? null, - labels: labelsArray, - category: categoryTitle, - }; - commits[hash] = commit; - - if (!githubCommit) { - missing.push(commit); + // Track commits not found on GitHub (for warning) + if (!raw.pr && raw.hash) { + missing.push(raw); } - if (!categoryTitle) { - leftovers.push(commit); + if (!categoryTitle || !raw.pr) { + // No category match or no PR - goes to leftovers + leftovers.push({ + author: raw.author, + hash: raw.hash, + title: raw.title, + body: raw.body, + hasPRinTitle: Boolean(raw.pr), + pr: raw.pr ?? null, + prTitle: raw.prTitle ?? null, + prBody: raw.prBody ?? null, + labels: raw.labels, + category: categoryTitle, + highlight: raw.highlight, + }); } else { - if (!commit.pr) { - leftovers.push(commit); - } else { - let category = categories.get(categoryTitle); - if (!category) { - category = { - title: categoryTitle, - scopeGroups: new Map(), - }; - categories.set(categoryTitle, category); - } + // Has category and PR - add to category + let category = categories.get(categoryTitle); + if (!category) { + category = { + title: categoryTitle, + scopeGroups: new Map(), + }; + categories.set(categoryTitle, category); + } - // Extract and normalize scope from PR title - const prTitle = commit.prTitle ?? commit.title; - const scope = extractScope(prTitle); + const prTitle = (raw.prTitle ?? raw.title).trim(); + const scope = extractScope(prTitle); - // Get or create the scope group - let scopeGroup = category.scopeGroups.get(scope); - if (!scopeGroup) { - scopeGroup = []; - category.scopeGroups.set(scope, scopeGroup); - } + let scopeGroup = category.scopeGroups.get(scope); + if (!scopeGroup) { + scopeGroup = []; + category.scopeGroups.set(scope, scopeGroup); + } - // Check for custom changelog entries in the PR body - const customChangelogEntries = extractChangelogEntry(commit.prBody); - - if (customChangelogEntries) { - // If there are multiple changelog entries, add each as a separate item - for (const entry of customChangelogEntries) { - scopeGroup.push({ - author: commit.author, - number: commit.pr, - hash: commit.hash, - body: entry.nestedContent ?? '', - title: entry.text, - }); - } - } else { - // No custom entry, use PR title as before + // Check for custom changelog entries in the PR body + const customChangelogEntries = extractChangelogEntry(raw.prBody); + + if (customChangelogEntries) { + // If there are multiple changelog entries, add each as a separate item + for (const entry of customChangelogEntries) { scopeGroup.push({ - author: commit.author, - number: commit.pr, - hash: commit.hash, - body: commit.prBody ?? '', - title: prTitle, + author: raw.author, + number: raw.pr, + hash: raw.hash, + body: entry.nestedContent ?? '', + title: entry.text, + highlight: raw.highlight, }); } + } else { + // No custom entry, use PR title as before + scopeGroup.push({ + author: raw.author, + number: raw.pr, + hash: raw.hash, + body: raw.prBody ?? '', + title: prTitle, + highlight: raw.highlight, + }); + } + } + } + + // Convert priority back to bump type + let bumpType: BumpType | null = null; + if (bumpPriority !== null) { + for (const [type, priority] of BUMP_TYPES) { + if (priority === bumpPriority) { + bumpType = type; + break; } } } @@ -917,11 +1227,57 @@ export async function generateChangesetFromGit( if (missing.length > 0) { logger.warn( 'The following commits were not found on GitHub:', - missing.map(commit => `${commit.hash.slice(0, 8)} ${commit.title}`) + missing.map(c => `${c.hash.slice(0, 8)} ${c.title}`) ); } - const changelogSections = []; + return { + data: { + categories, + leftovers, + releaseConfig, + }, + stats: { + bumpType, + totalCommits: rawCommits.length, + matchedCommitsWithSemver, + }, + }; +} + +/** + * Generates raw changelog data from git history. + * Convenience function that fetches commits and categorizes them. + * + * @param git Local git client + * @param rev Base revision (tag or SHA) to generate changelog from + * @param until Optional end revision (defaults to HEAD) + * @returns Raw changelog data structure + */ +async function generateRawChangelog( + git: SimpleGit, + rev: string, + until?: string +): Promise { + const rawCommits = await fetchRawCommitInfo(git, rev, until); + return categorizeCommits(rawCommits); +} + +/** + * Serializes raw changelog data to markdown format. + * Entries with `highlight: true` are rendered as blockquotes. + * + * @param rawData The raw changelog data to serialize + * @param maxLeftovers Maximum number of leftover entries to include + * @returns Formatted markdown changelog string + */ +async function serializeChangelog( + rawData: RawChangelogData, + maxLeftovers: number +): Promise { + const { categories, leftovers, releaseConfig } = rawData; + + const changelogSections: string[] = []; const { repo, owner } = await getGlobalGitHubConfig(); const repoUrl = `https://github.com/${owner}/${repo}`; @@ -936,7 +1292,7 @@ export async function generateChangesetFromGit( // Sort categories by the order defined in release config const categoryOrder = - releaseConfig?.changelog.categories.map(c => c.title) ?? []; + releaseConfig?.changelog?.categories?.map(c => c.title) ?? []; const sortedCategories = [...categories.entries()].sort((a, b) => { const aIndex = categoryOrder.indexOf(a[1].title); const bIndex = categoryOrder.indexOf(b[1].title); @@ -968,17 +1324,15 @@ export async function generateChangesetFromGit( return scopeA.localeCompare(scopeB); }); - for (const [scope, prs] of sortedScopes) { - // Add scope header if: - // - scope grouping is enabled AND - // - scope exists (not null) AND - // - there's more than one entry in this scope (single entry headers aren't useful) - if (scopeGroupingEnabled && scope !== null && prs.length > 1) { - changelogSections.push( - markdownHeader(SCOPE_HEADER_LEVEL, formatScopeTitle(scope)) - ); - } + // Check if any scope has multiple entries (would get a header) + const hasScopeHeaders = [...category.scopeGroups.entries()].some( + ([s, entries]) => s !== null && entries.length > 1 + ); + + // Collect entries without headers to combine them into a single section + const entriesWithoutHeaders: string[] = []; + for (const [scope, prs] of sortedScopes) { const prEntries = prs.map(pr => formatChangelogEntry({ title: pr.title, @@ -987,10 +1341,35 @@ export async function generateChangesetFromGit( hash: pr.hash, body: pr.body, repoUrl, + highlight: pr.highlight, }) ); - changelogSections.push(prEntries.join('\n')); + // Determine scope header: + // - Scoped entries with multiple PRs get formatted scope title + // - Scopeless entries get "Other" header when other scope headers exist + // - Otherwise no header (entries collected for later) + let scopeHeader: string | null = null; + if (scopeGroupingEnabled) { + if (scope !== null && prs.length > 1) { + scopeHeader = formatScopeTitle(scope); + } else if (scope === null && hasScopeHeaders) { + scopeHeader = 'Other'; + } + } + + if (scopeHeader) { + changelogSections.push(markdownHeader(SCOPE_HEADER_LEVEL, scopeHeader)); + changelogSections.push(prEntries.join('\n')); + } else { + // No header for this scope group - collect entries to combine later + entriesWithoutHeaders.push(...prEntries); + } + } + + // Push all entries without headers as a single section to avoid extra newlines + if (entriesWithoutHeaders.length > 0) { + changelogSections.push(entriesWithoutHeaders.join('\n')); } } @@ -1023,7 +1402,7 @@ export async function generateChangesetFromGit( // No custom entry, use PR title or commit title as before leftoverEntries.push( formatChangelogEntry({ - title: commit.prTitle ?? commit.title, + title: (commit.prTitle ?? commit.title).trim(), author: commit.author, prNumber: commit.pr ?? undefined, hash: commit.hash, @@ -1034,6 +1413,7 @@ export async function generateChangesetFromGit( : commit.body.includes(BODY_IN_CHANGELOG_MAGIC_WORD) ? commit.body : undefined, + highlight: commit.highlight, }) ); } @@ -1047,6 +1427,24 @@ export async function generateChangesetFromGit( return changelogSections.join('\n\n'); } +/** + * Implementation of changelog generation that uses the new architecture. + * Generates raw data, then serializes to markdown. + */ +async function generateChangesetFromGitImpl( + git: SimpleGit, + rev: string, + maxLeftovers: number +): Promise { + const { data: rawData, stats } = await generateRawChangelog(git, rev); + const changelog = await serializeChangelog(rawData, maxLeftovers); + + return { + changelog, + ...stats, + }; +} + interface CommitInfo { author: { user?: { login: string }; @@ -1076,7 +1474,7 @@ interface CommitInfoResult { repository: CommitInfoMap; } -async function getPRAndLabelsFromCommit(hashes: string[]): Promise< +export async function getPRAndLabelsFromCommit(hashes: string[]): Promise< Record< /* hash */ string, { diff --git a/src/utils/git.ts b/src/utils/git.ts index 35ff3470..e74273dc 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -55,10 +55,11 @@ export async function getLatestTag(git: SimpleGit): Promise { export async function getChangesSince( git: SimpleGit, - rev: string + rev: string, + until?: string ): Promise { const gitLogArgs: Options | LogOptions = { - to: 'HEAD', + to: until || 'HEAD', // The symmetric option defaults to true, giving us all the different commits // reachable from both `from` and `to` whereas what we are interested in is only the ones // reachable from `to` and _not_ from `from` so we get a "changelog" kind of list. diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 6df104f7..da95e731 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,3 +1,5 @@ +import { appendFileSync } from 'fs'; + import prompts from 'prompts'; import { logger, LogLevel, setLevel } from '../logger'; @@ -59,3 +61,26 @@ export async function promptConfirmation(): Promise { export function hasInput(): boolean { return !GLOBAL_FLAGS['no-input']; } + +/** + * Sets a GitHub Actions output variable. + * Automatically uses heredoc-style delimiter syntax for multiline values. + * No-op when not running in GitHub Actions. + */ +export function setGitHubActionsOutput(name: string, value: string): void { + const outputFile = process.env.GITHUB_OUTPUT; + if (!outputFile) { + return; + } + + if (value.includes('\n')) { + // Use heredoc-style delimiter for multiline values + const delimiter = `EOF_${Date.now()}_${Math.random().toString(36).slice(2)}`; + appendFileSync( + outputFile, + `${name}<<${delimiter}\n${value}\n${delimiter}\n` + ); + } else { + appendFileSync(outputFile, `${name}=${value}\n`); + } +} diff --git a/src/utils/system.ts b/src/utils/system.ts index 9c443c77..4ef1f10d 100644 --- a/src/utils/system.ts +++ b/src/utils/system.ts @@ -106,6 +106,8 @@ export interface SpawnProcessOptions { showStdout?: boolean; /** Force the process to run in dry-run mode */ enableInDryRunMode?: boolean; + /** Data to write to stdin (process will receive 'pipe' for stdin instead of 'inherit') */ + stdin?: string; } /** @@ -159,13 +161,23 @@ export async function spawnProcess( replaceEnvVariable(arg, { ...process.env, ...options.env }) ); - // Allow child to accept input - options.stdio = ['inherit', 'pipe', 'pipe']; + // Allow child to accept input (use 'pipe' for stdin if we need to write to it) + options.stdio = [ + spawnProcessOptions.stdin !== undefined ? 'pipe' : 'inherit', + 'pipe', + 'pipe', + ]; child = spawn(command, processedArgs, options); if (!child.stdout || !child.stderr) { throw new Error('Invalid standard output or error for child process'); } + + // Write stdin data if provided + if (spawnProcessOptions.stdin !== undefined && child.stdin) { + child.stdin.write(spawnProcessOptions.stdin); + child.stdin.end(); + } child.on('exit', code => (code === 0 ? succeed() : fail({ code }))); child.on('error', fail); diff --git a/src/utils/workspaces.ts b/src/utils/workspaces.ts new file mode 100644 index 00000000..b4fa5bfa --- /dev/null +++ b/src/utils/workspaces.ts @@ -0,0 +1,422 @@ +import { readFileSync } from 'fs'; +import * as path from 'path'; +import { load } from 'js-yaml'; +import { glob } from 'glob'; + +import { logger } from '../logger'; + +/** + * Check if an error is a "file not found" error + */ +function isNotFoundError(err: unknown): boolean { + return err instanceof Error && 'code' in err && err.code === 'ENOENT'; +} + +/** Information about a workspace package */ +export interface WorkspacePackage { + /** The package name from package.json */ + name: string; + /** Absolute path to the package directory */ + location: string; + /** Whether the package is private */ + private: boolean; + /** Whether the package has publishConfig.access set to 'public' */ + hasPublicAccess: boolean; + /** Dependencies that are also workspace packages */ + workspaceDependencies: string[]; +} + +/** Result of workspace discovery */ +export interface WorkspaceDiscoveryResult { + /** The type of workspace manager detected */ + type: 'npm' | 'yarn' | 'pnpm' | 'none'; + /** List of discovered packages */ + packages: WorkspacePackage[]; +} + +/** Structure of pnpm-workspace.yaml */ +interface PnpmWorkspaceConfig { + packages?: string[]; +} + +/** Parsed package.json structure */ +interface PackageJson { + name?: string; + workspaces?: string[] | { packages?: string[] }; + private?: boolean; + publishConfig?: { + access?: 'public' | 'restricted'; + }; + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + optionalDependencies?: Record; +} + +/** + * Read and parse a package.json file + */ +function readPackageJson(packagePath: string): PackageJson | null { + const packageJsonPath = path.join(packagePath, 'package.json'); + try { + return JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + } catch (err) { + if (!isNotFoundError(err)) { + logger.warn(`Failed to parse ${packageJsonPath}:`, err); + } + return null; + } +} + +/** + * Get all dependency names from a package.json + * Includes dependencies, peerDependencies, and optionalDependencies + * (not devDependencies as those don't need to be published first) + */ +function getAllDependencyNames(packageJson: PackageJson): string[] { + const deps = new Set(); + + for (const dep of Object.keys(packageJson.dependencies || {})) { + deps.add(dep); + } + for (const dep of Object.keys(packageJson.peerDependencies || {})) { + deps.add(dep); + } + for (const dep of Object.keys(packageJson.optionalDependencies || {})) { + deps.add(dep); + } + + return Array.from(deps); +} + +/** + * Extract workspaces array from package.json workspaces field + * Handles both array format and object format with packages property + */ +function extractWorkspacesGlobs( + workspaces: string[] | { packages?: string[] } | undefined +): string[] { + if (!workspaces) { + return []; + } + if (Array.isArray(workspaces)) { + return workspaces; + } + return workspaces.packages || []; +} + +/** + * Resolve glob patterns to actual package directories + */ +async function resolveWorkspaceGlobs( + rootDir: string, + patterns: string[] +): Promise { + // First: collect all workspace package names and locations + const workspaceLocations: Array<{ location: string; packageJson: PackageJson }> = []; + const workspaceNames = new Set(); + + for (const pattern of patterns) { + const matches = await glob(pattern, { + cwd: rootDir, + absolute: true, + ignore: ['**/node_modules/**'], + }); + + for (const match of matches) { + const packageJson = readPackageJson(match); + if (packageJson?.name) { + workspaceLocations.push({ location: match, packageJson }); + workspaceNames.add(packageJson.name); + } + } + } + + // Now resolve dependencies in a single pass, filtering against known workspace names + return workspaceLocations.map(({ location, packageJson }) => ({ + name: packageJson.name as string, + location, + private: packageJson.private ?? false, + hasPublicAccess: packageJson.publishConfig?.access === 'public', + workspaceDependencies: getAllDependencyNames(packageJson).filter(dep => + workspaceNames.has(dep) + ), + })); +} + +/** + * Check if a file exists by trying to read it + */ +function fileExists(filePath: string): boolean { + try { + readFileSync(filePath); + return true; + } catch (err) { + return false; + } +} + +/** + * Discover npm/yarn workspaces from package.json + */ +async function discoverNpmYarnWorkspaces( + rootDir: string +): Promise { + const packageJson = readPackageJson(rootDir); + if (!packageJson) { + return null; + } + + const workspacesGlobs = extractWorkspacesGlobs(packageJson.workspaces); + if (workspacesGlobs.length === 0) { + return null; + } + + // Detect if it's yarn or npm based on lock files + const type = fileExists(path.join(rootDir, 'yarn.lock')) ? 'yarn' : 'npm'; + + const packages = await resolveWorkspaceGlobs(rootDir, workspacesGlobs); + + logger.debug( + `Discovered ${ + packages.length + } ${type} workspace packages from ${workspacesGlobs.join(', ')}` + ); + + return { type, packages }; +} + +/** + * Discover pnpm workspaces from pnpm-workspace.yaml + */ +async function discoverPnpmWorkspaces( + rootDir: string +): Promise { + const pnpmWorkspacePath = path.join(rootDir, 'pnpm-workspace.yaml'); + + let config: PnpmWorkspaceConfig; + try { + const content = readFileSync(pnpmWorkspacePath, 'utf-8'); + config = load(content) as PnpmWorkspaceConfig; + } catch (err) { + if (!isNotFoundError(err)) { + logger.warn(`Failed to parse ${pnpmWorkspacePath}:`, err); + } + return null; + } + + const patterns = config.packages || []; + if (patterns.length === 0) { + return null; + } + + const packages = await resolveWorkspaceGlobs(rootDir, patterns); + + logger.debug( + `Discovered ${packages.length} pnpm workspace packages from ${patterns.join( + ', ' + )}` + ); + + return { type: 'pnpm', packages }; +} + +/** + * Discover all workspace packages in a monorepo + * + * Supports: + * - npm workspaces (package.json "workspaces" field) + * - yarn workspaces (package.json "workspaces" field) + * - pnpm workspaces (pnpm-workspace.yaml) + * + * @param rootDir Root directory of the monorepo + * @returns Discovery result with type and packages, or null if not a workspace + */ +export async function discoverWorkspaces( + rootDir: string +): Promise { + // Try pnpm first (more specific) + const pnpmResult = await discoverPnpmWorkspaces(rootDir); + if (pnpmResult) { + return pnpmResult; + } + + // Try npm/yarn workspaces + const npmYarnResult = await discoverNpmYarnWorkspaces(rootDir); + if (npmYarnResult) { + return npmYarnResult; + } + + // No workspaces found + return { type: 'none', packages: [] }; +} + +/** + * Convert a package name to an artifact filename pattern + * + * Default convention: + * - @sentry/browser -> sentry-browser-\d.*\.tgz + * - @sentry-internal/browser-utils -> sentry-internal-browser-utils-\d.*\.tgz + * + * @param packageName The npm package name + * @returns A regex pattern string to match the artifact + */ +export function packageNameToArtifactPattern(packageName: string): string { + // Remove @ prefix, replace / with - + const normalized = packageName.replace(/^@/, '').replace(/\//g, '-'); + // Create a regex pattern that matches the artifact filename + return `/^${normalized}-\\d.*\\.tgz$/`; +} + +/** + * Escape special regex characters in a string. + * Only escapes characters that have special meaning in regex. + */ +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'); +} + +/** + * Convert a package name to an artifact filename using a template + * + * Template variables: + * - {{name}}: The package name (e.g., @sentry/browser) + * - {{simpleName}}: Simplified name (e.g., sentry-browser) + * - {{version}}: The version string + * + * @param packageName The npm package name + * @param template The artifact template string + * @param version Optional version to substitute + * @returns The artifact filename pattern + */ +export function packageNameToArtifactFromTemplate( + packageName: string, + template: string, + version = '\\d.*' +): string { + const simpleName = packageName.replace(/^@/, '').replace(/\//g, '-'); + + // Use placeholders to preserve template markers during escaping + const NAME_PLACEHOLDER = '\x00NAME\x00'; + const SIMPLE_PLACEHOLDER = '\x00SIMPLE\x00'; + const VERSION_PLACEHOLDER = '\x00VERSION\x00'; + + // Replace template markers with placeholders + let result = template + .replace(/\{\{name\}\}/g, NAME_PLACEHOLDER) + .replace(/\{\{simpleName\}\}/g, SIMPLE_PLACEHOLDER) + .replace(/\{\{version\}\}/g, VERSION_PLACEHOLDER); + + // Escape regex special characters in the template + result = escapeRegex(result); + + // Replace placeholders with escaped values (or regex pattern for version) + // If version is the default regex pattern, use it as-is; otherwise escape it + const versionValue = version === '\\d.*' ? version : escapeRegex(version); + result = result + .replace( + new RegExp(escapeRegex(NAME_PLACEHOLDER), 'g'), + escapeRegex(packageName) + ) + .replace( + new RegExp(escapeRegex(SIMPLE_PLACEHOLDER), 'g'), + escapeRegex(simpleName) + ) + .replace(new RegExp(escapeRegex(VERSION_PLACEHOLDER), 'g'), versionValue); + + return `/^${result}$/`; +} + +/** + * Filter workspace packages based on include/exclude patterns + * + * @param packages List of workspace packages + * @param includePattern Optional regex pattern to include packages + * @param excludePattern Optional regex pattern to exclude packages + * @returns Filtered list of packages + */ +export function filterWorkspacePackages( + packages: WorkspacePackage[], + includePattern?: RegExp, + excludePattern?: RegExp +): WorkspacePackage[] { + return packages.filter(pkg => { + // Check exclude pattern first + if (excludePattern && excludePattern.test(pkg.name)) { + return false; + } + // Check include pattern + if (includePattern && !includePattern.test(pkg.name)) { + return false; + } + return true; + }); +} + +/** + * Topologically sort workspace packages based on their dependencies. + * Packages with no dependencies come first, then packages that depend on them, etc. + * + * Computes depth for each package (depth = 1 + max depth of dependencies) + * and sorts by depth ascending. + * + * @param packages List of workspace packages + * @returns Sorted list of packages (dependencies before dependents) + * @throws Error if there's a circular dependency + */ +export function topologicalSortPackages( + packages: WorkspacePackage[] +): WorkspacePackage[] { + // Map package name to its workspace dependencies + const depsMap = new Map(); + for (const pkg of packages) { + depsMap.set(pkg.name, pkg.workspaceDependencies); + } + + // Compute depth for each package using memoization + // Depth = 1 + max(depth of dependencies), or 0 if no dependencies + const depths = new Map(); + const computing = new Set(); // Tracks recursion stack for cycle detection + + function computeDepth(name: string): number { + const cached = depths.get(name); + if (cached !== undefined) { + return cached; + } + + if (computing.has(name)) { + const cyclePackages = Array.from(computing); + throw new Error( + `Circular dependency detected among workspace packages: ${cyclePackages.join(', ')}` + ); + } + + computing.add(name); + + let maxDepDepth = -1; + for (const dep of depsMap.get(name) || []) { + // Only consider dependencies that are in our package list + if (depsMap.has(dep)) { + maxDepDepth = Math.max(maxDepDepth, computeDepth(dep)); + } + } + + computing.delete(name); + + const depth = maxDepDepth + 1; + depths.set(name, depth); + return depth; + } + + // Compute depths for all packages + for (const name of depsMap.keys()) { + computeDepth(name); + } + + // Sort by depth (packages with lower depth come first) + return [...packages].sort((a, b) => { + const depthA = depths.get(a.name) ?? 0; + const depthB = depths.get(b.name) ?? 0; + return depthA - depthB; + }); +} diff --git a/yarn.lock b/yarn.lock index 4d6bfcd3..e654e11e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -985,6 +985,18 @@ dependencies: "@isaacs/balanced-match" "^4.0.1" +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" @@ -2480,14 +2492,7 @@ resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz" integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== -"@types/yargs@^15.0.3": - version "15.0.20" - resolved "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.20.tgz" - integrity sha512-KIkX+/GgfFitlASYCGoSF+T4XRXhOubJLhkLVtSfsRTe9jWMmuM2g28zQ41BtPTG7TRBb2xHW+LCNVE9QR/vsg== - dependencies: - "@types/yargs-parser" "*" - -"@types/yargs@^17.0.8": +"@types/yargs@^17", "@types/yargs@^17.0.8": version "17.0.35" resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz" integrity sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg== @@ -2656,7 +2661,7 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.21.3" -"ansi-regex@>=5.0.1 < 6.0.0", ansi-regex@^2.1.1, ansi-regex@^4.1.0, ansi-regex@^5.0.1: +"ansi-regex@>=5.0.1 < 6.0.0", ansi-regex@^2.1.1, ansi-regex@^4.1.0, ansi-regex@^5.0.1, ansi-regex@^6.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== @@ -2680,6 +2685,11 @@ ansi-styles@^5.0.0: resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +ansi-styles@^6.1.0, ansi-styles@^6.2.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" + integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz" @@ -2950,7 +2960,7 @@ callsites@^3.0.0: resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase@^5.0.0, camelcase@^5.3.1: +camelcase@^5.3.1: version "5.3.1" resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== @@ -3043,15 +3053,6 @@ cli-table@0.3.1: dependencies: colors "1.0.3" -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" - cliui@^8.0.1: version "8.0.1" resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" @@ -3061,6 +3062,15 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" +cliui@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-9.0.1.tgz#6f7890f386f6f1f79953adc1f78dec46fcc2d291" + integrity sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w== + dependencies: + string-width "^7.2.0" + strip-ansi "^7.1.0" + wrap-ansi "^9.0.0" + clone@^1.0.2: version "1.0.4" resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" @@ -3159,7 +3169,7 @@ create-jest@^29.7.0: jest-util "^29.7.0" prompts "^2.0.1" -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -3195,11 +3205,6 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" - integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== - dedent@^1.0.0: version "1.7.0" resolved "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz" @@ -3282,6 +3287,11 @@ duplexify@^4.0.0, duplexify@^4.1.1: readable-stream "^3.1.1" stream-shift "^1.0.2" +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: version "1.0.11" resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" @@ -3299,11 +3309,21 @@ emittery@^0.13.1: resolved "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz" integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== +emoji-regex@^10.3.0: + version "10.6.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.6.0.tgz#bf3d6e8f7f8fd22a65d9703475bc0147357a6b0d" + integrity sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.5" resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz" @@ -3767,6 +3787,14 @@ flatted@^3.2.9: resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz" integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== +foreground-child@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== + dependencies: + cross-spawn "^7.0.6" + signal-exit "^4.0.1" + form-data@^4.0.4: version "4.0.4" resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz" @@ -3839,11 +3867,16 @@ gensync@^1.0.0-beta.2: resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-caller-file@^2.0.0, get-caller-file@^2.0.1, get-caller-file@^2.0.5: +get-caller-file@^2.0.0, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-east-asian-width@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz#9bc4caa131702b4b61729cb7e42735bc550c9ee6" + integrity sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q== + get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" @@ -3921,6 +3954,18 @@ glob@*: minipass "^7.1.2" path-scurry "^2.0.0" +glob@^11.0.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-11.1.0.tgz#4f826576e4eb99c7dad383793d2f9f08f67e50a6" + integrity sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw== + dependencies: + foreground-child "^3.3.1" + jackspeak "^4.1.1" + minimatch "^10.1.1" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^2.0.0" + glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" @@ -4308,6 +4353,13 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +jackspeak@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.1.1.tgz#96876030f450502047fc7e8c7fcf8ce8124e43ae" + integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + jest-changed-files@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz" @@ -4764,9 +4816,9 @@ json5@^2.2.3: resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jwa@^2.0.0: +jwa@^2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== dependencies: buffer-equal-constant-time "^1.0.1" @@ -4774,11 +4826,11 @@ jwa@^2.0.0: safe-buffer "^5.0.1" jws@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz" - integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + version "4.0.1" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690" + integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA== dependencies: - jwa "^2.0.0" + jwa "^2.0.1" safe-buffer "^5.0.1" keyv@^4.5.3: @@ -5194,6 +5246,11 @@ p-try@^2.0.0: resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -5454,11 +5511,6 @@ require-in-the-middle@^8.0.0: debug "^4.3.5" module-details-from-path "^1.0.3" -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" @@ -5561,11 +5613,6 @@ semver@^7.2.1, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4, semver@^7.7.3: resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz" integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" - integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== - set-value@>=2.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/set-value/-/set-value-4.1.0.tgz#aa433662d87081b75ad88a4743bd450f044e7d09" @@ -5596,6 +5643,11 @@ signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + simple-git@^3.6.0: version "3.30.0" resolved "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz" @@ -5697,6 +5749,15 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" @@ -5706,6 +5767,24 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string-width@^7.0.0, string-width@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" @@ -5713,6 +5792,13 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz" @@ -5727,6 +5813,13 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-ansi@^7.0.1, strip-ansi@^7.1.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" + integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== + dependencies: + ansi-regex "^6.0.1" + strip-bom@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" @@ -6065,11 +6158,6 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -which-module@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz" - integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== - which@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" @@ -6087,10 +6175,10 @@ wordwrap@^1.0.0: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" string-width "^4.1.0" @@ -6105,6 +6193,24 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrap-ansi@^9.0.0: + version "9.0.2" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz#956832dea9494306e6d209eb871643bb873d7c98" + integrity sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww== + dependencies: + ansi-styles "^6.2.1" + string-width "^7.0.0" + strip-ansi "^7.1.0" + wrappy@1: version "1.0.2" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" @@ -6138,11 +6244,6 @@ xtend@^4.0.0: resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -y18n@^4.0.0: - version "4.0.3" - resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz" - integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== - y18n@^5.0.5: version "5.0.8" resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" @@ -6158,30 +6259,10 @@ yallist@^4.0.0: resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yargs-parser@^18.1.2, yargs-parser@^21.1.1, yargs-parser@~18.1.3: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs@15.4.1: - version "15.4.1" - resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" +yargs-parser@>=18.1.3, yargs-parser@^21.1.1, yargs-parser@^22.0.0: + version "22.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-22.0.0.tgz#87b82094051b0567717346ecd00fd14804b357c8" + integrity sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw== yargs@^17.3.1: version "17.7.2" @@ -6196,6 +6277,18 @@ yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.1.1" +yargs@^18: + version "18.0.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-18.0.0.tgz#6c84259806273a746b09f579087b68a3c2d25bd1" + integrity sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg== + dependencies: + cliui "^9.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + string-width "^7.2.0" + y18n "^5.0.5" + yargs-parser "^22.0.0" + yauzl@^2.10.0: version "2.10.0" resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz"